diff --git a/app/components/StaticMarkdownRender.server.ts b/app/components/StaticMarkdownRender.server.ts
index 057d6d70..d1d8ab19 100644
--- a/app/components/StaticMarkdownRender.server.ts
+++ b/app/components/StaticMarkdownRender.server.ts
@@ -1,4 +1,6 @@
-import { ContentRenderer } from '#components'
+import { MDCRenderer } from '#components'
+import { blogBodyLoaders } from '#build/markdown/blog/index.mjs'
+import { pageBodyLoaders } from '#build/markdown/page/index.mjs'
export default defineComponent({
props: {
@@ -6,13 +8,15 @@ export default defineComponent({
path: { type: String, required: true },
},
async setup (props) {
- if (import.meta.dev) {
- const { data } = await useAsyncData(() =>
- queryCollection(props.collection).path(props.path!).first(),
- )
- return () => h(ContentRenderer, { value: data.value! })
+ const slug = props.path.replace(/^\/(?:blog\/)?/, '').replace(/\/$/, '')
+ const loaders = props.collection === 'blog' ? blogBodyLoaders : pageBodyLoaders
+ const loader = loaders[slug]
+
+ if (!loader) {
+ return () => null
}
- const value = await queryCollection(props.collection).path(props.path!).first()
- return () => h(ContentRenderer, { value: value! })
+
+ const parsed = await loader()
+ return () => h(MDCRenderer, { body: parsed.body, tag: 'div' })
},
})
diff --git a/app/components/TheBlogIndex.server.vue b/app/components/TheBlogIndex.server.vue
index 2c1eff29..fe444604 100644
--- a/app/components/TheBlogIndex.server.vue
+++ b/app/components/TheBlogIndex.server.vue
@@ -1,17 +1,11 @@
{
- const result = await queryCollection('blog').select('title', 'date', 'path').all()
- return result
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
- .slice(0, 4)
+ const { blogEntries } = await import('#build/markdown/blog-entries.mjs')
+ return blogEntries.slice(0, 4).map(({ title, path, date }) => ({ title, path, date }))
}),
useFetch('/api/talks', {
transform: talks => {
diff --git a/app/components/global/CalSchedule.vue b/app/components/mdc/CalSchedule.vue
similarity index 100%
rename from app/components/global/CalSchedule.vue
rename to app/components/mdc/CalSchedule.vue
diff --git a/app/components/global/ProseA.vue b/app/components/mdc/ProseA.vue
similarity index 100%
rename from app/components/global/ProseA.vue
rename to app/components/mdc/ProseA.vue
diff --git a/app/components/global/ProseImg.vue b/app/components/mdc/ProseImg.vue
similarity index 100%
rename from app/components/global/ProseImg.vue
rename to app/components/mdc/ProseImg.vue
diff --git a/app/components/global/ProseTh.vue b/app/components/mdc/ProseTh.vue
similarity index 100%
rename from app/components/global/ProseTh.vue
rename to app/components/mdc/ProseTh.vue
diff --git a/app/components/global/SocialPost.vue b/app/components/mdc/SocialPost.vue
similarity index 100%
rename from app/components/global/SocialPost.vue
rename to app/components/mdc/SocialPost.vue
diff --git a/app/pages/blog/[article].vue b/app/pages/blog/[article].vue
index c58fea54..6bb17c0d 100644
--- a/app/pages/blog/[article].vue
+++ b/app/pages/blog/[article].vue
@@ -71,11 +71,22 @@ const path = computed(() =>
const { data: page } = await useAsyncData(
path.value,
- () =>
- ((import.meta.server || import.meta.dev) as true)
- && queryCollection('blog').path(path.value)
- .select('title', 'date', 'tags', 'description', 'bluesky')
- .first(),
+ async () => {
+ if (!import.meta.server && !import.meta.dev) return null
+ const { blogEntries } = await import('#build/markdown/blog-entries.mjs')
+ const slug = path.value.replace(/^\/blog\//, '').replace(/\/$/, '')
+ const entry = blogEntries.find(e => e.path === `/blog/${slug}`)
+ if (!entry) {
+ return null
+ }
+ return {
+ title: entry.title,
+ date: entry.date,
+ description: entry.description,
+ tags: entry.tags,
+ bluesky: entry.bluesky,
+ }
+ },
)
if (!page.value) {
diff --git a/app/shims.d.ts b/app/shims.d.ts
deleted file mode 100644
index d82244f4..00000000
--- a/app/shims.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// TODO: fix upstream
-declare module '#build/*' {
- const _default: any
- export default _default
-}
diff --git a/content.config.ts b/content.config.ts
deleted file mode 100644
index 60724308..00000000
--- a/content.config.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { defineCollection, defineContentConfig, z } from '@nuxt/content'
-
-export default defineContentConfig({
- collections: {
- page: defineCollection({
- source: '*.md',
- type: 'page',
- }),
- blog: defineCollection({
- source: 'blog/*.md',
- type: 'page',
- schema: z.object({
- title: z.string(),
- date: z.date(),
- tags: z.array(z.string()),
- description: z.string(),
- skip_dev: z.boolean().optional(),
- bluesky: z.string().optional(),
- }),
- }),
- },
-})
diff --git a/modules/bsky-comments.ts b/modules/bsky-comments.ts
index be1f5dc8..9238dc7e 100644
--- a/modules/bsky-comments.ts
+++ b/modules/bsky-comments.ts
@@ -4,9 +4,6 @@ import { existsSync } from 'node:fs'
import { addServerHandler, addServerTemplate, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, useNuxt } from 'nuxt/kit'
import { joinURL, withoutTrailingSlash } from 'ufo'
import { join } from 'pathe'
-import { filename } from 'pathe/utils'
-import { glob } from 'tinyglobby'
-import grayMatter from 'gray-matter'
import { AtpAgent, AppBskyFeedPost } from '@atproto/api'
// when I created my Bluesky account - don't judge me for hard coding it!
@@ -56,128 +53,120 @@ declare module '#build/bsky-runtime-discovery.mjs' {
}
const blueskyHandle = nuxt.options.social.networks.bluesky!.identifier
- // Read all blog posts from markdown files
- const files = await glob('./content/blog/**/*.md', { cwd: nuxt.options.rootDir, absolute: true })
+ // Receive blog data from the markdown module hook and discover Bluesky URIs
const blogPosts: BlogPost[] = []
- const feedIterator = createFeedIterator(agent, blueskyHandle)
- for (const filePath of files) {
- const contents = await readFile(filePath, 'utf-8')
- const { data } = grayMatter(contents)
+ nuxt.hook('markdown:blog-entries', async entries => {
+ const feedIterator = createFeedIterator(agent, blueskyHandle)
- if (!data.date) continue
+ for (const entry of entries) {
+ if (!entry.date) continue
- const slug = filename(filePath)!
- const blogPath = `/blog/${slug}`
- const blogUrl = withoutTrailingSlash(joinURL(nuxt.options.site.url, blogPath))
- const blogDate = new Date(data.date)
- const cacheFile = join(cacheDir, `${slug}.json`)
+ const blogPath = entry.path
+ const blogUrl = withoutTrailingSlash(joinURL(nuxt.options.site.url, blogPath))
+ const blogDate = new Date(entry.date)
+ const cacheFile = join(cacheDir, `${entry.slug}.json`)
- const blueskyUri = await discoverBlueskyUri()
- blogPosts.push({ slug, path: blogPath, url: blogUrl, date: blogDate, blueskyUri })
+ const blueskyUri = await discoverBlueskyUri()
+ blogPosts.push({ slug: entry.slug, path: blogPath, url: blogUrl, date: blogDate, blueskyUri })
- async function discoverBlueskyUri (): Promise {
- // Check cache first
- if (existsSync(cacheFile)) {
- try {
- const cacheData = JSON.parse(await readFile(cacheFile, 'utf-8')) as { uri: string | null }
- return cacheData.uri
- }
- catch {
- // Continue to discover
- }
+ // Inject the discovered URI back into the entry
+ if (blueskyUri) {
+ entry.bluesky = blueskyUri
}
- // 1. explicit bluesky URL in frontmatter
- if (data.bluesky?.startsWith('https://bsky.app/')) {
- const match = data.bluesky.match(/bsky\.app\/profile\/([^/]+)\/post\/([^/]+)/)
- if (match) {
- const [, handle, rkey] = match
+ async function discoverBlueskyUri (): Promise {
+ // Check cache first
+ if (existsSync(cacheFile)) {
try {
- const { data: resolved } = await agent.resolveHandle({ handle: handle! })
- const uri = `at://${resolved.did}/app.bsky.feed.post/${rkey}`
- await saveCache(cacheFile, uri)
- return uri
+ const cacheData = JSON.parse(await readFile(cacheFile, 'utf-8')) as { uri: string | null }
+ return cacheData.uri
}
- catch (error) {
- console.warn(`Failed to resolve Bluesky handle for ${blogPath}:`, error)
+ catch {
+ // Continue to discover
}
}
- await saveCache(cacheFile, null)
- return null
- }
- // 2. blog post date is before Bluesky account creation
- if (blogDate < BLUESKY_ACCOUNT_CREATED) {
- await saveCache(cacheFile, null)
- return null
- }
-
- // 3. auto-discover posts linking to this blog URL
- const searchCutoff = new Date(blogDate.getTime() - 24 * 60 * 60 * 1000)
+ // 1. explicit bluesky URL in frontmatter
+ if (entry.bluesky?.startsWith('https://bsky.app/')) {
+ const match = entry.bluesky.match(/bsky\.app\/profile\/([^/]+)\/post\/([^/]+)/)
+ if (match) {
+ const [, handle, rkey] = match
+ try {
+ const { data: resolved } = await agent.resolveHandle({ handle: handle! })
+ const uri = `at://${resolved.did}/app.bsky.feed.post/${rkey}`
+ await saveCache(cacheFile, uri)
+ return uri
+ }
+ catch (error) {
+ console.warn(`Failed to resolve Bluesky handle for ${blogPath}:`, error)
+ }
+ }
+ await saveCache(cacheFile, null)
+ return null
+ }
- while (!feedIterator.isExhausted()) {
- const oldestFetchedDate = feedIterator.getOldestPostDate()
- if (oldestFetchedDate && oldestFetchedDate < searchCutoff) {
- break
+ // 2. blog post date is before Bluesky account creation
+ if (blogDate < BLUESKY_ACCOUNT_CREATED) {
+ await saveCache(cacheFile, null)
+ return null
}
- await feedIterator.fetchMore()
- }
- const matchingPosts = feedIterator.posts
- .filter(post => {
- const postDate = new Date(post.createdAt)
- if (postDate < searchCutoff) return false
- return post.links.some(link => withoutTrailingSlash(link) === blogUrl)
- })
- .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
+ // 3. auto-discover posts linking to this blog URL
+ const searchCutoff = new Date(blogDate.getTime() - 24 * 60 * 60 * 1000)
- const uri = matchingPosts[0]?.uri ?? null
- if (uri) {
- console.log(`Auto-discovered Bluesky post for ${blogPath}`)
+ while (!feedIterator.isExhausted()) {
+ const oldestFetchedDate = feedIterator.getOldestPostDate()
+ if (oldestFetchedDate && oldestFetchedDate < searchCutoff) {
+ break
+ }
+ await feedIterator.fetchMore()
+ }
+
+ const matchingPosts = feedIterator.posts
+ .filter(post => {
+ const postDate = new Date(post.createdAt)
+ if (postDate < searchCutoff) return false
+ return post.links.some(link => withoutTrailingSlash(link) === blogUrl)
+ })
+ .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
+
+ const uri = matchingPosts[0]?.uri ?? null
+ if (uri) {
+ console.log(`Auto-discovered Bluesky post for ${blogPath}`)
+ }
+ await saveCache(cacheFile, uri)
+ return uri
}
- await saveCache(cacheFile, uri)
- return uri
}
- }
- // Sort by date descending to find newest
- blogPosts.sort((a, b) => b.date.getTime() - a.date.getTime())
- const newestPost = blogPosts[0]
- const needsRuntimeDiscovery = newestPost && !newestPost.blueskyUri
+ // Sort by date descending to find newest
+ blogPosts.sort((a, b) => b.date.getTime() - a.date.getTime())
+ const newestPost = blogPosts[0]
+ const needsRuntimeDiscovery = newestPost && !newestPost.blueskyUri
- // Add virtual file for runtime discovery flag (client-side)
- addTemplate({
- filename: 'bsky-runtime-discovery.mjs',
- getContents: () => `export const needsRuntimeDiscovery = ${!!needsRuntimeDiscovery}
+ // Add virtual file for runtime discovery flag (client-side)
+ addTemplate({
+ filename: 'bsky-runtime-discovery.mjs',
+ getContents: () => `export const needsRuntimeDiscovery = ${!!needsRuntimeDiscovery}
export const newestPostPath = ${newestPost ? JSON.stringify(newestPost.path) : 'null'}`,
- write: true,
- })
+ write: true,
+ })
- // Add server template with the blog URL and date
- addServerTemplate({
- filename: 'bsky-runtime-discovery-server.mjs',
- getContents: () => `
+ // Add server template with the blog URL and date
+ addServerTemplate({
+ filename: 'bsky-runtime-discovery-server.mjs',
+ getContents: () => `
export const newestPostUrl = ${needsRuntimeDiscovery ? JSON.stringify(newestPost.url) : 'null'}
export const newestPostDate = ${needsRuntimeDiscovery ? JSON.stringify(newestPost.date.toISOString()) : 'null'}`,
- })
-
- // Conditionally add the server endpoint only when needed
- if (needsRuntimeDiscovery) {
- addServerHandler({
- route: '/api/discover-bluesky-post',
- handler: resolver.resolve('./runtime/server/discover-bluesky-post.get'),
})
- }
-
- // Still use the content hook to inject the bluesky URI into content
- nuxt.hook('content:file:afterParse', ctx => {
- const content = ctx.content as { path?: string, bluesky?: string }
- if (!content.path?.startsWith('/blog/')) return
- const post = blogPosts.find(p => p.path === content.path)
- if (post?.blueskyUri) {
- content.bluesky = post.blueskyUri
+ // Conditionally add the server endpoint only when needed
+ if (needsRuntimeDiscovery) {
+ addServerHandler({
+ route: '/api/discover-bluesky-post',
+ handler: resolver.resolve('./runtime/server/discover-bluesky-post.get'),
+ })
}
})
},
diff --git a/modules/content.ts b/modules/content.ts
deleted file mode 100644
index 800b67ea..00000000
--- a/modules/content.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { defineNuxtModule, useNuxt } from 'nuxt/kit'
-
-export default defineNuxtModule({
- meta: {
- name: 'nuxt-content-purge',
- },
- setup () {
- const nuxt = useNuxt()
- if (nuxt.options.dev) return
- nuxt.hook('modules:done', () => {
- const purgedHandlers: any[] = []
- nuxt.hook('nitro:init', nitro => {
- for (const handler of nitro.options.handlers!) {
- if (handler?.route && /\/api\/_(content|mdc)/.test(handler.route)) {
- purgedHandlers.push(handler)
- }
- }
- nitro.options.handlers = nitro.options.handlers!.filter(handler => !purgedHandlers.includes(handler))
- })
- })
- },
-})
diff --git a/modules/markdown.ts b/modules/markdown.ts
new file mode 100644
index 00000000..e4d55cbd
--- /dev/null
+++ b/modules/markdown.ts
@@ -0,0 +1,250 @@
+import { readFile } from 'node:fs/promises'
+
+import { addTemplate, addTypeTemplate, defineNuxtModule, useNuxt } from 'nuxt/kit'
+import { glob } from 'tinyglobby'
+import grayMatter from 'gray-matter'
+import { filename } from 'pathe/utils'
+import { remark } from 'remark'
+import remarkHtml from 'remark-html'
+
+import { serializers } from './shared/serialisers'
+
+interface BlogFrontmatter {
+ title: string
+ date: string
+ tags: string[]
+ description: string
+ skip_dev?: boolean
+ bluesky?: string
+}
+
+interface ParsedBlogPost {
+ slug: string
+ title: string
+ date: string
+ tags: string[]
+ description: string
+ path: string
+ skip_dev?: boolean
+ bluesky?: string
+ body: string
+}
+
+export default defineNuxtModule({
+ meta: {
+ name: 'markdown',
+ },
+ async setup () {
+ const nuxt = useNuxt()
+ const rootDir = nuxt.options.rootDir
+
+ // Read and parse all content files at build time
+ const [blogFiles, pageFiles] = await Promise.all([
+ glob('./content/blog/**/*.md', { cwd: rootDir, absolute: true }),
+ glob('./content/*.md', { cwd: rootDir, absolute: true }),
+ ])
+
+ const blogPosts: ParsedBlogPost[] = []
+ const pageBodies: Record = {}
+
+ // Parse blog posts
+ for (const filePath of blogFiles) {
+ const raw = await readFile(filePath, 'utf-8')
+ const { data, content } = grayMatter(raw)
+ const slug = filename(filePath)!
+ const fm = data as BlogFrontmatter
+
+ blogPosts.push({
+ slug,
+ title: fm.title,
+ date: typeof fm.date === 'object' ? (fm.date as Date).toISOString() : fm.date,
+ tags: fm.tags || [],
+ description: fm.description || '',
+ path: `/blog/${slug}`,
+ skip_dev: fm.skip_dev,
+ bluesky: fm.bluesky,
+ body: content,
+ })
+ }
+
+ // Parse page files
+ for (const filePath of pageFiles) {
+ const raw = await readFile(filePath, 'utf-8')
+ const { content } = grayMatter(raw)
+ const slug = filename(filePath)!
+ pageBodies[slug] = content
+ }
+
+ // Sort blog posts by date descending
+ blogPosts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+
+ // Wait until all modules are loaded before calling the hook,
+ // so listeners don't depend on module load order.
+ nuxt.hook('modules:done', () => nuxt.callHook('markdown:blog-entries', blogPosts))
+
+ // --- Client-side virtual modules ---
+
+ addTemplate({
+ filename: 'markdown/blog-entries.mjs',
+ getContents: () => {
+ const entries = blogPosts.map(({ slug: _, body: __, ...entry }) => entry)
+ return `export const blogEntries = ${JSON.stringify(entries)}`
+ },
+ write: true,
+ })
+
+ for (const post of blogPosts) {
+ addTemplate({
+ filename: `markdown/blog/${post.slug}.mjs`,
+ getContents: () => `
+import { parseMarkdown } from '@nuxtjs/mdc/runtime'
+
+let _parsed
+export async function getBody () {
+ if (!_parsed) {
+ _parsed = await parseMarkdown(${JSON.stringify(post.body)})
+ }
+ return _parsed
+}
+`,
+ write: true,
+ })
+ }
+
+ for (const slug of Object.keys(pageBodies)) {
+ addTemplate({
+ filename: `markdown/page/${slug}.mjs`,
+ getContents: () => `
+import { parseMarkdown } from '@nuxtjs/mdc/runtime'
+
+let _parsed
+export async function getBody () {
+ if (!_parsed) {
+ _parsed = await parseMarkdown(${JSON.stringify(pageBodies[slug])})
+ }
+ return _parsed
+}
+`,
+ write: true,
+ })
+ }
+
+ addTemplate({
+ filename: 'markdown/blog/index.mjs',
+ getContents: () => {
+ const imports = blogPosts.map(
+ (p, i) => `import { getBody as getBody${i} } from './${p.slug}.mjs'`,
+ ).join('\n')
+ const entries = blogPosts.map(
+ (p, i) => ` '${p.slug}': getBody${i},`,
+ ).join('\n')
+ return `${imports}\n\nexport const blogBodyLoaders = {\n${entries}\n}\n`
+ },
+ write: true,
+ })
+
+ addTemplate({
+ filename: 'markdown/page/index.mjs',
+ getContents: () => {
+ const slugs = Object.keys(pageBodies)
+ const imports = slugs.map(
+ (slug, i) => `import { getBody as getBody${i} } from './${slug}.mjs'`,
+ ).join('\n')
+ const entries = slugs.map(
+ (slug, i) => ` '${slug}': getBody${i},`,
+ ).join('\n')
+ return `${imports}\n\nexport const pageBodyLoaders = {\n${entries}\n}\n`
+ },
+ write: true,
+ })
+
+ // --- Server-side virtual modules ---
+
+ // RSS metadata (HTML rendering + frontmatter), replaces the old metadata.ts module
+ const md = remark().use(remarkHtml)
+ const rssMetadata: Record = {}
+
+ for (const post of blogPosts) {
+ let contents = post.body
+ for (const [pattern, replacement] of serializers) {
+ contents = contents.replace(pattern, replacement)
+ }
+ const date = new Date(post.date)
+ rssMetadata[post.slug] = {
+ title: post.title,
+ description: post.description,
+ tags: post.tags,
+ html: await md.process(contents).then(r => r.value),
+ date: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
+ }
+ }
+
+ nuxt.options.nitro.virtual ||= {}
+ nuxt.options.nitro.virtual['#metadata.json'] = () =>
+ `export const metadata = ${JSON.stringify(rssMetadata)}`
+
+ // Sync articles data, replaces sync/runtime/server/utils/items.ts reading from disk
+ const syncArticles = blogPosts
+ .filter(p => !p.skip_dev)
+ .map(post => {
+ let body = post.body
+ for (const [pattern, replacement] of serializers) {
+ body = body.replace(pattern, replacement)
+ }
+ const firstParagraph = post.body.split(/\n\s*\n/).find(p => p.trim())?.trim() || ''
+ const date = new Date(post.date)
+ return {
+ type: 'blog' as const,
+ title: post.title,
+ date: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`,
+ description: firstParagraph || '',
+ body_markdown: body,
+ canonical_url: `https://roe.dev/blog/${post.slug}/`,
+ tags: post.tags.length ? post.tags : undefined,
+ }
+ })
+
+ nuxt.options.nitro.virtual['#sync-articles.json'] = () =>
+ `export const syncArticles = ${JSON.stringify(syncArticles)}`
+
+ nuxt.options.nitro.externals ||= {}
+ nuxt.options.nitro.externals.inline ||= []
+ nuxt.options.nitro.externals.inline.push('#metadata.json', '#sync-articles.json')
+
+ // --- Type declarations ---
+
+ addTypeTemplate({
+ filename: 'types/markdown.d.ts',
+ getContents: () => `
+declare module '#build/markdown/blog-entries.mjs' {
+ interface BlogEntry {
+ title: string
+ date: string
+ tags: string[]
+ description: string
+ path: string
+ skip_dev?: boolean
+ bluesky?: string
+ }
+ export const blogEntries: BlogEntry[]
+}
+
+declare module '#build/markdown/blog/index.mjs' {
+ import type { MDCParserResult } from '@nuxtjs/mdc'
+ export const blogBodyLoaders: Record Promise>
+}
+
+declare module '#build/markdown/page/index.mjs' {
+ import type { MDCParserResult } from '@nuxtjs/mdc'
+ export const pageBodyLoaders: Record Promise>
+}
+`,
+ }, { nuxt: true })
+ },
+})
+
+declare module '@nuxt/schema' {
+ interface NuxtHooks {
+ 'markdown:blog-entries': (entries: Array<{ path: string, slug: string, date: string, bluesky?: string }>) => void
+ }
+}
diff --git a/modules/metadata.ts b/modules/metadata.ts
deleted file mode 100644
index 816214f6..00000000
--- a/modules/metadata.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { readFile } from 'node:fs/promises'
-import { defineNuxtModule, useNuxt } from 'nuxt/kit'
-
-import { glob as globby } from 'tinyglobby'
-import grayMatter from 'gray-matter'
-import { filename } from 'pathe/utils'
-import { remark } from 'remark'
-import remarkHtml from 'remark-html'
-
-import { serializers } from './shared/serialisers'
-
-export default defineNuxtModule({
- meta: {
- name: 'metadata',
- },
- async setup () {
- const nuxt = useNuxt()
- const files = await globby('./content/blog/**/*.md', { cwd: nuxt.options.rootDir, absolute: true })
- const metadata: Record = {}
-
- const md = remark().use(remarkHtml)
-
- for (const path of files) {
- let contents = await readFile(path, 'utf-8')
- const slug = filename(path)!
- const { data } = grayMatter(contents)
- const date = new Date(data.date)
-
- for (const item of serializers) {
- contents = contents.replace(item[0], item[1])
- }
-
- metadata[slug] = {
- ...data,
- html: await md
- .process(contents.replace(/---[\s\S]*---/m, ''))
- .then(r => r.value),
- date: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
- }
- }
-
- nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {}
- nuxt.options.nitro.virtual['#metadata.json'] = () =>
- `export const metadata = ${JSON.stringify(metadata)}`
- nuxt.options.nitro.externals = nuxt.options.nitro.externals || {}
- nuxt.options.nitro.externals.inline
- = nuxt.options.nitro.externals.inline || []
- nuxt.options.nitro.externals.inline.push('#metadata.json')
- },
-})
diff --git a/modules/router/index.ts b/modules/router/index.ts
index d8c50b42..8e622b28 100644
--- a/modules/router/index.ts
+++ b/modules/router/index.ts
@@ -80,6 +80,7 @@ export default defineNuxtModule({
addTemplate({
filename: 'routes.mjs',
+ write: true,
async getContents () {
const files = await readRecursive(resolver.resolve('../../app/pages'))
const componentNames = Object.fromEntries(
diff --git a/modules/router/runtime/routes.ts b/modules/router/runtime/routes.ts
index 68cfe4d5..c2e00c78 100644
--- a/modules/router/runtime/routes.ts
+++ b/modules/router/runtime/routes.ts
@@ -1,5 +1,5 @@
import { withoutTrailingSlash } from 'ufo'
-import _routes from '#build/routes'
+import _routes from '#build/routes.mjs'
interface NuxtRoute {
path: string | RegExp
diff --git a/modules/sync/runtime/server/tasks/sync.ts b/modules/sync/runtime/server/tasks/sync.ts
index b78c5f31..ff80c756 100644
--- a/modules/sync/runtime/server/tasks/sync.ts
+++ b/modules/sync/runtime/server/tasks/sync.ts
@@ -1,5 +1,7 @@
+// @ts-expect-error virtual file
+import { syncArticles } from '#sync-articles.json'
import { providerNames, syncWithProvider } from '../providers/index'
-import { getMarkdownArticles, getTalks } from '../utils/items'
+import { getTalks } from '../utils/items'
export default defineTask({
meta: {
@@ -8,7 +10,7 @@ export default defineTask({
},
async run () {
const items = [
- ...await getMarkdownArticles(),
+ ...syncArticles,
...await getTalks(),
]
const result = await Promise.all(providerNames.map(p => syncWithProvider(p, items)))
diff --git a/modules/sync/runtime/server/utils/items.ts b/modules/sync/runtime/server/utils/items.ts
index 028108a2..cf1cfb94 100644
--- a/modules/sync/runtime/server/utils/items.ts
+++ b/modules/sync/runtime/server/utils/items.ts
@@ -1,50 +1,5 @@
-import fsp from 'node:fs/promises'
-import { glob as globby } from 'tinyglobby'
-import matter from 'gray-matter'
-import { serializers } from '../../../../shared/serialisers'
import type { SyncItem } from '../providers'
-export async function getMarkdownArticles (): Promise {
- const rootDir = process.cwd()
- const articles: SyncItem[] = []
- const files = await globby('./content/blog/**/*.md', {
- cwd: rootDir,
- absolute: true,
- })
- for (const file of files) {
- let contents = await fsp.readFile(file, 'utf-8')
- if (contents.includes('skip_dev')) continue
- const { data, content } = matter(contents)
- const date = typeof data.date === 'string' ? data.date.trim() : (data.date instanceof Date ? data.date.toISOString().slice(0, 10) : undefined)
- const title = data.title?.toString().trim() || 'Untitled'
- for (const item of serializers) {
- contents = contents.replace(item[0], item[1])
- }
- const slug = file.match(/\/([^/]*)$/)?.[1]?.replace(/\.md$/, '') || ''
- const firstParagraph = content.split(/\n\s*\n/).find(p => p.trim())?.trim() || ''
- let tags: string[] = []
- if (Array.isArray(data.tags)) {
- tags = data.tags.map((t: any) => t.toString().trim())
- }
- else if (typeof data.tags === 'string') {
- tags = data.tags.split(',').map((t: string) => t.trim())
- }
- articles.push({
- type: 'blog' as const,
- title,
- date,
- description: firstParagraph || '',
- body_markdown: content,
- canonical_url: `https://roe.dev/blog/${slug}/`,
- tags: tags.length ? tags : undefined,
- })
- if (!date) {
- console.warn(`Article "${title}" in ${file} has no date set. Please add a date to the frontmatter.`)
- }
- }
- return articles
-}
-
const talkMap: Record = {
podcast: 'video',
talk: 'talk',
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 0c914bab..e93b59c2 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,7 +1,7 @@
import process from 'node:process'
import { defineNuxtConfig } from 'nuxt/config'
-import { extendViteConfig, useNuxt } from 'nuxt/kit'
+import { extendViteConfig } from 'nuxt/kit'
import { isTest } from 'std-env'
import type { HmrOptions } from 'vite'
@@ -16,7 +16,7 @@ export default defineNuxtConfig({
'@nuxtjs/html-validator',
'@unocss/nuxt',
'@nuxtjs/color-mode',
- '@nuxt/content',
+ '@nuxtjs/mdc',
'@nuxtjs/plausible',
'@nuxt/fonts',
'@nuxt/scripts',
@@ -81,17 +81,11 @@ export default defineNuxtConfig({
url: 'https://roe.dev',
},
- content: {
- watch: {
- enabled: false,
- },
- build: {
- markdown: {
- highlight: {
- preload: ['js', 'ts', 'json', 'vue'],
- theme: 'material-theme-palenight',
- },
- },
+ mdc: {
+ highlight: {
+ theme: 'material-theme-palenight',
+ langs: ['js', 'ts', 'json', 'vue', 'css', 'html', 'bash', 'md', 'mdc', 'yaml'],
+ noApiRoute: true,
},
},
@@ -189,7 +183,6 @@ export default defineNuxtConfig({
prerender: {
crawlLinks: true,
routes: ['/', '/live', '/rss.xml', '/voted', '/work', '/feedback', '/ama', '/ai'],
- ignore: ['/__nuxt_content'],
},
hooks: {
'prerender:generate' (route) {
@@ -250,19 +243,6 @@ export default defineNuxtConfig({
},
},
- hooks: {
- 'components:extend' (components) {
- // This code ensures that we can run our markdown renderer on the
- // client side in development mode (for HMR).
- const nuxt = useNuxt()
- for (const comp of components) {
- if (comp.pascalName === 'StaticMarkdownRender' && nuxt.options.dev) {
- comp.mode = 'all'
- }
- }
- },
- },
-
eslint: {
config: {
stylistic: true,
diff --git a/package.json b/package.json
index 4c95b87c..325e0c79 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@iconify-json/ri": "^1.2.6",
"@iconify-json/svg-spinners": "^1.2.4",
"@iconify-json/tabler": "^1.2.23",
- "@nuxt/content": "3.11.0",
+ "@nuxtjs/mdc": "0.20.0",
"@nuxt/eslint": "1.12.1",
"@nuxt/fonts": "0.12.1",
"@nuxt/image": "2.0.0",
@@ -51,7 +51,6 @@
"@unocss/postcss": "66.6.0",
"@unocss/reset": "66.6.0",
"@vercel/blob": "^2.0.0",
- "better-sqlite3": "^12.4.1",
"citty": "^0.1.6",
"defu": "^6.1.4",
"exsolve": "^1.0.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fd63d0b7..3873be3c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,9 +26,6 @@ importers:
'@iconify-json/tabler':
specifier: ^1.2.23
version: 1.2.26
- '@nuxt/content':
- specifier: 3.11.0
- version: 3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))
'@nuxt/eslint':
specifier: 1.12.1
version: 1.12.1(@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.27)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))
@@ -47,6 +44,9 @@ importers:
'@nuxtjs/html-validator':
specifier: 2.1.0
version: 2.1.0(magicast@0.5.1)(vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.3)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))
+ '@nuxtjs/mdc':
+ specifier: 0.20.0
+ version: 0.20.0(magicast@0.5.1)
'@nuxtjs/plausible':
specifier: 2.0.1
version: 2.0.1(magicast@0.5.1)
@@ -77,9 +77,6 @@ importers:
'@vercel/blob':
specifier: ^2.0.0
version: 2.0.0
- better-sqlite3:
- specifier: ^12.4.1
- version: 12.6.2
citty:
specifier: ^0.1.6
version: 0.1.6
@@ -301,10 +298,6 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
- '@apidevtools/json-schema-ref-parser@11.9.3':
- resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
- engines: {node: '>= 16'}
-
'@apidevtools/json-schema-ref-parser@14.2.1':
resolution: {integrity: sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==}
engines: {node: '>= 20'}
@@ -2421,9 +2414,6 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
- '@jsdevtools/ono@7.1.3':
- resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
-
'@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
@@ -2517,30 +2507,6 @@ packages:
engines: {node: ^16.10.0 || >=18.0.0}
hasBin: true
- '@nuxt/content@3.11.0':
- resolution: {integrity: sha512-sC2AyuQAZpw+iSxwekh75AsLc7Ja9aEY+l4r1DxGBEMkq+YGj8+6AqQSRqFjOH0Hu9yDUhRgpIUnlGVq43WqOA==}
- engines: {node: '>= 20.19.0'}
- peerDependencies:
- '@electric-sql/pglite': '*'
- '@libsql/client': '*'
- '@valibot/to-json-schema': ^1.5.0
- better-sqlite3: ^12.5.0
- sqlite3: '*'
- valibot: ^1.2.0
- peerDependenciesMeta:
- '@electric-sql/pglite':
- optional: true
- '@libsql/client':
- optional: true
- '@valibot/to-json-schema':
- optional: true
- better-sqlite3:
- optional: true
- sqlite3:
- optional: true
- valibot:
- optional: true
-
'@nuxt/devalue@2.0.2':
resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==}
@@ -3969,16 +3935,9 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
- '@socket.io/component-emitter@3.1.2':
- resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
-
'@speed-highlight/core@1.2.14':
resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==}
- '@sqlite.org/sqlite-wasm@3.50.4-build1':
- resolution: {integrity: sha512-Qig2Wso7gPkU1PtXwFzndh+CTRzrIFxVGqv6eCetjU7YqxlHItj+GvQYwYTppCRgAPawtRN/4AJcEgB9xDHGug==}
- hasBin: true
-
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -4071,9 +4030,6 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
- '@types/lodash@4.17.23':
- resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
-
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -4630,9 +4586,6 @@ packages:
'@webassemblyjs/wast-printer@1.14.1':
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
- '@webcontainer/env@1.1.1':
- resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==}
-
'@xstate/react@6.0.0':
resolution: {integrity: sha512-xXlLpFJxqLhhmecAXclBECgk+B4zYSrDTl8hTfPZBogkn82OHKbm9zJxox3Z/YXoOhAQhKFTRLMYGdlbhc6T9A==}
peerDependencies:
@@ -4856,9 +4809,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
- async-lock@1.4.1:
- resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
-
async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
@@ -5183,9 +5133,6 @@ packages:
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
- clean-git-ref@2.0.1:
- resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==}
-
clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@@ -5693,9 +5640,6 @@ packages:
dfa@1.2.0:
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
- diff3@0.0.3:
- resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==}
-
diff@8.0.3:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'}
@@ -5812,13 +5756,6 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
- engine.io-client@6.6.4:
- resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==}
-
- engine.io-parser@5.2.3:
- resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
- engines: {node: '>=10.0.0'}
-
enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
@@ -7277,11 +7214,6 @@ packages:
resolution: {integrity: sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw==}
engines: {node: '>=18'}
- isomorphic-git@1.36.1:
- resolution: {integrity: sha512-fC8SRT8MwoaXDK8G4z5biPEbqf2WyEJUb2MJ2ftSd39/UIlsnoZxLGux+lae0poLZO4AEcx6aUVOh5bV+P8zFA==}
- engines: {node: '>=14.17'}
- hasBin: true
-
isomorphic-ws@5.0.0:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
@@ -7379,11 +7311,6 @@ packages:
json-schema-to-typescript-lite@15.0.0:
resolution: {integrity: sha512-5mMORSQm9oTLyjM4mWnyNBi2T042Fhg1/0gCIB6X8U/LVpM2A+Nmj2yEyArqVouDmFThDxpEXcnTgSrjkGJRFA==}
- json-schema-to-typescript@15.0.4:
- resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
- engines: {node: '>=16.0.0'}
- hasBin: true
-
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -7952,9 +7879,6 @@ packages:
engines: {node: '>=16.13'}
hasBin: true
- minimark@0.2.0:
- resolution: {integrity: sha512-AmtWU9pO0C2/3AM2pikaVhJ//8E5rOpJ7+ioFQfjIq+wCsBeuZoxPd97hBFZ9qrI7DMHZudwGH3r8A7BMnsIew==}
-
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
@@ -7981,9 +7905,6 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- minimisted@2.0.1:
- resolution: {integrity: sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==}
-
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -8199,10 +8120,6 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
- nuxt-component-meta@0.17.1:
- resolution: {integrity: sha512-5pVCzWXqg9HP159JDhdfQJtFvgmS/KouEVpyYLPEBXWMrQoJBwujsczmLeIKXKI2BTy4RqfXy8N1GfGTZNb57g==}
- hasBin: true
-
nuxt-csurf@1.6.5:
resolution: {integrity: sha512-/DMNTON8LIVhntamKbBmAuM879B0QnuSJa7ZAkmkZe+21m+1QGcjVUxtSkizaM48NUvkuAGYOG0ncn+kqEgrzw==}
@@ -8428,9 +8345,6 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
- pako@1.0.11:
- resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
-
parallel-transform@1.2.0:
resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==}
@@ -9422,11 +9336,6 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
- sha.js@2.4.12:
- resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==}
- engines: {node: '>= 0.10'}
- hasBin: true
-
sha256-uint8array@0.10.7:
resolution: {integrity: sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==}
@@ -9545,21 +9454,9 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
- slugify@1.6.6:
- resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
- engines: {node: '>=8.0.0'}
-
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
- socket.io-client@4.8.3:
- resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
- engines: {node: '>=10.0.0'}
-
- socket.io-parser@4.2.5:
- resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==}
- engines: {node: '>=10.0.0'}
-
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -9962,10 +9859,6 @@ packages:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
- to-buffer@1.2.2:
- resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
- engines: {node: '>= 0.4'}
-
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -10634,14 +10527,6 @@ packages:
vue-bundle-renderer@2.2.0:
resolution: {integrity: sha512-sz/0WEdYH1KfaOm0XaBmRZOWgYTEvUDt6yPYaUzl4E52qzgWLlknaPPTTZmp6benaPTlQAI/hN1x3tAzZygycg==}
- vue-component-meta@3.2.2:
- resolution: {integrity: sha512-i1sAzQwHBXKvIFxxEoFL8+YzaJfIwyAypFOcElwXga2+J+ZxrhySiPRbnZuT9mHOEj40rkEm8Sw/93jumk7haA==}
- peerDependencies:
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
-
vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
@@ -10822,18 +10707,6 @@ packages:
resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==}
engines: {node: ^20.17.0 || >=22.9.0}
- ws@8.18.3:
- resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: '>=5.0.2'
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
-
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
@@ -10873,10 +10746,6 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
- xmlhttprequest-ssl@2.1.2:
- resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
- engines: {node: '>=0.4.0'}
-
xregexp@2.0.0:
resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==}
@@ -10958,11 +10827,6 @@ packages:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
- zod-to-json-schema@3.25.1:
- resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
- peerDependencies:
- zod: ^3.25 || ^4
-
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
@@ -11029,12 +10893,6 @@ snapshots:
package-manager-detector: 1.6.0
tinyexec: 1.0.2
- '@apidevtools/json-schema-ref-parser@11.9.3':
- dependencies:
- '@jsdevtools/ono': 7.1.3
- '@types/json-schema': 7.0.15
- js-yaml: 4.1.1
-
'@apidevtools/json-schema-ref-parser@14.2.1(@types/json-schema@7.0.15)':
dependencies:
'@types/json-schema': 7.0.15
@@ -13080,8 +12938,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@jsdevtools/ono@7.1.3': {}
-
'@juggle/resize-observer@3.4.0': {}
'@keyv/bigmap@1.3.0(keyv@5.5.5)':
@@ -13232,67 +13088,6 @@ snapshots:
- magicast
- supports-color
- '@nuxt/content@3.11.0(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))':
- dependencies:
- '@nuxt/kit': 4.2.2(magicast@0.5.1)
- '@nuxtjs/mdc': 0.20.0(magicast@0.5.1)
- '@shikijs/langs': 3.21.0
- '@sqlite.org/sqlite-wasm': 3.50.4-build1
- '@standard-schema/spec': 1.1.0
- '@webcontainer/env': 1.1.1
- c12: 3.3.3(magicast@0.5.1)
- chokidar: 5.0.0
- consola: 3.4.2
- db0: 0.3.4(better-sqlite3@12.6.2)
- defu: 6.1.4
- destr: 2.0.5
- git-url-parse: 16.1.0
- hookable: 5.5.3
- isomorphic-git: 1.36.1
- jiti: 2.6.1
- json-schema-to-typescript: 15.0.4
- knitwork: 1.3.0
- mdast-util-to-hast: 13.2.1
- mdast-util-to-string: 4.0.0
- micromark: 4.0.2
- micromark-util-character: 2.1.1
- micromark-util-chunked: 2.0.1
- micromark-util-resolve-all: 2.0.1
- micromark-util-sanitize-uri: 2.0.1
- micromatch: 4.0.8
- minimark: 0.2.0
- minimatch: 10.1.1
- nuxt-component-meta: 0.17.1(magicast@0.5.1)
- nypm: 0.6.2
- ohash: 2.0.11
- pathe: 2.0.3
- pkg-types: 2.3.0
- remark-mdc: 3.10.0
- scule: 1.3.0
- shiki: 3.21.0
- slugify: 1.6.6
- socket.io-client: 4.8.3
- std-env: 3.10.0
- tinyglobby: 0.2.15
- ufo: 1.6.3
- unctx: 2.5.0
- unified: 11.0.5
- unist-util-stringify-position: 4.0.0
- unist-util-visit: 5.0.0
- unplugin: 2.3.11
- zod: 3.25.76
- zod-to-json-schema: 3.25.1(zod@3.25.76)
- optionalDependencies:
- better-sqlite3: 12.6.2
- valibot: 1.2.0(typescript@5.9.3)
- transitivePeerDependencies:
- - bufferutil
- - drizzle-orm
- - magicast
- - mysql2
- - supports-color
- - utf-8-validate
-
'@nuxt/devalue@2.0.2': {}
'@nuxt/devtools-kit@3.1.1(magicast@0.5.1)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))':
@@ -15300,12 +15095,8 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
- '@socket.io/component-emitter@3.1.2': {}
-
'@speed-highlight/core@1.2.14': {}
- '@sqlite.org/sqlite-wasm@3.50.4-build1': {}
-
'@standard-schema/spec@1.1.0': {}
'@stylistic/eslint-plugin@5.7.0(eslint@9.39.2(jiti@2.6.1))':
@@ -15415,8 +15206,6 @@ snapshots:
'@types/json-schema@7.0.15': {}
- '@types/lodash@4.17.23': {}
-
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -16189,8 +15978,6 @@ snapshots:
'@webassemblyjs/ast': 1.14.1
'@xtuc/long': 4.2.2
- '@webcontainer/env@1.1.1': {}
-
'@xstate/react@6.0.0(@types/react@18.3.27)(react@18.3.1)(xstate@5.25.1)':
dependencies:
react: 18.3.1
@@ -16420,8 +16207,6 @@ snapshots:
async-function@1.0.0: {}
- async-lock@1.4.1: {}
-
async-mutex@0.5.0:
dependencies:
tslib: 2.8.1
@@ -16507,6 +16292,7 @@ snapshots:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
+ optional: true
bignumber.js@9.3.1: {}
@@ -16747,8 +16533,6 @@ snapshots:
classnames@2.5.1: {}
- clean-git-ref@2.0.1: {}
-
clean-regexp@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -17141,6 +16925,7 @@ snapshots:
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
+ optional: true
decompress-response@7.0.0:
dependencies:
@@ -17209,8 +16994,6 @@ snapshots:
dfa@1.2.0: {}
- diff3@0.0.3: {}
-
diff@8.0.3: {}
dir-glob@3.0.1:
@@ -17322,20 +17105,6 @@ snapshots:
dependencies:
once: 1.4.0
- engine.io-client@6.6.4:
- dependencies:
- '@socket.io/component-emitter': 3.1.2
- debug: 4.4.3(supports-color@8.1.1)
- engine.io-parser: 5.2.3
- ws: 8.18.3
- xmlhttprequest-ssl: 2.1.2
- transitivePeerDependencies:
- - bufferutil
- - supports-color
- - utf-8-validate
-
- engine.io-parser@5.2.3: {}
-
enhanced-resolve@5.18.4:
dependencies:
graceful-fs: 4.2.11
@@ -17933,7 +17702,8 @@ snapshots:
exit-hook@2.2.1: {}
- expand-template@2.0.3: {}
+ expand-template@2.0.3:
+ optional: true
expect-type@1.3.0: {}
@@ -18344,7 +18114,8 @@ snapshots:
dependencies:
git-up: 8.1.1
- github-from-package@0.0.0: {}
+ github-from-package@0.0.0:
+ optional: true
github-slugger@2.0.0: {}
@@ -19210,20 +18981,6 @@ snapshots:
- supports-color
- utf-8-validate
- isomorphic-git@1.36.1:
- dependencies:
- async-lock: 1.4.1
- clean-git-ref: 2.0.1
- crc-32: 1.2.2
- diff3: 0.0.3
- ignore: 5.3.2
- minimisted: 2.0.1
- pako: 1.0.11
- pify: 4.0.1
- readable-stream: 4.7.0
- sha.js: 2.4.12
- simple-get: 4.0.1
-
isomorphic-ws@5.0.0(ws@8.19.0):
dependencies:
ws: 8.19.0
@@ -19339,18 +19096,6 @@ snapshots:
'@apidevtools/json-schema-ref-parser': 14.2.1(@types/json-schema@7.0.15)
'@types/json-schema': 7.0.15
- json-schema-to-typescript@15.0.4:
- dependencies:
- '@apidevtools/json-schema-ref-parser': 11.9.3
- '@types/json-schema': 7.0.15
- '@types/lodash': 4.17.23
- is-glob: 4.0.3
- js-yaml: 4.1.1
- lodash: 4.17.21
- minimist: 1.2.8
- prettier: 3.8.0
- tinyglobby: 0.2.15
-
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -20102,8 +19847,6 @@ snapshots:
- supports-color
- utf-8-validate
- minimark@0.2.0: {}
-
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
@@ -20132,10 +19875,6 @@ snapshots:
minimist@1.2.8: {}
- minimisted@2.0.1:
- dependencies:
- minimist: 1.2.8
-
minipass@7.1.2: {}
minizlib@3.1.0:
@@ -20219,7 +19958,8 @@ snapshots:
nanotar@0.2.0: {}
- napi-build-utils@2.0.0: {}
+ napi-build-utils@2.0.0:
+ optional: true
napi-postinstall@0.3.4: {}
@@ -20332,6 +20072,7 @@ snapshots:
node-abi@3.86.0:
dependencies:
semver: 7.7.3
+ optional: true
node-addon-api@7.1.1: {}
@@ -20410,19 +20151,6 @@ snapshots:
dependencies:
boolbase: 1.0.0
- nuxt-component-meta@0.17.1(magicast@0.5.1):
- dependencies:
- '@nuxt/kit': 4.2.2(magicast@0.5.1)
- citty: 0.1.6
- mlly: 1.8.0
- ohash: 2.0.11
- scule: 1.3.0
- typescript: 5.9.3
- ufo: 1.6.3
- vue-component-meta: 3.2.2(typescript@5.9.3)
- transitivePeerDependencies:
- - magicast
-
nuxt-csurf@1.6.5(magicast@0.5.1):
dependencies:
'@nuxt/kit': 3.20.2(magicast@0.5.1)
@@ -20891,8 +20619,6 @@ snapshots:
pako@0.2.9: {}
- pako@1.0.11: {}
-
parallel-transform@1.2.0:
dependencies:
cyclist: 1.0.2
@@ -21293,6 +21019,7 @@ snapshots:
simple-get: 4.0.1
tar-fs: 2.1.4
tunnel-agent: 0.6.0
+ optional: true
preferred-pm@4.1.1:
dependencies:
@@ -22217,12 +21944,6 @@ snapshots:
setprototypeof@1.2.0: {}
- sha.js@2.4.12:
- dependencies:
- inherits: 2.0.4
- safe-buffer: 5.2.1
- to-buffer: 1.2.2
-
sha256-uint8array@0.10.7: {}
shallow-clone@3.0.1:
@@ -22318,13 +22039,15 @@ snapshots:
signal-exit@4.1.0: {}
- simple-concat@1.0.1: {}
+ simple-concat@1.0.1:
+ optional: true
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
+ optional: true
simple-git-hooks@2.13.1: {}
@@ -22391,28 +22114,8 @@ snapshots:
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
- slugify@1.6.6: {}
-
smob@1.5.0: {}
- socket.io-client@4.8.3:
- dependencies:
- '@socket.io/component-emitter': 3.1.2
- debug: 4.4.3(supports-color@8.1.1)
- engine.io-client: 6.6.4
- socket.io-parser: 4.2.5
- transitivePeerDependencies:
- - bufferutil
- - supports-color
- - utf-8-validate
-
- socket.io-parser@4.2.5:
- dependencies:
- '@socket.io/component-emitter': 3.1.2
- debug: 4.4.3(supports-color@8.1.1)
- transitivePeerDependencies:
- - supports-color
-
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -22866,12 +22569,6 @@ snapshots:
dependencies:
tldts-core: 6.1.86
- to-buffer@1.2.2:
- dependencies:
- isarray: 2.0.5
- safe-buffer: 5.2.1
- typed-array-buffer: 1.0.3
-
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -23591,14 +23288,6 @@ snapshots:
dependencies:
ufo: 1.6.3
- vue-component-meta@3.2.2(typescript@5.9.3):
- dependencies:
- '@volar/typescript': 2.4.27
- '@vue/language-core': 3.2.2
- path-browserify: 1.0.1
- optionalDependencies:
- typescript: 5.9.3
-
vue-component-type-helpers@2.2.12: {}
vue-devtools-stub@0.1.0: {}
@@ -23831,8 +23520,6 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
- ws@8.18.3: {}
-
ws@8.19.0: {}
wsl-utils@0.1.0:
@@ -23853,8 +23540,6 @@ snapshots:
xmlchars@2.2.0: {}
- xmlhttprequest-ssl@2.1.2: {}
-
xregexp@2.0.0: {}
xss@1.0.15:
@@ -23927,10 +23612,6 @@ snapshots:
compress-commons: 6.0.2
readable-stream: 4.7.0
- zod-to-json-schema@3.25.1(zod@3.25.76):
- dependencies:
- zod: 3.25.76
-
zod-validation-error@4.0.2(zod@4.3.5):
dependencies:
zod: 4.3.5
diff --git a/test/unit/bundle.spec.ts b/test/unit/bundle.spec.ts
index 6b4318d9..03e8356d 100644
--- a/test/unit/bundle.spec.ts
+++ b/test/unit/bundle.spec.ts
@@ -78,12 +78,12 @@ describe('project sizes', () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect
.soft(roundToKilobytes(stats.server.totalBytes))
- .toMatchInlineSnapshot(`"942k"`)
+ .toMatchInlineSnapshot(`"2231k"`)
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect
.soft(roundToKilobytes(modules.totalBytes))
- .toMatchInlineSnapshot(`"10990k"`)
+ .toMatchInlineSnapshot(`"8548k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@@ -131,8 +131,6 @@ describe('project sizes', () => {
"@vue/shared",
"await-lock",
"bail",
- "better-sqlite3",
- "bindings",
"ccount",
"char-regex",
"character-entities",
@@ -158,7 +156,6 @@ describe('project sizes', () => {
"eventsource",
"extend",
"feed",
- "file-uri-to-path",
"flat",
"follow-redirects",
"get-it",
@@ -236,7 +233,6 @@ describe('project sizes', () => {
"micromark-util-sanitize-uri",
"micromark-util-subtokenize",
"mimic-response",
- "minimark",
"ms",
"multiformats",
"nanoid",
diff --git a/test/unit/sync-items.spec.ts b/test/unit/sync-items.spec.ts
index f8f33426..0f5cd62e 100644
--- a/test/unit/sync-items.spec.ts
+++ b/test/unit/sync-items.spec.ts
@@ -1,50 +1,46 @@
-import fsp from 'node:fs/promises'
-import { join } from 'node:path'
+import { readFile } from 'node:fs/promises'
import process from 'node:process'
import { describe, it, expect } from 'vitest'
-import { getMarkdownArticles } from '../../modules/sync/runtime/server/utils/items'
-
-describe('getMarkdownArticles', () => {
- it('parses markdown frontmatter and content correctly', async () => {
- // Create a temporary markdown file with frontmatter
- const filePath = join(process.cwd(), 'content/blog/test-article.md')
- const markdown = `---\ntitle: Test Article\ndate: 2025-06-26\ntags: ["foo", "bar"]\n---\n\nThis is the first paragraph.\n\nSecond paragraph.`
- await fsp.mkdir(join(process.cwd(), 'content/blog'), { recursive: true })
- await fsp.writeFile(filePath, markdown)
- const articles = await getMarkdownArticles()
- const testArticle = articles.find(a => a.title === 'Test Article')
- expect(testArticle).toBeDefined()
- expect(testArticle?.tags).toEqual(['foo', 'bar'])
- expect(testArticle?.description).toContain('This is the first paragraph')
- expect(testArticle?.date).toBe('2025-06-26')
- // Cleanup
- await fsp.unlink(filePath)
+import grayMatter from 'gray-matter'
+import { filename } from 'pathe/utils'
+import { glob } from 'tinyglobby'
+
+describe('blog content parsing', () => {
+ it('all blog posts have required frontmatter', async () => {
+ const files = await glob('./content/blog/**/*.md', { cwd: process.cwd(), absolute: true })
+ expect(files.length).toBeGreaterThan(0)
+
+ for (const file of files) {
+ const raw = await readFile(file, 'utf-8')
+ const { data } = grayMatter(raw)
+ const slug = filename(file)
+
+ expect(data.title, `${slug} missing title`).toBeTruthy()
+ expect(data.date, `${slug} missing date`).toBeTruthy()
+ expect(Array.isArray(data.tags), `${slug} tags should be an array`).toBe(true)
+ }
})
- it('parses titles and dates with quotes', async () => {
- const filePath = join(process.cwd(), 'content/blog/test-article-quotes.md')
- const markdown = `---\ntitle: "A 'Quoted' Title"\ndate: '2025-07-01'\ntags: ["foo"]\n---\n\nContent with quotes.`
- await fsp.mkdir(join(process.cwd(), 'content/blog'), { recursive: true })
- await fsp.writeFile(filePath, markdown)
- const articles = await getMarkdownArticles()
- const testArticle = articles.find(a => a.title === 'A \'Quoted\' Title')
- expect(testArticle).toBeDefined()
- expect(testArticle?.date).toBe('2025-07-01')
- expect(testArticle?.tags).toEqual(['foo'])
- // Cleanup
- await fsp.unlink(filePath)
+ it('blog post dates are valid', async () => {
+ const files = await glob('./content/blog/**/*.md', { cwd: process.cwd(), absolute: true })
+
+ for (const file of files) {
+ const raw = await readFile(file, 'utf-8')
+ const { data } = grayMatter(raw)
+ const slug = filename(file)
+ const date = new Date(data.date)
+
+ expect(date.toString(), `${slug} has invalid date: ${data.date}`).not.toBe('Invalid Date')
+ }
})
- it('parses tags as YAML array', async () => {
- const filePath = join(process.cwd(), 'content/blog/test-article-yaml-array.md')
- const markdown = `---\ntitle: YAML Array Tags\ndate: 2025-08-01\ntags:\n - personal\n - ux\n - brand\n---\n\nYAML array tags test.`
- await fsp.mkdir(join(process.cwd(), 'content/blog'), { recursive: true })
- await fsp.writeFile(filePath, markdown)
- const articles = await getMarkdownArticles()
- const testArticle = articles.find(a => a.title === 'YAML Array Tags')
- expect(testArticle).toBeDefined()
- expect(testArticle?.tags).toEqual(['personal', 'ux', 'brand'])
- // Cleanup
- await fsp.unlink(filePath)
+ it('blog post slugs form valid paths', async () => {
+ const files = await glob('./content/blog/**/*.md', { cwd: process.cwd(), absolute: true })
+
+ for (const file of files) {
+ const slug = filename(file)
+ expect(slug, `${file} has no slug`).toBeTruthy()
+ expect(slug).toMatch(/^[a-z0-9-]+$/)
+ }
})
})
diff --git a/uno.config.ts b/uno.config.ts
index 0244171c..fc9b68cb 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -9,9 +9,6 @@ export default defineConfig({
resolver.resolve('./content/**/*.md'),
resolver.resolve('./app/**/*.vue'),
],
- pipeline: {
- exclude: ['**/@nuxt/content/**'],
- },
},
shortcuts: [
{