[READ-ONLY] Mirror of https://github.com/danielroe/roe.dev. This is the code and content for my personal website, built in Nuxt. roe.dev
0

Configure Feed

Select the types of activity you want to include in your feed.

perf: move markdown transforms to build-time (#1829)

+39 -65
+3 -19
modules/markdown.ts
··· 9 9 import { convert as htmlToText } from 'html-to-text' 10 10 11 11 import { serialize } from './shared/serialisers' 12 + import { mdCleanHtml, mdInternalLinks } from './shared/md-transforms' 12 13 13 14 interface BlogFrontmatter { 14 15 title: string ··· 39 40 const nuxt = useNuxt() 40 41 const rootDir = nuxt.options.rootDir 41 42 42 - // Read and parse all content files at build time 43 43 const [blogFiles, pageFiles] = await Promise.all([ 44 44 glob('./content/blog/**/*.md', { cwd: rootDir, absolute: true }), 45 45 glob('./content/*.md', { cwd: rootDir, absolute: true }), ··· 48 48 const blogPosts: ParsedBlogPost[] = [] 49 49 const pageBodies: Record<string, string> = {} 50 50 51 - // Parse blog posts 52 51 for (const filePath of blogFiles) { 53 52 const raw = await readFile(filePath, 'utf-8') 54 53 const { data, content } = grayMatter(raw) ··· 68 67 }) 69 68 } 70 69 71 - // Parse page files 72 70 for (const filePath of pageFiles) { 73 71 const raw = await readFile(filePath, 'utf-8') 74 72 const { content } = grayMatter(raw) ··· 76 74 pageBodies[slug] = content 77 75 } 78 76 79 - // Sort blog posts by date descending 80 77 blogPosts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) 81 78 82 - // Wait until all modules are loaded before calling the hook, 83 - // so listeners don't depend on module load order. 84 79 nuxt.hook('modules:done', () => nuxt.callHook('markdown:blog-entries', blogPosts)) 85 - 86 - // --- Client-side virtual modules --- 87 80 88 81 addTemplate({ 89 82 filename: 'markdown/blog-entries.mjs', ··· 159 152 write: true, 160 153 }) 161 154 162 - // --- Server-side virtual modules --- 163 - 164 - // RSS metadata (HTML rendering + frontmatter), replaces the old metadata.ts module 165 155 const md = remark().use(remarkHtml) 166 156 const rssMetadata: Record<string, any> = {} 167 157 ··· 181 171 nuxt.options.nitro.virtual['#metadata.json'] = () => 182 172 `export const metadata = ${JSON.stringify(rssMetadata)}` 183 173 184 - // Sync articles data, replaces sync/runtime/server/utils/items.ts reading from disk 185 174 const syncArticles = await Promise.all(blogPosts 186 175 .filter(p => !p.skip_dev) 187 176 .map(async post => { 188 177 const body = serialize(post.body) 189 178 const date = new Date(post.date) 190 - // Render markdown to HTML, then convert to faithful plaintext 191 179 const html = String(await md.process(body)) 192 180 const textContent = htmlToText(html, { wordwrap: false }) 193 181 return { ··· 205 193 nuxt.options.nitro.virtual['#sync-articles.json'] = () => 206 194 `export const syncArticles = ${JSON.stringify(syncArticles)}` 207 195 208 - // Raw blog markdown for .md routes (blog post bodies + frontmatter) 209 196 const rawBlogData = blogPosts.map(post => ({ 210 197 slug: post.slug, 211 198 title: post.title, 212 199 date: typeof post.date === 'string' ? post.date.split('T')[0] : post.date, 213 200 tags: post.tags, 214 201 description: post.description, 215 - body: serialize(post.body), 202 + body: mdInternalLinks(serialize(post.body)).trim(), 216 203 })) 217 204 218 205 nuxt.options.nitro.virtual['#md-raw-blog.json'] = () => 219 206 `export const rawBlogPosts = ${JSON.stringify(rawBlogData)}` 220 207 221 - // Raw page markdown for .md routes (ai, bio) 222 208 const rawPageData: Record<string, string> = {} 223 209 for (const [slug, body] of Object.entries(pageBodies)) { 224 - rawPageData[slug] = serialize(body) 210 + rawPageData[slug] = mdInternalLinks(mdCleanHtml(serialize(body))) 225 211 } 226 212 227 213 nuxt.options.nitro.virtual['#md-raw-pages.json'] = () => ··· 230 216 nuxt.options.nitro.externals ||= {} 231 217 nuxt.options.nitro.externals.inline ||= [] 232 218 nuxt.options.nitro.externals.inline.push('#metadata.json', '#sync-articles.json', '#md-raw-blog.json', '#md-raw-pages.json') 233 - 234 - // --- Type declarations --- 235 219 236 220 addTypeTemplate({ 237 221 filename: 'types/markdown.d.ts',
+1 -1
modules/md-routes/runtime/server/blog-md.get.ts
··· 25 25 `url: https://roe.dev/blog/${post.slug}`, 26 26 '---', 27 27 '', 28 - mdInternalLinks(post.body.trim()), 28 + post.body, 29 29 '', 30 30 ].join('\n') 31 31
+30
modules/shared/md-transforms.ts
··· 1 + const SITE_URL = 'https://roe.dev' 2 + 3 + /** Rewrite absolute roe.dev links to their .md equivalents. */ 4 + export function mdInternalLinks (content: string): string { 5 + return content.replace( 6 + /\]\(https:\/\/roe\.dev(\/[^)]*?)\)/g, 7 + (_match, path: string) => { 8 + if (/\.(xml|png|jpg|jpeg|svg|webp|pdf|webmanifest|json|css|js)$/i.test(path)) return `](${SITE_URL}${path})` 9 + if (path.startsWith('/.well-known/')) return `](${SITE_URL}${path})` 10 + if (path.endsWith('.md')) return `](${SITE_URL}${path})` 11 + if (path.includes('@')) return `](${SITE_URL}${path})` 12 + 13 + const clean = path.replace(/\/$/, '') || '/index' 14 + return `](${SITE_URL}${clean}.md)` 15 + }, 16 + ) 17 + } 18 + 19 + /** Convert residual HTML elements in markdown to their markdown equivalents. */ 20 + export function mdCleanHtml (content: string): string { 21 + return content 22 + .replace(/<p[^>]*><img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*><\/p>/g, '![$2]($1)') 23 + .replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/g, '![$2]($1)') 24 + .replace(/<(?:div|span)[^>]*>/g, '') 25 + .replace(/<\/(?:div|span)>/g, '') 26 + .replace(/<p[^>]*class="[^"]*"[^>]*>/g, '') 27 + .replace(/<\/p>/g, '') 28 + .replace(/\n{3,}/g, '\n\n') 29 + .trim() 30 + }
+1 -3
server/routes/ai.md.get.ts
··· 2 2 import { pageMeta } from '#md-page-meta.json' 3 3 4 4 export default defineEventHandler(() => { 5 - const body = rawPages['ai'] || '' 6 - 7 5 const md = [ 8 6 mdFrontmatter('/ai', pageMeta['/ai']!), 9 7 '', 10 - mdInternalLinks(mdCleanHtml(body)), 8 + rawPages['ai'] || '', 11 9 '', 12 10 ].join('\n') 13 11
+1 -3
server/routes/bio.md.get.ts
··· 2 2 import { pageMeta } from '#md-page-meta.json' 3 3 4 4 export default defineEventHandler(() => { 5 - const body = rawPages['bio'] || '' 6 - 7 5 const md = [ 8 6 mdFrontmatter('/bio', pageMeta['/bio']!), 9 7 '', 10 - mdInternalLinks(mdCleanHtml(body)), 8 + rawPages['bio'] || '', 11 9 '', 12 10 ].join('\n') 13 11
+2 -2
server/routes/llms-full.txt.get.ts
··· 21 21 '', 22 22 '## Bio', 23 23 '', 24 - mdCleanHtml(rawPages['bio'] || ''), 24 + rawPages['bio'] || '', 25 25 '', 26 26 '---', 27 27 '', 28 28 '## AI Policy', 29 29 '', 30 - mdCleanHtml(rawPages['ai'] || ''), 30 + rawPages['ai'] || '', 31 31 '', 32 32 '---', 33 33 '',
-36
server/utils/md.ts
··· 15 15 return lines.join('\n') 16 16 } 17 17 18 - export function mdInternalLinks (content: string): string { 19 - return content.replace( 20 - /\]\(https:\/\/roe\.dev(\/[^)]*?)\)/g, 21 - (_match, path: string) => { 22 - // Skip non-page resources 23 - if (/\.(xml|png|jpg|jpeg|svg|webp|pdf|webmanifest|json|css|js)$/i.test(path)) return `](${SITE_URL}${path})` 24 - if (path.startsWith('/.well-known/')) return `](${SITE_URL}${path})` 25 - if (path.endsWith('.md')) return `](${SITE_URL}${path})` 26 - // Skip mailto and other non-path links 27 - if (path.includes('@')) return `](${SITE_URL}${path})` 28 - 29 - // Normalise trailing slash 30 - const clean = path.replace(/\/$/, '') || '/index' 31 - return `](${SITE_URL}${clean}.md)` 32 - }, 33 - ) 34 - } 35 - 36 - export function mdCleanHtml (content: string): string { 37 - return content 38 - // Convert <img> wrapped in a <p> to markdown image 39 - .replace(/<p[^>]*><img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*><\/p>/g, '![$2]($1)') 40 - // Convert standalone <img> to markdown image 41 - .replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/g, '![$2]($1)') 42 - // Strip opening div/span tags (keep content) 43 - .replace(/<(?:div|span)[^>]*>/g, '') 44 - // Strip closing div/span tags 45 - .replace(/<\/(?:div|span)>/g, '') 46 - // Strip <p> with only classes (presentational) 47 - .replace(/<p[^>]*class="[^"]*"[^>]*>/g, '') 48 - .replace(/<\/p>/g, '') 49 - // Collapse multiple blank lines left behind 50 - .replace(/\n{3,}/g, '\n\n') 51 - .trim() 52 - } 53 - 54 18 export function mdResponse (content: string): Response { 55 19 return new Response(content, { 56 20 headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
+1 -1
test/unit/bundle.spec.ts
··· 78 78 stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) 79 79 expect 80 80 .soft(roundToKilobytes(stats.server.totalBytes)) 81 - .toMatchInlineSnapshot(`"2335k"`) 81 + .toMatchInlineSnapshot(`"2333k"`) 82 82 83 83 const modules = await analyzeSizes('node_modules/**/*', serverDir) 84 84 expect