diff --git a/app/app.vue b/app/app.vue index 4c75b76c..b86ec447 100644 --- a/app/app.vue +++ b/app/app.vue @@ -20,7 +20,7 @@ diff --git a/budget.json b/budget.json index 6fb5058a..9b72f4a7 100644 --- a/budget.json +++ b/budget.json @@ -8,7 +8,7 @@ }, { "resourceType": "script", - "budget": 95 + "budget": 96 }, { "resourceType": "stylesheet", diff --git a/modules/markdown.ts b/modules/markdown.ts index 4e781c36..c42c8384 100644 --- a/modules/markdown.ts +++ b/modules/markdown.ts @@ -275,6 +275,7 @@ declare module '#md-page-meta.json' { interface PageMeta { title: string description?: string + llmLabel?: string } export const pageMeta: Record } diff --git a/modules/shared/page-meta.ts b/modules/shared/page-meta.ts index 0869438d..14e102cc 100644 --- a/modules/shared/page-meta.ts +++ b/modules/shared/page-meta.ts @@ -1,6 +1,8 @@ export interface PageMeta { title: string description?: string + /** Short label used in llms.txt page index. Pages without this are excluded from llms.txt. */ + llmLabel?: string } export const pageMeta: Record = { @@ -10,6 +12,7 @@ export const pageMeta: Record = { }, '/ai': { title: 'AI Usage', + llmLabel: 'How Daniel uses AI', }, '/ama': { title: 'Ask me anything', @@ -17,10 +20,12 @@ export const pageMeta: Record = { '/bio': { title: 'Bio', description: 'Biography of Daniel Roe', + llmLabel: 'Full biography', }, '/blog': { title: 'Blog', description: 'Blog posts by Daniel Roe', + llmLabel: 'All blog posts', }, '/feed': { title: 'Feed', @@ -34,10 +39,12 @@ export const pageMeta: Record = { '/talks': { title: 'Talks', description: 'Talks and presentations by Daniel Roe', + llmLabel: 'Conference talks and podcasts', }, '/uses': { title: 'Uses', description: 'Tools, software, and hardware that Daniel Roe uses', + llmLabel: 'Tools and software', }, '/voted': { title: 'Thank you!', @@ -45,5 +52,6 @@ export const pageMeta: Record = { '/work': { title: 'Work', description: 'Open source projects and past client work by Daniel Roe', + llmLabel: 'Open source projects and past client work', }, } diff --git a/nuxt.config.ts b/nuxt.config.ts index bc68e7b7..f5714a1b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -182,7 +182,7 @@ export default defineNuxtConfig({ future: { nativeSWR: true }, prerender: { crawlLinks: true, - routes: ['/', '/live', '/rss.xml', '/voted', '/work', '/feedback', '/ama', '/ai'], + routes: ['/', '/live', '/rss.xml', '/voted', '/work', '/feedback', '/ama', '/ai', '/llms.txt', '/llms-full.txt'], }, hooks: { 'prerender:generate' (route) { diff --git a/public/robots.txt b/public/robots.txt index ab15820a..90d9d205 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,4 @@ User-agent: * Disallow: /cv.pdf + +Sitemap: https://roe.dev/sitemap.xml diff --git a/server/routes/index.md.get.ts b/server/routes/index.md.get.ts index 0460e855..99bfa504 100644 --- a/server/routes/index.md.get.ts +++ b/server/routes/index.md.get.ts @@ -1,5 +1,6 @@ import { rawBlogPosts } from '#md-raw-blog.json' import { pageMeta } from '#md-page-meta.json' +import { links } from '#shared/utils/links' export default defineEventHandler(async () => { if (import.meta.test) { @@ -34,12 +35,9 @@ export default defineEventHandler(async () => { '', '## Links', '', - '- [GitHub](https://github.com/danielroe/)', - '- [Bluesky](https://bsky.app/profile/danielroe.dev)', - '- [LinkedIn](https://www.linkedin.com/in/daniel-roe/)', - '- [Mastodon](https://mastodon.roe.dev/@daniel)', - '- [YouTube](https://www.youtube.com/@danielroe)', - '- [Twitch](https://twitch.tv/danielroe)', + ...links + .filter(l => l.link.startsWith('https://')) + .map(l => `- [${l.name}](${l.link})`), '- [Email](mailto:daniel@roe.dev)', '', ] @@ -91,30 +89,3 @@ export default defineEventHandler(async () => { return mdResponse(lines.join('\n')) }) - -function formatDate (dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('en-GB', { - day: 'numeric', - month: 'long', - year: 'numeric', - }) -} - -interface Talk { - _id: string - title: string - source: string - link?: string - video?: string - date: string - type: string - group?: { _id: string } -} - -interface Conference { - name: string - dates: string - link?: string - location?: string -} diff --git a/server/routes/llms-full.txt.get.ts b/server/routes/llms-full.txt.get.ts new file mode 100644 index 00000000..42b7f93f --- /dev/null +++ b/server/routes/llms-full.txt.get.ts @@ -0,0 +1,105 @@ +import { rawBlogPosts } from '#md-raw-blog.json' +import { rawPages } from '#md-raw-pages.json' +import { links } from '#shared/utils/links' + +export default defineEventHandler(async () => { + if (import.meta.test) { + return new Response('', { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) + } + + const [talks, upcomingConferences] = await Promise.all([ + $fetch('/api/talks').catch(() => [] as Talk[]), + $fetch('/api/upcoming-conferences').catch(() => [] as Conference[]), + ]) + + const lines = [ + '# Daniel Roe', + '', + '> Nuxt core team lead, open source maintainer, and keynote speaker. Part of the team at Vercel. Based in Scotland.', + '', + '---', + '', + '## Bio', + '', + mdCleanHtml(rawPages['bio'] || ''), + '', + '---', + '', + '## AI Policy', + '', + mdCleanHtml(rawPages['ai'] || ''), + '', + '---', + '', + ] + + if (upcomingConferences.length) { + lines.push('## Upcoming Conferences') + lines.push('') + for (const conf of upcomingConferences) { + const link = conf.link ? `[${conf.name}](${conf.link})` : conf.name + lines.push(`- ${link} — ${conf.dates}${conf.location ? ` (${conf.location})` : ''}`) + } + lines.push('') + lines.push('---') + lines.push('') + } + + // Group talks and list them + if (talks.length) { + const groupedTalks: Record = {} + for (const talk of talks) { + const groupKey = talk.group?._id || talk._id + groupedTalks[groupKey] ||= [] + groupedTalks[groupKey]!.push(talk) + } + const talkSeries = Object.values(groupedTalks) + .map(g => g.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]!) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + + lines.push('## Talks') + lines.push('') + for (const talk of talkSeries) { + const talkLinks: string[] = [] + if (talk.video) talkLinks.push(`[Video](${talk.video})`) + if (talk.link) talkLinks.push(`[Link](${talk.link})`) + const suffix = talkLinks.length ? ` — ${talkLinks.join(', ')}` : '' + lines.push(`- ${talk.title} (${formatDate(talk.date)}, ${talk.source})${suffix}`) + } + lines.push('') + lines.push('---') + lines.push('') + } + + // All blog posts with descriptions + lines.push('## Blog Posts') + lines.push('') + for (const post of rawBlogPosts) { + lines.push(`### ${post.title}`) + lines.push('') + lines.push(`- **Date:** ${post.date}`) + lines.push(`- **URL:** https://roe.dev/blog/${post.slug}.md`) + if (post.tags.length) { + lines.push(`- **Tags:** ${post.tags.join(', ')}`) + } + if (post.description) { + lines.push('') + lines.push(post.description) + } + lines.push('') + } + + lines.push('---') + lines.push('') + lines.push('## Contact') + lines.push('') + lines.push('- Email: daniel@roe.dev') + for (const l of links.filter(l => l.link.startsWith('https://'))) { + lines.push(`- ${l.name}: ${l.link}`) + } + lines.push('') + + return new Response(lines.join('\n'), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) +}) diff --git a/server/routes/llms.txt.get.ts b/server/routes/llms.txt.get.ts new file mode 100644 index 00000000..28255b2e --- /dev/null +++ b/server/routes/llms.txt.get.ts @@ -0,0 +1,64 @@ +import { rawBlogPosts } from '#md-raw-blog.json' +import { pageMeta } from '#md-page-meta.json' +import { links } from '#shared/utils/links' + +export default defineEventHandler(async () => { + if (import.meta.test) { + return new Response('', { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) + } + + const upcomingConferences = await $fetch('/api/upcoming-conferences').catch(() => [] as Conference[]) + + const recentPosts = rawBlogPosts.slice(0, 5) + + const pageIndex = Object.entries(pageMeta) + .filter(([, meta]) => meta.llmLabel) + .map(([path, meta]) => `- [${meta.title}](https://roe.dev${path}.md): ${meta.llmLabel}`) + + const lines = [ + '# Daniel Roe', + '', + '> Nuxt core team lead, open source maintainer, and keynote speaker. Part of the team at Vercel. Based in Scotland.', + '', + '## About', + '', + ...pageIndex, + '', + '## Key Links', + '', + '- Website: https://roe.dev', + ...links + .filter(l => l.link.startsWith('https://')) + .map(l => `- ${l.name}: ${l.link}`), + '- Nuxt: https://nuxt.com', + '- Email: daniel@roe.dev', + '', + ] + + if (upcomingConferences.length) { + lines.push('## Upcoming Conferences') + lines.push('') + for (const conf of upcomingConferences) { + lines.push(`- ${conf.name} — ${conf.dates}${conf.location ? ` (${conf.location})` : ''}`) + } + lines.push('') + } + + if (recentPosts.length) { + lines.push('## Recent Blog Posts') + lines.push('') + for (const post of recentPosts) { + lines.push(`- [${post.title}](https://roe.dev/blog/${post.slug}.md) — ${post.date}`) + } + lines.push('') + } + + lines.push('## Optional') + lines.push('') + lines.push('- [Full content](https://roe.dev/llms-full.txt): Complete bio, blog index, and talks in a single document') + lines.push('') + + return new Response(lines.join('\n'), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) +}) diff --git a/server/routes/talks.md.get.ts b/server/routes/talks.md.get.ts index 1e89bd19..bedfe46e 100644 --- a/server/routes/talks.md.get.ts +++ b/server/routes/talks.md.get.ts @@ -70,32 +70,3 @@ export default defineEventHandler(async () => { return mdResponse(lines.join('\n')) }) - -function formatDate (dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('en-GB', { - day: 'numeric', - month: 'long', - year: 'numeric', - }) -} - -interface Talk { - _id: string - title: string - description?: string - source: string - tags: string[] - link?: string - video?: string - date: string - type: string - slides?: string - repo?: string - demo?: string - group?: { - _id: string - title: string - description?: string - } -} diff --git a/server/utils/md.ts b/server/utils/md.ts index 1083630c..21259eff 100644 --- a/server/utils/md.ts +++ b/server/utils/md.ts @@ -56,3 +56,39 @@ export function mdResponse (content: string): Response { headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, }) } + +export function formatDate (dateStr: string): string { + const date = new Date(dateStr) + return date.toLocaleDateString('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) +} + +export interface Talk { + _id: string + title: string + description?: string + source: string + tags?: string[] + link?: string + video?: string + date: string + type: string + slides?: string + repo?: string + demo?: string + group?: { + _id: string + title?: string + description?: string + } +} + +export interface Conference { + name: string + dates: string + link?: string + location?: string +} diff --git a/app/utils/data.ts b/shared/utils/links.ts similarity index 88% rename from app/utils/data.ts rename to shared/utils/links.ts index dd3c16a8..7923cfe5 100644 --- a/app/utils/data.ts +++ b/shared/utils/links.ts @@ -1,4 +1,10 @@ -export const links = [ +export interface SocialLink { + name: string + icon: string + link: string +} + +export const links: SocialLink[] = [ { name: 'GitHub', icon: 'i-ri:github-fill', diff --git a/test/unit/bundle.spec.ts b/test/unit/bundle.spec.ts index a75cce7d..e16e8541 100644 --- a/test/unit/bundle.spec.ts +++ b/test/unit/bundle.spec.ts @@ -36,7 +36,7 @@ describe('project sizes', () => { stats.client = await analyzeSizes('**/*.js', publicDir) expect .soft(roundToKilobytes(stats.client.totalBytes)) - .toMatchInlineSnapshot(`"334k"`) + .toMatchInlineSnapshot(`"335k"`) expect.soft(stats.client.files.map(f => f.replace(/\..*\.js/, '.js').replace(/_scripts\/.*\.js/, '_scripts/script.js')).sort()) .toMatchInlineSnapshot(` [ @@ -78,7 +78,7 @@ describe('project sizes', () => { stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) expect .soft(roundToKilobytes(stats.server.totalBytes)) - .toMatchInlineSnapshot(`"2332k"`) + .toMatchInlineSnapshot(`"2335k"`) const modules = await analyzeSizes('node_modules/**/*', serverDir) expect