3ae48af67c
This PR starts putting in place the high-level changes we want to make to the docs site. - It makes separate sections for Reference and Examples and Community. - Gets rid of the secondary sidebar and integrates it into the main sidebar. - Groups the reference articles by type. - Pulls in the examples alongside code and a live playground so people don't have to visit examples.tldraw.com separately. <img width="1458" alt="Screenshot 2024-01-30 at 09 43 46" src="https://github.com/tldraw/tldraw/assets/469604/4f5aa339-3a69-4d9b-9b9f-dfdddea623e8"> Again, this is the top-level changes and there's more to be done for the next PR(s): - create quick start page - clean up installation page - add accordion to Examples page prbly - put fun stuff in header (from footer) - landing page - something for landing page of API - search cmd-k and border - cleanup _sidebarReferenceContentLinks - external links _blank - address potential skew issue with code examples - have a link to other examples (next.js, etc.) ### Change Type - [x] `documentation` — Changes to the documentation only[^2] ### Test Plan 1. Make sure examples work! ### Release Notes - Rework our docs site to pull together the examples app and reference section more cohesively. --------- Co-authored-by: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: Mitja Bezenšek <mitja.bezensek@gmail.com> Co-authored-by: alex <alex@dytry.ch> Co-authored-by: Lu Wilson <l2wilson94@gmail.com> Co-authored-by: Dan Groshev <git@dgroshev.com>
170 lines
5 KiB
TypeScript
170 lines
5 KiB
TypeScript
import { SearchResult } from '@/types/search-types'
|
|
import { getDb } from '@/utils/ContentDatabase'
|
|
import assert from 'assert'
|
|
import { NextRequest } from 'next/server'
|
|
|
|
type Data = {
|
|
results: {
|
|
articles: SearchResult[]
|
|
apiDocs: SearchResult[]
|
|
}
|
|
status: 'success' | 'error' | 'no-query'
|
|
}
|
|
|
|
const BANNED_HEADINGS = ['new', 'constructor', 'properties', 'example', 'methods']
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const { searchParams } = new URL(req.url)
|
|
const query = searchParams.get('q')?.toLowerCase()
|
|
if (!query) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: {
|
|
articles: [],
|
|
apiDocs: [],
|
|
},
|
|
status: 'error',
|
|
error: 'No query',
|
|
}),
|
|
{
|
|
status: 400,
|
|
}
|
|
)
|
|
}
|
|
|
|
try {
|
|
const results: Data['results'] = {
|
|
articles: [],
|
|
apiDocs: [],
|
|
}
|
|
const db = await getDb()
|
|
|
|
const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb
|
|
|
|
const vdb = await getVectorDb()
|
|
const queryResults = await vdb.query(query, 25)
|
|
queryResults.sort((a, b) => b.score - a.score)
|
|
const headings = await Promise.all(
|
|
queryResults.map(async (result) => {
|
|
if (result.type !== 'heading') return // bleg
|
|
const article = await db.db.get(
|
|
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
|
|
result.id
|
|
)
|
|
assert(article, `No article found for heading ${result.id}`)
|
|
const category = await db.db.get(
|
|
`SELECT id, title FROM categories WHERE id = ?`,
|
|
article.categoryId
|
|
)
|
|
const section = await db.db.get(
|
|
`SELECT id, title FROM sections WHERE id = ?`,
|
|
article.sectionId
|
|
)
|
|
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
|
|
assert(heading, `No heading found for ${result.id} ${result.slug}`)
|
|
return {
|
|
id: result.id,
|
|
article,
|
|
category,
|
|
section,
|
|
heading,
|
|
score: result.score,
|
|
}
|
|
})
|
|
)
|
|
const visited = new Set<string>()
|
|
for (const result of headings) {
|
|
if (!result) continue
|
|
if (visited.has(result.id)) continue
|
|
visited.add(result.id)
|
|
const { category, section, article, heading, score } = result
|
|
const isUncategorized = category.id === section.id + '_ucg'
|
|
if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue
|
|
results[section.id === 'reference' ? 'apiDocs' : 'articles'].push({
|
|
id: result.id,
|
|
type: 'heading',
|
|
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
|
title:
|
|
section.id === 'reference'
|
|
? article.title + '.' + heading.title
|
|
: article.title + ': ' + heading.title,
|
|
url: isUncategorized
|
|
? `${section.id}/${article.id}#${heading.slug}`
|
|
: `${section.id}/${category.id}/${article.id}#${heading.slug}`,
|
|
score,
|
|
})
|
|
}
|
|
const articles = await Promise.all(
|
|
queryResults.map(async (result) => ({
|
|
score: result.score,
|
|
article: await db.db.get(
|
|
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
|
|
result.id
|
|
),
|
|
}))
|
|
)
|
|
for (const { score, article } of articles.filter(Boolean)) {
|
|
if (visited.has(article.id)) continue
|
|
visited.add(article.id)
|
|
const category = await db.db.get(
|
|
`SELECT id, title FROM categories WHERE categories.id = ?`,
|
|
article.categoryId
|
|
)
|
|
const section = await db.db.get(
|
|
`SELECT id, title FROM sections WHERE sections.id = ?`,
|
|
article.sectionId
|
|
)
|
|
const isUncategorized = category.id === section.id + '_ucg'
|
|
results[section.id === 'reference' ? 'apiDocs' : 'articles'].push({
|
|
id: article.id,
|
|
type: 'article',
|
|
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
|
title: article.title,
|
|
url: isUncategorized
|
|
? `${section.id}/${article.id}`
|
|
: `${section.id}/${category.id}/${article.id}`,
|
|
score,
|
|
})
|
|
}
|
|
const apiDocsScores = results.apiDocs.map((a) => a.score)
|
|
const maxScoreApiDocs = Math.max(...apiDocsScores)
|
|
const minScoreApiDocs = Math.min(...apiDocsScores)
|
|
const apiDocsBottom = minScoreApiDocs + (maxScoreApiDocs - minScoreApiDocs) * 0.75
|
|
results.apiDocs
|
|
.filter((a) => a.score > apiDocsBottom)
|
|
.sort((a, b) => b.score - a.score)
|
|
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
|
|
.slice(0, 10)
|
|
const articleScores = results.articles.map((a) => a.score)
|
|
const maxScoreArticles = Math.max(...articleScores)
|
|
const minScoreArticles = Math.min(...articleScores)
|
|
const articlesBottom = minScoreArticles + (maxScoreArticles - minScoreArticles) * 0.5
|
|
results.articles
|
|
.filter((a) => a.score > articlesBottom)
|
|
.sort((a, b) => b.score - a.score)
|
|
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
|
|
return new Response(
|
|
JSON.stringify({
|
|
results,
|
|
status: 'success',
|
|
}),
|
|
{
|
|
status: 200,
|
|
}
|
|
)
|
|
} catch (e: any) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: {
|
|
articles: [],
|
|
apiDocs: [],
|
|
},
|
|
status: 'error',
|
|
error: e.message,
|
|
}),
|
|
{
|
|
status: 500,
|
|
}
|
|
)
|
|
}
|
|
}
|