From eed00fdb3a99841d4c149eaa7fcc52fd23a5cc9a Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:30:55 +0200 Subject: [PATCH 01/10] feat: add project metadata config --- project.config.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 project.config.json diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..027b686 --- /dev/null +++ b/project.config.json @@ -0,0 +1,32 @@ +{ + "author": { + "contact": "hello@todde.tv", + "location": "Dresden, Germany", + "name": "Thorsten Seyschab", + "sameAs": [ + "https://github.com/toddeTV/", + "https://x.com/toddeTV", + "https://bsky.app/profile/todde.tv", + "https://www.linkedin.com/in/toddetv/" + ], + "url": "https://todde.tv" + }, + "legal": { + "legalNoticePath": "/legal-notice", + "privacyPolicyPath": "/privacy-policy" + }, + "projectName": "todde.tv", + "repository": { + "licenseUrl": "https://github.com/toddeTV/todde.tv/blob/main/LICENSE.md", + "url": "https://github.com/toddeTV/todde.tv" + }, + "security": { + "contact": "mailto:hello@todde.tv", + "preferredLanguages": [ + "en", + "de" + ] + }, + "siteDescription": "Personal portfolio of Thorsten Seyschab - IT consultant, senior full-stack developer, and conference speaker.", + "siteUrl": "https://todde.tv" +} \ No newline at end of file From 16fcae0c57a39278f5b8990f3d6a17fefe6270ee Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:31:02 +0200 Subject: [PATCH 02/10] feat: add text metadata route handlers --- server/routes/.well-known/security.txt.get.ts | 13 +++++++ server/routes/humans.txt.get.ts | 37 ++++++++++++++++++ server/utils/security-txt.ts | 39 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 server/routes/.well-known/security.txt.get.ts create mode 100644 server/routes/humans.txt.get.ts create mode 100644 server/utils/security-txt.ts diff --git a/server/routes/.well-known/security.txt.get.ts b/server/routes/.well-known/security.txt.get.ts new file mode 100644 index 0000000..649f0c7 --- /dev/null +++ b/server/routes/.well-known/security.txt.get.ts @@ -0,0 +1,13 @@ +import projectConfig from '~~/project.config.json' +import { buildSecurityTxtContent } from '#server/utils/security-txt' + +export default defineEventHandler((event) => { + const content = buildSecurityTxtContent({ + canonicalUrl: new URL('/.well-known/security.txt', projectConfig.siteUrl).toString(), + contact: projectConfig.security.contact, + preferredLanguages: projectConfig.security.preferredLanguages, + }) + + setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8') + return content +}) diff --git a/server/routes/humans.txt.get.ts b/server/routes/humans.txt.get.ts new file mode 100644 index 0000000..7b063df --- /dev/null +++ b/server/routes/humans.txt.get.ts @@ -0,0 +1,37 @@ +import projectConfig from '~~/project.config.json' + +export default defineEventHandler((event) => { + const { author, legal, projectName, repository, siteDescription, siteUrl } = projectConfig + const legalNoticeUrl = new URL(legal.legalNoticePath, siteUrl).toString() + const privacyPolicyUrl = new URL(legal.privacyPolicyPath, siteUrl).toString() + const runtimeConfig = useRuntimeConfig(event) + + const content = [ + '# humanstxt.org/', + '', + '/* PROJECT */', + ` Name: ${projectName}`, + ` Version: ${runtimeConfig.public.build.releaseLabel}`, + ` URL: ${siteUrl}`, + ` Description: ${siteDescription}`, + ` Legal Notice: ${legalNoticeUrl}`, + ` Privacy Policy: ${privacyPolicyUrl}`, + '', + '/* AUTHOR */', + ` Name: ${author.name}`, + ` Contact: ${author.contact}`, + ` Website: ${author.url}`, + ` Location: ${author.location}`, + '', + '/* SOURCE CODE */', + ` Repository: ${repository.url}`, + '', + '/* LEGAL DISCLAIMER */', + ` License: ${repository.licenseUrl}`, + ` Legal Notice: ${legalNoticeUrl}`, + ` Privacy Policy: ${privacyPolicyUrl}`, + ].join('\n') + + setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8') + return `${content}\n` +}) diff --git a/server/utils/security-txt.ts b/server/utils/security-txt.ts new file mode 100644 index 0000000..ed30cdf --- /dev/null +++ b/server/utils/security-txt.ts @@ -0,0 +1,39 @@ +const defaultSecurityTxtLifetimeDays = 180 + +export interface SecurityTxtContentOptions { + canonicalUrl: string + contact: string + policyUrl?: string + preferredLanguages: string[] +} + +export function createSecurityTxtExpires( + now: Date = new Date(), + lifetimeDays: number = defaultSecurityTxtLifetimeDays, +): string { + const expiresAt = new Date(now.getTime()) + + expiresAt.setUTCDate(expiresAt.getUTCDate() + lifetimeDays) + + return expiresAt.toISOString() +} + +export function buildSecurityTxtContent( + options: SecurityTxtContentOptions, + now: Date = new Date(), +): string { + const lines = [ + `Contact: ${options.contact}`, + `Expires: ${createSecurityTxtExpires(now)}`, + `Canonical: ${options.canonicalUrl}`, + `Preferred-Languages: ${options.preferredLanguages.join(', ')}`, + ] + + if (options.policyUrl) { + lines.push(`Policy: ${options.policyUrl}`) + } + + return `${lines.join('\n')}\n` +} + +export { defaultSecurityTxtLifetimeDays } From 17231dd7da931ecb79a81e951dc34b3e50099542 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:31:06 +0200 Subject: [PATCH 03/10] feat: prerender metadata text routes --- nuxt.config.ts | 25 +++++++++++++++++++------ public/_redirects | 3 +++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index a004be8..ea6ec1a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,7 +1,14 @@ import tailwindcss from '@tailwindcss/vite' +import projectConfig from './project.config.json' import { resolveBuildReleaseMetadata } from './server/utils/build-release-metadata' const buildReleaseMetadata = resolveBuildReleaseMetadata() +const staticMachineReadableTextRouteRule = { + prerender: true, + headers: { + 'cache-control': 'public, max-age=86400, s-maxage=604800', + }, +} as const export default defineNuxtConfig({ modules: [ @@ -32,8 +39,8 @@ export default defineNuxtConfig({ // titleTemplate, description, og:site_name, and htmlAttrs.lang are handled by `@nuxtjs/seo` via `site` // config - do not duplicate here. meta: [ - { name: 'application-name', content: 'todde.tv' }, - { name: 'author', content: 'Thorsten Seyschab' }, + { name: 'application-name', content: projectConfig.projectName }, + { name: 'author', content: projectConfig.author.name }, // Ignored by Google since 2009, but some minor search engines (Yandex, Baidu) still // consider it. Harmless to include. @@ -97,11 +104,10 @@ export default defineNuxtConfig({ ], site: { // for `@nuxtjs/seo` - shared site config used by all SEO sub-modules (like `ogImage`, `schemaOrg`, etc.) - url: 'https://todde.tv', + url: projectConfig.siteUrl, // name: 'todde.tv', - name: 'Thorsten Seyschab', - description: - 'Personal portfolio of Thorsten Seyschab - IT consultant, senior full-stack developer, and conference speaker.', + name: projectConfig.author.name, + description: projectConfig.siteDescription, defaultLocale: 'en', }, @@ -138,6 +144,13 @@ export default defineNuxtConfig({ // Privacy Policy alternative paths '/privacy': { redirect: { to: '/privacy-policy', statusCode: 301 } }, '/datenschutz': { redirect: { to: '/privacy-policy', statusCode: 301 } }, + + // Machine-readable metadata alias + '/security.txt': { redirect: { to: '/.well-known/security.txt', statusCode: 301 } }, + + // Build these machine-readable text endpoints as static files for Cloudflare Pages SSG. + '/.well-known/security.txt': staticMachineReadableTextRouteRule, + '/humans.txt': staticMachineReadableTextRouteRule, }, compatibilityDate: '2026-03-04', diff --git a/public/_redirects b/public/_redirects index 5a2601c..ce76e1d 100644 --- a/public/_redirects +++ b/public/_redirects @@ -9,3 +9,6 @@ # Privacy Policy alternative paths /privacy /privacy-policy 301 /datenschutz /privacy-policy 301 + +# Machine-readable metadata alias +/security.txt /.well-known/security.txt 301 From fafd2bd5f0f48b5eff0f1d659219d359fd36502f Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:51:11 +0200 Subject: [PATCH 04/10] refactor: simplify humans.txt metadata --- server/routes/humans.txt.get.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/routes/humans.txt.get.ts b/server/routes/humans.txt.get.ts index 7b063df..88608fe 100644 --- a/server/routes/humans.txt.get.ts +++ b/server/routes/humans.txt.get.ts @@ -21,15 +21,10 @@ export default defineEventHandler((event) => { ` Name: ${author.name}`, ` Contact: ${author.contact}`, ` Website: ${author.url}`, - ` Location: ${author.location}`, '', '/* SOURCE CODE */', ` Repository: ${repository.url}`, - '', - '/* LEGAL DISCLAIMER */', ` License: ${repository.licenseUrl}`, - ` Legal Notice: ${legalNoticeUrl}`, - ` Privacy Policy: ${privacyPolicyUrl}`, ].join('\n') setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8') From f4932e19209f090c93eb2d8d7678dfa65c591aa3 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:58:50 +0200 Subject: [PATCH 05/10] feat: add author details to humans.txt --- nuxt.config.ts | 2 +- project.config.json | 1 + server/routes/humans.txt.get.ts | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index ea6ec1a..109594b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -147,7 +147,7 @@ export default defineNuxtConfig({ // Machine-readable metadata alias '/security.txt': { redirect: { to: '/.well-known/security.txt', statusCode: 301 } }, - + // Build these machine-readable text endpoints as static files for Cloudflare Pages SSG. '/.well-known/security.txt': staticMachineReadableTextRouteRule, '/humans.txt': staticMachineReadableTextRouteRule, diff --git a/project.config.json b/project.config.json index 027b686..d101c6e 100644 --- a/project.config.json +++ b/project.config.json @@ -3,6 +3,7 @@ "contact": "hello@todde.tv", "location": "Dresden, Germany", "name": "Thorsten Seyschab", + "role": "IT consultant, senior full-stack developer, and conference speaker", "sameAs": [ "https://github.com/toddeTV/", "https://x.com/toddeTV", diff --git a/server/routes/humans.txt.get.ts b/server/routes/humans.txt.get.ts index 88608fe..b5c65bb 100644 --- a/server/routes/humans.txt.get.ts +++ b/server/routes/humans.txt.get.ts @@ -5,6 +5,7 @@ export default defineEventHandler((event) => { const legalNoticeUrl = new URL(legal.legalNoticePath, siteUrl).toString() const privacyPolicyUrl = new URL(legal.privacyPolicyPath, siteUrl).toString() const runtimeConfig = useRuntimeConfig(event) + const authorProfiles = author.sameAs.map(profileUrl => ` Profile: ${profileUrl}`) const content = [ '# humanstxt.org/', @@ -19,8 +20,10 @@ export default defineEventHandler((event) => { '', '/* AUTHOR */', ` Name: ${author.name}`, + ` Role: ${author.role}`, ` Contact: ${author.contact}`, ` Website: ${author.url}`, + ...authorProfiles, '', '/* SOURCE CODE */', ` Repository: ${repository.url}`, From 0b59fae7238a701c6118fa22a0c705658a08feae Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 19:59:56 +0200 Subject: [PATCH 06/10] refactor: inline security.txt builder --- server/routes/.well-known/security.txt.get.ts | 39 ++++++++++++++++++- server/utils/security-txt.ts | 39 ------------------- 2 files changed, 38 insertions(+), 40 deletions(-) delete mode 100644 server/utils/security-txt.ts diff --git a/server/routes/.well-known/security.txt.get.ts b/server/routes/.well-known/security.txt.get.ts index 649f0c7..2fc1eb5 100644 --- a/server/routes/.well-known/security.txt.get.ts +++ b/server/routes/.well-known/security.txt.get.ts @@ -1,5 +1,42 @@ import projectConfig from '~~/project.config.json' -import { buildSecurityTxtContent } from '#server/utils/security-txt' + +const defaultSecurityTxtLifetimeDays = 180 + +interface SecurityTxtContentOptions { + canonicalUrl: string + contact: string + policyUrl?: string + preferredLanguages: string[] +} + +function createSecurityTxtExpires( + now: Date = new Date(), + lifetimeDays: number = defaultSecurityTxtLifetimeDays, +): string { + const expiresAt = new Date(now.getTime()) + + expiresAt.setUTCDate(expiresAt.getUTCDate() + lifetimeDays) + + return expiresAt.toISOString() +} + +function buildSecurityTxtContent( + options: SecurityTxtContentOptions, + now: Date = new Date(), +): string { + const lines = [ + `Contact: ${options.contact}`, + `Expires: ${createSecurityTxtExpires(now)}`, + `Canonical: ${options.canonicalUrl}`, + `Preferred-Languages: ${options.preferredLanguages.join(', ')}`, + ] + + if (options.policyUrl) { + lines.push(`Policy: ${options.policyUrl}`) + } + + return `${lines.join('\n')}\n` +} export default defineEventHandler((event) => { const content = buildSecurityTxtContent({ diff --git a/server/utils/security-txt.ts b/server/utils/security-txt.ts deleted file mode 100644 index ed30cdf..0000000 --- a/server/utils/security-txt.ts +++ /dev/null @@ -1,39 +0,0 @@ -const defaultSecurityTxtLifetimeDays = 180 - -export interface SecurityTxtContentOptions { - canonicalUrl: string - contact: string - policyUrl?: string - preferredLanguages: string[] -} - -export function createSecurityTxtExpires( - now: Date = new Date(), - lifetimeDays: number = defaultSecurityTxtLifetimeDays, -): string { - const expiresAt = new Date(now.getTime()) - - expiresAt.setUTCDate(expiresAt.getUTCDate() + lifetimeDays) - - return expiresAt.toISOString() -} - -export function buildSecurityTxtContent( - options: SecurityTxtContentOptions, - now: Date = new Date(), -): string { - const lines = [ - `Contact: ${options.contact}`, - `Expires: ${createSecurityTxtExpires(now)}`, - `Canonical: ${options.canonicalUrl}`, - `Preferred-Languages: ${options.preferredLanguages.join(', ')}`, - ] - - if (options.policyUrl) { - lines.push(`Policy: ${options.policyUrl}`) - } - - return `${lines.join('\n')}\n` -} - -export { defaultSecurityTxtLifetimeDays } From 873f29fd7dc7b9d4ca84251784ebed5e31280433 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 21:36:25 +0200 Subject: [PATCH 07/10] fix: align site seo name --- nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 109594b..9a59094 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -106,7 +106,7 @@ export default defineNuxtConfig({ site: { // for `@nuxtjs/seo` - shared site config used by all SEO sub-modules (like `ogImage`, `schemaOrg`, etc.) url: projectConfig.siteUrl, // name: 'todde.tv', - name: projectConfig.author.name, + name: projectConfig.projectName, description: projectConfig.siteDescription, defaultLocale: 'en', }, From b156dbebc6985567c5ae9c703d135f23c4cd4910 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 21:36:49 +0200 Subject: [PATCH 08/10] docs: add security txt jsdoc --- server/routes/.well-known/security.txt.get.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routes/.well-known/security.txt.get.ts b/server/routes/.well-known/security.txt.get.ts index 2fc1eb5..41dc2e0 100644 --- a/server/routes/.well-known/security.txt.get.ts +++ b/server/routes/.well-known/security.txt.get.ts @@ -9,6 +9,7 @@ interface SecurityTxtContentOptions { preferredLanguages: string[] } +/** Returns the ISO 8601 expiration timestamp for `security.txt`. */ function createSecurityTxtExpires( now: Date = new Date(), lifetimeDays: number = defaultSecurityTxtLifetimeDays, @@ -20,6 +21,7 @@ function createSecurityTxtExpires( return expiresAt.toISOString() } +/** Builds the plain text `security.txt` response body from route metadata. */ function buildSecurityTxtContent( options: SecurityTxtContentOptions, now: Date = new Date(), From a424b649333343d4e57dd6d7d2c49058f81f665b Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 21:37:11 +0200 Subject: [PATCH 09/10] docs: add humans txt jsdoc --- server/routes/humans.txt.get.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routes/humans.txt.get.ts b/server/routes/humans.txt.get.ts index b5c65bb..a75b7b6 100644 --- a/server/routes/humans.txt.get.ts +++ b/server/routes/humans.txt.get.ts @@ -1,5 +1,6 @@ import projectConfig from '~~/project.config.json' +/** Returns the plain text `humans.txt` response using `projectConfig`, `useRuntimeConfig`, and `setResponseHeader`. */ export default defineEventHandler((event) => { const { author, legal, projectName, repository, siteDescription, siteUrl } = projectConfig const legalNoticeUrl = new URL(legal.legalNoticePath, siteUrl).toString() From 76a63a612b8535a8d515199dfbd3831c1ee88d9a Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Mon, 18 May 2026 21:46:23 +0200 Subject: [PATCH 10/10] fix: restore person-first site name --- nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 9a59094..109594b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -106,7 +106,7 @@ export default defineNuxtConfig({ site: { // for `@nuxtjs/seo` - shared site config used by all SEO sub-modules (like `ogImage`, `schemaOrg`, etc.) url: projectConfig.siteUrl, // name: 'todde.tv', - name: projectConfig.projectName, + name: projectConfig.author.name, description: projectConfig.siteDescription, defaultLocale: 'en', },