feat: add file system based static generation

This commit is contained in:
Youwen Wu 2024-04-07 16:51:05 -07:00
parent a3c61c2ed4
commit b8e9890a5f
Signed by: youwen5
GPG key ID: 865658ED1FE61EC3
17 changed files with 194 additions and 65 deletions

View file

@ -0,0 +1,18 @@
title = "A Brief Introduction to Taylor Series"
[manifest]
date = 2024-01-01T12:00:00Z
blurb = "The essence of calculus."
description = "A brief introduction to Taylor series, a powerful tool in calculus. We will discuss the intuition behind Taylor series, how to derive them, and how to use them to approximate functions. We will also discuss the limitations of Taylor series and how to overcome them. This article is aimed at beginners who are interested in calculus and want to learn more about Taylor series."
[manifest.tags]
primary = ["math", "calculus"]
secondary = ["taylor series", "numerical methods", "approximation"]
[manifest.authors]
["Youwen Wu"]
[cover]
src = "https://i.ytimg.com/vi/3d6DsjIBzJ4/maxresdefault.jpg"
alt = "visual of taylor series for a quadratic function"
caption = "Taylor series approximation of a quadratic function."

View file

@ -0,0 +1,3 @@
## This is some test content
Whatever gets written here will be rendered statically!

View file

@ -0,0 +1,16 @@
title = "My Awesome Blog Post"
[manifest]
date = 2024-01-01T12:00:00Z
author = "Youwen Wu"
blurb = "A blog post about Rust and WebAssembly"
description = "A blog post about Rust and WebAssembly, and how to use it in web development. This post will cover the basics of Rust and WebAssembly, and how to use it in web development. We will also discuss the benefits of using Rust and WebAssembly, and how it can help you build faster and more efficient web applications."
[manifest.tags]
primary = ["Rust", "WebAssembly"]
secondary = ["Web Development", "Frontend"]
[cover]
src = "cover.jpg"
alt = "A Rust and WebAssembly logo"
caption = "Rust and WebAssembly"

View file

@ -49,6 +49,7 @@
"@fontsource/zilla-slab": "^5.0.12", "@fontsource/zilla-slab": "^5.0.12",
"@svelte-put/toc": "^5.0.1", "@svelte-put/toc": "^5.0.1",
"@sveltejs/adapter-vercel": "^5.2.0", "@sveltejs/adapter-vercel": "^5.2.0",
"@types/node": "^20.12.5",
"bits-ui": "^0.21.2", "bits-ui": "^0.21.2",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@ -65,6 +66,7 @@
"svelte-sonner": "^0.3.21", "svelte-sonner": "^0.3.21",
"tailwind-merge": "^2.2.2", "tailwind-merge": "^2.2.2",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"toml": "^3.0.0",
"unified": "^11.0.4", "unified": "^11.0.4",
"vaul-svelte": "^0.3.0" "vaul-svelte": "^0.3.0"
}, },

View file

