From af325768f0a7fd10e5eb3ba5d1e25237a42a8cd7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:38:23 +0200 Subject: [PATCH] chore: wire up nav (#36) - deduplicate blog posts logic, add to nav, use correct render function for blog posts - wire up docs to nav - wire up tutorial, remove examples reference --- apps/svelte.dev/content/docs/index.md | 3 + apps/svelte.dev/src/lib/server/content.ts | 12 ++ apps/svelte.dev/src/lib/server/docs/index.js | 162 ------------------ .../svelte.dev/src/lib/server/docs/types.d.ts | 26 --- apps/svelte.dev/src/lib/server/renderer.js | 52 ------ apps/svelte.dev/src/lib/server/renderer.ts | 92 ++++++++++ .../src/routes/blog/+page.server.js | 16 +- .../src/routes/blog/[slug]/+page.server.js | 4 +- .../src/routes/docs/[...path]/+page.server.js | 98 +---------- .../svelte.dev/src/routes/nav.json/+server.js | 86 ---------- .../svelte.dev/src/routes/nav.json/+server.ts | 76 ++++++++ 11 files changed, 188 insertions(+), 439 deletions(-) create mode 100644 apps/svelte.dev/content/docs/index.md delete mode 100644 apps/svelte.dev/src/lib/server/docs/index.js delete mode 100644 apps/svelte.dev/src/lib/server/docs/types.d.ts delete mode 100644 apps/svelte.dev/src/lib/server/renderer.js create mode 100644 apps/svelte.dev/src/lib/server/renderer.ts delete mode 100644 apps/svelte.dev/src/routes/nav.json/+server.js create mode 100644 apps/svelte.dev/src/routes/nav.json/+server.ts diff --git a/apps/svelte.dev/content/docs/index.md b/apps/svelte.dev/content/docs/index.md new file mode 100644 index 000000000..10e4da03d --- /dev/null +++ b/apps/svelte.dev/content/docs/index.md @@ -0,0 +1,3 @@ +--- +title: Docs +--- diff --git a/apps/svelte.dev/src/lib/server/content.ts b/apps/svelte.dev/src/lib/server/content.ts index 20ff88d16..c7e722c64 100644 --- a/apps/svelte.dev/src/lib/server/content.ts +++ b/apps/svelte.dev/src/lib/server/content.ts @@ -15,3 +15,15 @@ const assets = import.meta.glob('../../../content/**/+assets/**', { // https://github.com/vitejs/vite/issues/17453 export const index = await create_index(documents, assets, '../../../content', read); + +export const blog_posts = index.blog.children + .map((document) => { + return { + slug: document.slug, + title: document.metadata.title, + date: document.metadata.date, + description: document.metadata.description, + draft: document.metadata.draft + }; + }) + .sort((a, b) => (a.date < b.date ? 1 : -1)); diff --git a/apps/svelte.dev/src/lib/server/docs/index.js b/apps/svelte.dev/src/lib/server/docs/index.js deleted file mode 100644 index 3aca9a86d..000000000 --- a/apps/svelte.dev/src/lib/server/docs/index.js +++ /dev/null @@ -1,162 +0,0 @@ -import { base as app_base } from '$app/paths'; -import { - escape, - extractFrontmatter, - markedTransform, - normalizeSlugify, - removeMarkdown -} from '@sveltejs/site-kit/markdown'; -import { CONTENT_BASE_PATHS } from '../../../constants.js'; -import { render_content } from '../renderer'; - -/** - * @param {import('./types').DocsData} docs_data - * @param {string} slug - */ -export async function get_parsed_docs(docs_data, section, slug) { - for (const { pages } of docs_data) { - for (const page of pages) { - if (page.slug === slug) { - return { - ...page, - content: await render_content(page.file, page.content) - }; - } - } - } - - return null; -} - -/** @return {Promise} */ -export async function get_docs_data(base = CONTENT_BASE_PATHS.DOCS) { - const { readdir, readFile } = await import('node:fs/promises'); - - /** @type {import('./types').DocsData} */ - const docs_data = []; - - for (const category_dir of await readdir(base)) { - const match = /\d{2}-(.+)/.exec(category_dir); - if (!match) continue; - - const category_slug = match[1]; - - // Read the meta.json - const { title: category_title, draft = 'false' } = JSON.parse( - await readFile(`${base}/${category_dir}/meta.json`, 'utf-8') - ); - - if (draft === 'true') continue; - - /** @type {import('./types').Category} */ - const category = { - title: category_title, - slug: category_slug, - pages: [] - }; - - for (const filename of await readdir(`${base}/${category_dir}`)) { - if (filename === 'meta.json') continue; - const match = /\d{2}-(.+)/.exec(filename); - if (!match) continue; - - const page_slug = match[1].replace('.md', ''); - - const page_data = extractFrontmatter( - await readFile(`${base}/${category_dir}/${filename}`, 'utf-8') - ); - - if (page_data.metadata.draft === 'true') continue; - - const page_title = page_data.metadata.title; - const page_content = page_data.body; - - category.pages.push({ - title: page_title, - slug: page_slug, - content: page_content, - category: category_title, - sections: await get_sections(page_content), - path: `${app_base}/docs/${page_slug}`, - file: `${category_dir}/${filename}` - }); - } - - docs_data.push(category); - } - - return docs_data; -} - -/** @param {import('./types').DocsData} docs_data */ -export function get_docs_list(docs_data) { - return docs_data.map((category) => ({ - title: category.title, - pages: category.pages.map((page) => ({ - title: page.title, - path: page.path - })) - })); -} - -/** @param {string} str */ -const titled = async (str) => - removeMarkdown( - escape(await markedTransform(str, { paragraph: (txt) => txt })) - .replace(/<\/?code>/g, '') - .replace(/'/g, "'") - .replace(/"/g, '"') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/, '&') - .replace(/<(\/)?(em|b|strong|code)>/g, '') - ); - -/** - * @param {string} markdown - * @returns {Promise} - */ -export async function get_sections(markdown) { - const lines = markdown.split('\n'); - const root = /** @type {import('./types').Section} */ ({ - title: 'Root', - slug: 'root', - sections: [], - breadcrumbs: [''], - text: '' - }); - let currentNodes = [root]; - - for (const line of lines) { - const match = line.match(/^(#{2,4})\s(.*)/); - if (match) { - const level = match[1].length - 2; - const text = await titled(match[2]); - const slug = normalizeSlugify(text); - - // Prepare new node - /** @type {import('./types').Section} */ - const newNode = { - title: text, - slug, - sections: [], - breadcrumbs: [...currentNodes[level].breadcrumbs, text], - text: '' - }; - - // Add the new node to the tree - const sections = currentNodes[level].sections; - if (!sections) throw new Error(`Could not find section ${level}`); - sections.push(newNode); - - // Prepare for potential children of the new node - currentNodes = currentNodes.slice(0, level + 1); - currentNodes.push(newNode); - } else if (line.trim() !== '') { - // Add non-heading line to the text of the current section - currentNodes[currentNodes.length - 1].text += line + '\n'; - } - } - - return /** @type {import('./types').Section[]} */ (root.sections); -} diff --git a/apps/svelte.dev/src/lib/server/docs/types.d.ts b/apps/svelte.dev/src/lib/server/docs/types.d.ts deleted file mode 100644 index 99c732413..000000000 --- a/apps/svelte.dev/src/lib/server/docs/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type DocsData = Category[]; - -export interface Section { - title: string; - slug: string; - // Currently, we are only going with 2 level headings, so this will be undefined. In future, we may want to support 3 levels, in which case this will be a list of sections - sections?: Section[]; - breadcrumbs: string[]; - text: string; -} - -export type Category = { - title: string; - slug: string; - pages: Page[]; -}; - -export type Page = { - title: string; - category: string; - slug: string; - file: string; - path: string; - content: string; - sections: Section[]; -}; diff --git a/apps/svelte.dev/src/lib/server/renderer.js b/apps/svelte.dev/src/lib/server/renderer.js deleted file mode 100644 index aa3c0446f..000000000 --- a/apps/svelte.dev/src/lib/server/renderer.js +++ /dev/null @@ -1,52 +0,0 @@ -import { modules } from '$lib/generated/type-info'; -import { renderContentMarkdown, slugify } from '@sveltejs/site-kit/markdown'; - -/** - * @param {string} filename - * @param {string} body - * @returns - */ -export const render_content = (filename, body) => - renderContentMarkdown(filename, body, { - cacheCodeSnippets: true, - modules, - - resolveTypeLinks: (module_name, type_name) => { - return { - page: `/docs/${slugify(module_name)}`, - slug: `types-${slugify(type_name)}` - }; - }, - - twoslashBanner: (filename, source) => { - const injected = []; - - if (/(svelte)/.test(source) || filename.includes('typescript')) { - injected.push(`// @filename: ambient.d.ts`, `/// `); - } - - if (filename.includes('svelte-compiler')) { - injected.push('// @esModuleInterop'); - } - - if (filename.includes('svelte.md')) { - injected.push('// @errors: 2304'); - } - - // Actions JSDoc examples are invalid. Too many errors, edge cases - if (filename.includes('svelte-action')) { - injected.push('// @noErrors'); - } - - if (filename.includes('typescript')) { - injected.push('// @errors: 2304'); - } - - // Tutorials - if (filename.startsWith('tutorial')) { - injected.push('// @noErrors'); - } - - return injected.join('\n'); - } - }); diff --git a/apps/svelte.dev/src/lib/server/renderer.ts b/apps/svelte.dev/src/lib/server/renderer.ts new file mode 100644 index 000000000..e28b13d15 --- /dev/null +++ b/apps/svelte.dev/src/lib/server/renderer.ts @@ -0,0 +1,92 @@ +import { renderContentMarkdown } from '@sveltejs/site-kit/markdown'; + +export const render_content = (filename: string, body: string) => + renderContentMarkdown(filename, body, { + cacheCodeSnippets: true, + + // TODO these didn't work for a while in the old sites, too, investigate bringing back this functionality at some point + // resolveTypeLinks: (module_name, type_name) => { + // return { + // page: `/docs/${slugify(module_name)}`, + // slug: `types-${slugify(type_name)}` + // }; + // }, + + twoslashBanner: (filename, source) => { + // TODO these are copied from Svelte and SvelteKit - adjust for new filenames + const injected = []; + + if (/(svelte)/.test(source) || filename.includes('typescript')) { + injected.push(`// @filename: ambient.d.ts`, `/// `); + } + + if (filename.includes('svelte-compiler')) { + injected.push('// @esModuleInterop'); + } + + if (filename.includes('svelte.md')) { + injected.push('// @errors: 2304'); + } + + // Actions JSDoc examples are invalid. Too many errors, edge cases + // d.ts files are not properly supported right now, fix this later + if (filename.includes('svelte-action') || source.includes(' declare const ')) { + injected.push('// @noErrors'); + } + + if (filename.includes('typescript')) { + injected.push('// @errors: 2304'); + } + + if ( + source.includes('$app/') || + source.includes('$service-worker') || + source.includes('@sveltejs/kit/') + ) { + injected.push(`// @filename: ambient-kit.d.ts`, `/// `); + } + + if (source.includes('$env/')) { + // TODO we're hardcoding static env vars that are used in code examples + // in the types, which isn't... totally ideal, but will do for now + injected.push( + `declare module '$env/dynamic/private' { export const env: Record }`, + `declare module '$env/dynamic/public' { export const env: Record }`, + `declare module '$env/static/private' { export const API_KEY: string }`, + `declare module '$env/static/public' { export const PUBLIC_BASE_URL: string }` + ); + } + + if (source.includes('./$types') && !source.includes('@filename: $types.d.ts')) { + injected.push( + `// @filename: $types.d.ts`, + `import type * as Kit from '@sveltejs/kit';`, + `export type PageLoad = Kit.Load>;`, + `export type PageServerLoad = Kit.ServerLoad>;`, + `export type LayoutLoad = Kit.Load>;`, + `export type LayoutServerLoad = Kit.ServerLoad>;`, + `export type RequestHandler = Kit.RequestHandler>;`, + `export type Action = Kit.Action>;`, + `export type Actions = Kit.Actions>;`, + `export type EntryGenerator = () => Promise>> | Array>;` + ); + } + + // special case — we need to make allowances for code snippets coming + // from e.g. ambient.d.ts + if (filename.endsWith('$env-all.md') || filename.endsWith('$app-forms.md')) { + injected.push('// @errors: 7006 7031'); + } + + if (filename.endsWith('10-configuration.md')) { + injected.push('// @errors: 2307'); + } + + // another special case + if (source.includes('$lib/types')) { + injected.push(`declare module '$lib/types' { export interface User {} }`); + } + + return injected.join('\n'); + } + }); diff --git a/apps/svelte.dev/src/routes/blog/+page.server.js b/apps/svelte.dev/src/routes/blog/+page.server.js index e49bec135..491ba98b8 100644 --- a/apps/svelte.dev/src/routes/blog/+page.server.js +++ b/apps/svelte.dev/src/routes/blog/+page.server.js @@ -1,21 +1,9 @@ -import { index } from '$lib/server/content'; +import { blog_posts } from '$lib/server/content'; export const prerender = true; export async function load() { - const posts = index.blog.children - .map((document) => { - return { - slug: document.slug, - title: document.metadata.title, - date: document.metadata.date, - description: document.metadata.description, - draft: document.metadata.draft - }; - }) - .sort((a, b) => (a.date < b.date ? 1 : -1)); - return { - posts + posts: blog_posts }; } diff --git a/apps/svelte.dev/src/routes/blog/[slug]/+page.server.js b/apps/svelte.dev/src/routes/blog/[slug]/+page.server.js index 55bbd9c1b..205452758 100644 --- a/apps/svelte.dev/src/routes/blog/[slug]/+page.server.js +++ b/apps/svelte.dev/src/routes/blog/[slug]/+page.server.js @@ -1,6 +1,6 @@ import { error } from '@sveltejs/kit'; -import { markedTransform } from '@sveltejs/site-kit/markdown'; import { index } from '$lib/server/content'; +import { render_content } from '$lib/server/renderer'; export const prerender = true; @@ -36,7 +36,7 @@ export async function load({ params }) { path: `/${post.slug}`, date, date_formatted: format_date(date), - body: await markedTransform(post.body), + body: await render_content(post.file, post.body), authors, sections: post.sections }; diff --git a/apps/svelte.dev/src/routes/docs/[...path]/+page.server.js b/apps/svelte.dev/src/routes/docs/[...path]/+page.server.js index 6bcbd039a..b14a2fa09 100644 --- a/apps/svelte.dev/src/routes/docs/[...path]/+page.server.js +++ b/apps/svelte.dev/src/routes/docs/[...path]/+page.server.js @@ -1,6 +1,6 @@ import { index } from '$lib/server/content'; +import { render_content } from '$lib/server/renderer'; import { error, redirect } from '@sveltejs/kit'; -import { renderContentMarkdown } from '@sveltejs/site-kit/markdown'; export async function load({ params }) { const document = index[`docs/${params.path}`]; @@ -34,99 +34,3 @@ export async function load({ params }) { } }; } - -// TODO find a better place for this once everything settles down? -/** - * @param {string} filename - * @param {string} body - */ -const render_content = (filename, body) => - renderContentMarkdown(filename, body, { - cacheCodeSnippets: true, - - // TODO these didn't work for a while in the old sites, too, investigate bringing back this functionality at some point - // resolveTypeLinks: (module_name, type_name) => { - // return { - // page: `/docs/${slugify(module_name)}`, - // slug: `types-${slugify(type_name)}` - // }; - // }, - - twoslashBanner: (filename, source) => { - // TODO these are copied from Svelte and SvelteKit - adjust for new filenames - const injected = []; - - if (/(svelte)/.test(source) || filename.includes('typescript')) { - injected.push(`// @filename: ambient.d.ts`, `/// `); - } - - if (filename.includes('svelte-compiler')) { - injected.push('// @esModuleInterop'); - } - - if (filename.includes('svelte.md')) { - injected.push('// @errors: 2304'); - } - - // Actions JSDoc examples are invalid. Too many errors, edge cases - // d.ts files are not properly supported right now, fix this later - if (filename.includes('svelte-action') || source.includes(' declare const ')) { - injected.push('// @noErrors'); - } - - if (filename.includes('typescript')) { - injected.push('// @errors: 2304'); - } - - if ( - source.includes('$app/') || - source.includes('$service-worker') || - source.includes('@sveltejs/kit/') - ) { - injected.push(`// @filename: ambient-kit.d.ts`, `/// `); - } - - if (source.includes('$env/')) { - // TODO we're hardcoding static env vars that are used in code examples - // in the types, which isn't... totally ideal, but will do for now - injected.push( - `declare module '$env/dynamic/private' { export const env: Record }`, - `declare module '$env/dynamic/public' { export const env: Record }`, - `declare module '$env/static/private' { export const API_KEY: string }`, - `declare module '$env/static/public' { export const PUBLIC_BASE_URL: string }` - ); - } - - if (source.includes('./$types') && !source.includes('@filename: $types.d.ts')) { - injected.push( - `// @filename: $types.d.ts`, - `import type * as Kit from '@sveltejs/kit';`, - `export type PageLoad = Kit.Load>;`, - `export type PageServerLoad = Kit.ServerLoad>;`, - `export type LayoutLoad = Kit.Load>;`, - `export type LayoutServerLoad = Kit.ServerLoad>;`, - `export type RequestHandler = Kit.RequestHandler>;`, - `export type Action = Kit.Action>;`, - `export type Actions = Kit.Actions>;`, - `export type EntryGenerator = () => Promise>> | Array>;` - ); - } - - // special case — we need to make allowances for code snippets coming - // from e.g. ambient.d.ts - if (filename.endsWith('$env-all.md') || filename.endsWith('$app-forms.md')) { - injected.push('// @errors: 7006 7031'); - } - - if (filename.endsWith('10-configuration.md')) { - injected.push('// @errors: 2307'); - } - - // another special case - if (source.includes('$lib/types')) { - injected.push(`declare module '$lib/types' { export interface User {} }`); - } - - return injected.join('\n'); - } - }); diff --git a/apps/svelte.dev/src/routes/nav.json/+server.js b/apps/svelte.dev/src/routes/nav.json/+server.js deleted file mode 100644 index 2a2feda27..000000000 --- a/apps/svelte.dev/src/routes/nav.json/+server.js +++ /dev/null @@ -1,86 +0,0 @@ -import { get_docs_data, get_docs_list } from '$lib/server/docs/index.js'; -import { get_examples_list } from '$lib/server/examples/index.js'; -import examples_data from '$lib/generated/examples-data.js'; -import { json } from '@sveltejs/kit'; - -export const prerender = true; - -export const GET = async () => { - return json(await get_nav_list()); -}; - -/** - * @returns {Promise} - */ -async function get_nav_list() { - // const [docs_list, blog_list] = await Promise.all([ - // get_docs_list(await get_docs_data()), - // get_blog_list(await get_blog_data()) - // ]); - - // const processed_docs_list = docs_list.map(({ title, pages }) => ({ - // title, - // sections: pages.map(({ title, path }) => ({ title, path })) - // })); - - // const processed_blog_list = [ - // { - // title: '', - // sections: blog_list.map(({ title, slug, date }) => ({ - // title, - // path: '/blog/' + slug, - // // Put a NEW badge on blog posts that are less than 14 days old - // badge: (+new Date() - +new Date(date)) / (1000 * 60 * 60 * 24) < 14 ? 'NEW' : undefined - // })) - // } - // ]; - - // const examples_list = get_examples_list(examples_data); - // const processed_examples_list = examples_list - // .map(({ title, examples }) => ({ - // title, - // sections: examples.map(({ title, slug }) => ({ title, path: '/examples/' + slug })) - // })) - // .filter(({ title }) => title !== 'Embeds'); - - return [ - { - title: 'Docs', - prefix: 'docs', - pathname: '/docs', - sections: [ - { - title: 'DOCS', - sections: [] //processed_docs_list - } - ] - }, - { - title: 'Tutorial', - prefix: 'tutorial', - pathname: '/tutorial', - sections: [ - { - title: 'TUTORIAL', - sections: [] //processed_examples_list - } - ] - }, - { - title: 'REPL', - prefix: 'repl', - pathname: '/repl' - }, - { - title: 'Blog', - prefix: 'blog', - pathname: '/blog', - sections: [ - { - title: 'BLOG', - sections: [] //processed_blog_list - } - ] - } - ]; -} diff --git a/apps/svelte.dev/src/routes/nav.json/+server.ts b/apps/svelte.dev/src/routes/nav.json/+server.ts new file mode 100644 index 000000000..e71856e11 --- /dev/null +++ b/apps/svelte.dev/src/routes/nav.json/+server.ts @@ -0,0 +1,76 @@ +import { json } from '@sveltejs/kit'; +import { blog_posts, index } from '$lib/server/content'; +import type { NavigationLink } from '@sveltejs/site-kit'; + +export const prerender = true; + +export const GET = async () => { + return json(await get_nav_list()); +}; + +async function get_nav_list(): Promise { + const docs = index.docs.children.map((topic) => ({ + title: topic.metadata.title, + sections: topic.children.map((section) => ({ + title: section.metadata.title, + sections: section.children.map((page) => ({ + title: page.metadata.title, + path: '/' + page.slug + })) + })) + })); + + const blog = [ + { + title: '', + sections: blog_posts.map(({ title, slug, date }) => ({ + title, + path: '/' + slug, + // Put a NEW badge on blog posts that are less than 14 days old + badge: (+new Date() - +new Date(date)) / (1000 * 60 * 60 * 24) < 14 ? 'NEW' : undefined + })) + } + ]; + + const tutorial = index.tutorial.children.map((topic) => ({ + title: topic.metadata.title, + sections: topic.children.map((section) => ({ + title: section.metadata.title, + sections: section.children.map((page) => ({ + title: page.metadata.title, + path: '/tutorial/' + page.slug.split('/').pop() + })) + })) + })); + + return [ + { + title: 'Docs', + prefix: 'docs', + pathname: '/docs', + sections: docs + }, + { + title: 'Tutorial', + prefix: 'tutorial', + pathname: '/tutorial', + sections: tutorial + }, + { + title: 'REPL', + prefix: 'repl', + pathname: '/repl' + }, + { + title: 'Blog', + prefix: 'blog', + pathname: '/blog', + sections: [ + { + title: 'BLOG', + sections: blog + } + ] + } + ]; +}