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