@ -23,6 +23,9 @@ dependencies:
'@sveltejs/adapter-vercel': '@sveltejs/adapter-vercel':
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0(@sveltejs/kit@2.5.5) version: 5.2.0(@sveltejs/kit@2.5.5)
'@types/node':
specifier: ^20.12.5
version: 20.12.5
bits-ui: bits-ui:
specifier: ^0.21.2 specifier: ^0.21.2
version: 0.21.2(svelte@4.2.12) version: 0.21.2(svelte@4.2.12)
@ -71,6 +74,9 @@ dependencies:
tailwind-variants: tailwind-variants:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(tailwindcss@3.4.3) version: 0.2.1(tailwindcss@3.4.3)
toml:
specifier: ^3.0.0
version: 3.0.0
unified: unified:
specifier: ^11.0.4 specifier: ^11.0.4
version: 11.0.4 version: 11.0.4
@ -141,7 +147,7 @@ devDependencies:
version: 5.4.4 version: 5.4.4
vite: vite:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.2.8 version: 5.2.8(@types/node@20.12.5)
packages: packages:
@ -956,7 +962,7 @@ packages:
sirv: 2.0.4 sirv: 2.0.4
svelte: 4.2.12 svelte: 4.2.12
tiny-glob: 0.2.9 tiny-glob: 0.2.9
vite: 5.2.8 vite: 5.2.8(@types/node@20.12.5)
/@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8): /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8):
resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==} resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==}
@ -969,7 +975,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.2.8) '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.2.8)
debug: 4.3.4 debug: 4.3.4
svelte: 4.2.12 svelte: 4.2.12
vite: 5.2.8 vite: 5.2.8(@types/node@20.12.5)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -987,7 +993,7 @@ packages:
magic-string: 0.30.9 magic-string: 0.30.9
svelte: 4.2.12 svelte: 4.2.12
svelte-hmr: 0.15.3(svelte@4.2.12) svelte-hmr: 0.15.3(svelte@4.2.12)
vite: 5.2.8 vite: 5.2.8(@types/node@20.12.5)
vitefu: 0.2.5(vite@5.2.8) vitefu: 0.2.5(vite@5.2.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1041,6 +1047,11 @@ packages:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
dev: false dev: false
/@types/node@20.12.5:
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==}
dependencies:
undici-types: 5.26.5
/@types/pug@2.0.10: /@types/pug@2.0.10:
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
dev: true dev: true
@ -3806,6 +3817,10 @@ packages:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
/toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
dev: false
/totalist@3.0.1: /totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3855,6 +3870,9 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/unified@11.0.4: /unified@11.0.4:
resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==}
dependencies: dependencies:
@ -3993,7 +4011,7 @@ packages:
vfile-message: 4.0.2 vfile-message: 4.0.2
dev: false dev: false
/vite@5.2.8: /vite@5.2.8(@types/node@20.12.5):
resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
@ -4021,6 +4039,7 @@ packages:
terser: terser:
optional: true optional: true
dependencies: dependencies:
'@types/node': 20.12.5
esbuild: 0.20.2 esbuild: 0.20.2
postcss: 8.4.38 postcss: 8.4.38
rollup: 4.14.0 rollup: 4.14.0
@ -4035,7 +4054,7 @@ packages:
vite: vite:
optional: true optional: true
dependencies: dependencies:
vite: 5.2.8 vite: 5.2.8(@types/node@20.12.5)
/web-namespaces@2.0.1: /web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<link <link
rel="icon" rel="icon"
href="%sveltekit.assets%/favicon-light.ico" href="%sveltekit.assets%/favicon-light.ico"

View file

@ -16,11 +16,9 @@
<h1 class="scroll-m-20 text-5xl font-bold font-serif tracking-tight"> <h1 class="scroll-m-20 text-5xl font-bold font-serif tracking-tight">
{doc.title} {doc.title}
</h1> </h1>
{#if doc.description}
<p class="text-balance text-lg text-muted-foreground"> <p class="text-balance text-lg text-muted-foreground">
{doc.description} {doc.blurb}
</p> </p>
{/if}
<PostMetadata <PostMetadata
primaryTags={doc.primaryTags} primaryTags={doc.primaryTags}
secondaryTags={doc.secondaryTags} secondaryTags={doc.secondaryTags}
@ -29,10 +27,9 @@
reverseDateAndRest reverseDateAndRest
/> />
</div> </div>
<slot name="mobile-toc" />
</header> </header>
<div class="markdown-body my-8 font-serif"> <div class="markdown-body mb-8 font-serif">
{@html doc.content} {@html doc.content}
</div> </div>
</article> </article>

View file

@ -14,7 +14,7 @@
<Card.Root> <Card.Root>
<Card.Header> <Card.Header>
<Card.Title><h2 class="text-lg xl:text-xl font-serif">On this page</h2></Card.Title> <Card.Title><p class="text-lg xl:text-xl font-serif">On this page</p></Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
{#if $tocStore.items.size} {#if $tocStore.items.size}

View file

@ -1,6 +1,5 @@
.markdown-body { .markdown-body {
@apply text-primary/90; @apply text-primary/90;
h1,
h2 { h2 {
@apply text-3xl font-serif mb-5 mt-16; @apply text-3xl font-serif mb-5 mt-16;
} }

56
src/lib/utils/crawl.ts Normal file
View file

@ -0,0 +1,56 @@
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
import toml from 'toml';
export type PostMeta = {
title: string;
manifest: {
authors: string[];
date: Date;
tags: { primary: string[]; secondary: string[] };
blurb: string;
description: string;
};
cover: {
src: string;
alt: string;
};
};
export default async () => {
const blogPath = join(process.cwd(), 'blog');
const years = await readdir(blogPath);
const posts: {
[key: string]: {
metadata: PostMeta;
content: string;
};
} = {};
for (const year of years) {
const yearPath = join(blogPath, year);
const directories = await readdir(yearPath);
for (const directory of directories) {
const postPath = join(yearPath, directory);
const postTomlPath = join(postPath, 'post.toml');
const contentPath = join(postPath, 'content.md');
try {
const postToml = await readFile(postTomlPath, 'utf-8');
const content = await readFile(contentPath, 'utf-8');
const metadata = toml.parse(postToml);
posts[`${year}/${directory}`] = {
metadata,
content
};
} catch (error) {
throw new Error(`Error reading post: ${error}`);
}
}
return posts;
}
};

View file

@ -0,0 +1,32 @@
import { unified } from 'unified';
import rehypeKatex from 'rehype-katex';
import rehypeStringify from 'rehype-stringify';
import remarkMath from 'remark-math';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkGfm from 'remark-gfm';
import remarkGhAlerts from 'remark-gh-alerts';
import remarkSectionize from 'remark-sectionize';
export default async (markdown: string) => {
try {
const html = String(
await unified()
.use(remarkParse)
.use(remarkSectionize)
.use(remarkGfm)
.use(remarkGhAlerts)
.use(remarkMath)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
.process(markdown)
);
if (typeof html === 'undefined') throw new Error('No content');
return html;
} catch (e) {
throw new Error(`Failed to parse markdown: ${e}`);
}
};

View file

@ -1,5 +1,26 @@
import type { EntryGenerator } from './$types'; import crawl from '$lib/utils/crawl';
import { error } from '@sveltejs/kit';
import type { EntryGenerator, PageServerLoad } from './$types';
import parseMarkdown from '$lib/utils/parseMarkdown';
const posts = await crawl();
if (!posts) throw new Error('No posts found!');
export const load: PageServerLoad = async ({ params }) => {
const post = posts[params.slug];
if (!post) {
error(404, 'Post not found.');
}
const parsed = await parseMarkdown(post.content);
return {
...post,
content: parsed
};
};
export const entries: EntryGenerator = () => { export const entries: EntryGenerator = () => {
return [{ slug: '2024/test-post' }, { slug: '2024/another-post' }]; return Object.keys(posts).map((slug) => ({ slug }));
}; };

View file

@ -10,14 +10,13 @@
export let data: PageData; export let data: PageData;
let doc: BlogDocument = { let doc: BlogDocument = {
title: 'Test Post', title: data.metadata.title,
primaryTags: ['Computer Science', 'Mathematics'], primaryTags: data.metadata.manifest.tags.primary,
secondaryTags: ['Calculus', 'Taylor Series'], secondaryTags: data.metadata.manifest.tags.secondary,
time: Date.now() / 1000, time: data.metadata.manifest.date.getTime() / 1000,
content: data.content!, content: data.content,
blurb: 'A short and succinct, yet descriptive blurb about the post.', blurb: data.metadata.manifest.blurb,
description: description: data.metadata.manifest.description
'An insightful and longer description of the post. This should be a bit more detailed than the blurb. It should give the reader a good idea of what the post is about.'
}; };
// $: doc = data.metadata; // $: doc = data.metadata;
// $: componentSource = data.metadata.source?.replace('default', $config.style ?? 'default'); // $: componentSource = data.metadata.source?.replace('default', $config.style ?? 'default');

View file

@ -1,40 +1 @@
import type { PageLoad } from '../[year]/[slug]/$types.js';
import { unified } from 'unified';
import rehypeKatex from 'rehype-katex';
import rehypeStringify from 'rehype-stringify';
import remarkMath from 'remark-math';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkGfm from 'remark-gfm';
import remarkGhAlerts from 'remark-gh-alerts';
import remarkSectionize from 'remark-sectionize';
export const prerender = true; export const prerender = true;
export const load: PageLoad = async ({ fetch }) => {
try {
const data = await fetch('/test.md');
const content = String(
await unified()
.use(remarkParse)
.use(remarkSectionize)
.use(remarkGfm)
.use(remarkGhAlerts)
.use(remarkMath)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
.process(await data.text())
);
if (typeof content === 'undefined') throw new Error('No content');
return {
content
};
} catch (e) {
return {
markdown: `Error: ${e}`
};
}
};

View file

@ -2,5 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit()],
server: {
fs: {
allow: ['./blog']
}
}
}); });