Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/danielroe/roe.dev. This is the code and content for my personal website, built in Nuxt. roe.dev
Something went wrong. Try again.
roe.dev nuxt.config.ts
11 kB · 452 lines
TypeScript
at feat/nuxt-v5
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453import { mkdir, writeFile } from 'node:fs/promises'import { createRequire } from 'node:module'import { dirname } from 'node:path'import process from 'node:process'
import { defineNuxtConfig } from 'nuxt/config'import { extendViteConfig } from 'nuxt/kit'import { isTest } from 'std-env'import type { HmrOptions } from 'vite'import { pageMeta } from './modules/shared/page-meta.ts'
/** * Rendered routes served from the SWR cache. Applied outside development only: * nitro persists SWR responses to `.nuxt/cache`, so in dev an edit to content * or a component stays invisible until the cached entry expires. Tests opt in * so they cover what the cache does to a handler's return value. */const renderedSwrRules: Record<string, { swr: number }> = { ...Object.fromEntries(Object.keys(pageMeta).flatMap(path => [ [path, { swr: 60 * 60 }], [path + '/_payload.json', { swr: 60 * 60 }], [path + '.md', { swr: 60 * 60 }], ])), '/blog/**': { swr: 60 * 60 },}
/** API routes served from the SWR cache, in dev too, to spare the upstreams. */const apiSwrRules: Record<string, { swr: number }> = { '/api/talks': { swr: 60 * 60 }, '/api/upcoming-conferences': { swr: 60 * 60 }, '/api/uses': { swr: 60 * 60 }, '/api/projects': { swr: 60 * 60 }, '/api/current-location': { swr: 60 * 5 }, '/api/streams': { swr: 60 * 60 }, '/api/discover-bluesky-post': { swr: 60 * 60 },}
export default defineNuxtConfig({ modules: [ 'nuxt-og-image', '@nuxt/eslint', '@nuxt/test-utils/module', 'magic-regexp/nuxt', '@nuxt/image', '@nuxtjs/html-validator', '@unocss/nuxt', '@nuxtjs/color-mode', '@comark/nuxt', '@nuxtjs/plausible', '@nuxt/fonts', '@nuxt/scripts', ],
// TODO: remove when Nuxt v3.14 is released $development: { modules: [ function (_options, nuxt) { if (process.env.IDX_CHANNEL) { nuxt.hook('modules:done', () => { nuxt.options.vite.server ||= {} nuxt.options.vite.server.hmr ||= {} ;(nuxt.options.vite.server.hmr as HmrOptions).protocol = 'wss' }) } }, ], runtimeConfig: { admin: { baseUrl: 'http://127.0.0.1:3000', }, }, devServer: { host: '127.0.0.1', }, },
$production: { // modules: ['nuxt-security'], // routeRules: renderedSwrRules, },
$test: { modules: [ function () { extendViteConfig(config => { config.plugins = config.plugins?.filter(p => !p || !('name' in p) || p.name !== 'nuxt:scripts:bundler-transformer') }) }, ], routeRules: renderedSwrRules, experimental: { componentIslands: true, }, },
components: [ // markdown components are passed explicitly to `<MarkdownDocument>` rather // than registered globally, so they stay out of the client bundle { path: '~/components/markdown', pathPrefix: false }, '~/components', ],
imports: { polyfills: true, },
devtools: { enabled: true },
app: { head: { htmlAttrs: { lang: 'en' }, }, pageTransition: false, layoutTransition: false, },
css: ['@unocss/reset/tailwind.css', '~/assets/main.css'],
site: { url: 'https://roe.dev', },
runtimeConfig: { atproto: { password: '', did: '', handle: '', }, admin: { // Public origin used to derive the OAuth `client_id` // (`<baseUrl>/oauth-client-metadata.json`) and `redirect_uri`. Vercel // preview deployments self-host their own metadata so each preview // registers a fresh OAuth client with the IdP rather than pointing // back at production. baseUrl: process.env.VERCEL_ENV && process.env.VERCEL_ENV !== 'production' && process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'https://roe.dev', }, blobReadWriteToken: '', mastodon: { accessToken: '', }, linkedin: { accessToken: '', }, bluesky: { accessToken: '', }, youtube: { refreshToken: '', clientId: '', clientSecret: '', amaPlaylistId: 'PLQnM-cL9ttacD7fyv6yrtQxaICAUs-2KJ', }, voteUrl: '', // Pushover notifications pushover: { token: '', userKey: '', }, // Location API key for secure updates locationApiKey: '', bigdataApiKey: '', twitch: { clientId: '', clientSecret: '', }, github: { // my GH ID id: '28706372', // fetching GH repos in build + sponsors when deployed token: '', // token used to update GitHub status profileToken: '', // oauth flow clientSecret: '', inviteToken: '', }, public: { githubClientId: '', atproto: { did: '', service: '', }, }, },
routeRules: { ...apiSwrRules, '/admin/**': { prerender: false }, '/api/sponsors': { prerender: true }, '/api/hi': { cors: true }, // redirects '/feed.xml': { redirect: '/rss.xml' }, '/work': { redirect: '/projects' }, '/pad': { redirect: '/touchpad' }, '/thumbnail/**': { cache: { maxAge: 60 * 60 * 24 * 365 } }, '/blog/a-virtuous-cycle': { redirect: '/blog/virtuous-circle' }, '/blog/ai-writes-my-code': { redirect: 'https://www.youtube.com/watch?v=Zfs3BJZxKkc' }, '/chat': { redirect: 'https://roe.dev/blog/open-invitation' }, '/.well-known/webfinger/**': { redirect: { to: 'https://mastodon.roe.dev/.well-known/webfinger/**', statusCode: 301, }, }, },
sourcemap: { client: true, server: false },
future: { compatibilityVersion: 5, },
experimental: { viewTransition: true, },
compatibilityDate: '2025-06-09',
nitro: { externals: { // `@atproto-labs/fetch-node` depends on undici via npm aliases // (`undici_v6`, etc.), which nitro's externals tracing collapses to the // real package name, breaking the aliased imports at runtime. // https://github.com/nitrojs/nitro/issues/1574 traceAlias: Object.fromEntries( ['undici_v6', 'undici_v7', 'undici_v8'].flatMap(alias => { try { let req = createRequire(import.meta.url) for (const hop of ['@atproto/oauth-client-node', '@atproto-labs/handle-resolver-node', '@atproto-labs/fetch-node']) { req = createRequire(req.resolve(hop)) } const { version } = req(`${alias}/package.json`) return [[`.nitro/undici@${version}`, alias]] } catch { return [] } }), ), inline: ['forges'], }, replace: { 'import.meta.test': isTest, }, future: { nativeSWR: true }, hooks: { 'prerender:generate' (route) { if (route.fileName) route.fileName = route.fileName.replace( /(\.\w{2,3})\/index.html$/, '$1', )
if (route.error) { if (route.route.startsWith('/_ipx')) { console.warn('Could not prerender', route.route) // ignore IPX rendering errors delete route.error } else { console.error(route.route, route.error, route) process.exit(1) } } }, }, },
vite: { css: { // TODO: remove with `@nuxt/fonts` v1 lightningcss: {}, }, build: { modulePreload: { polyfill: false, }, }, $client: { build: { rollupOptions: { output: { codeSplitting: { groups: [ { name: 'shared', tags: ['$initial'], }, ], }, }, }, }, }, optimizeDeps: { include: [ '@atcute/bluesky-richtext-segmenter', '@atproto/lex-schema', '@formkit/drag-and-drop/vue', 'airspace', 'gsap', 'magic-regexp', 'mediabunny', 'modern-screenshot', 'partysocket', ], }, },
typescript: { // The generated lexicon types in `shared/lex/**` cross-reference each // other with `.ts` extensions so that Node’s native type-stripping can // resolve them at runtime (`node script.ts`) without a loader. Every // tsconfig that transitively includes those files needs to allow that. tsConfig: { compilerOptions: { allowImportingTsExtensions: true, }, }, nodeTsConfig: { include: ['../scripts'], }, },
postcss: { plugins: { 'postcss-nesting': {}, '@unocss/postcss': {}, }, },
hooks: { /** * The client manifest is inlined into the server bundle rather than emitted * to disk, so persist a copy when a consumer asks for one by setting * `NUXT_CLIENT_MANIFEST_PATH` (see `test/unit/bundle.spec.ts`). */ async 'build:manifest' (manifest) { const target = process.env.NUXT_CLIENT_MANIFEST_PATH if (!target) return await mkdir(dirname(target), { recursive: true }) await writeFile(target, JSON.stringify(manifest), 'utf8') }, },
eslint: { config: { stylistic: true, }, },
fonts: { families: [ { name: 'Barlow', preload: true }, ], },
htmlValidator: { failOnError: true, options: { rules: { 'unrecognized-char-ref': 'off', 'wcag/h37': 'warn', 'element-permitted-content': 'warn', 'element-required-attributes': 'warn', 'attribute-empty-style': 'off', }, }, },
image: { quality: 80, domains: [ 'www.dundeescript.co.uk', 'devoxx.gr', 'conference.vueschool.io', 'static-cdn.jtvnw.net', 'avatars.githubusercontent.com', 'raw.githubusercontent.com', 's3.nl-ams.scw.cloud', 'cdn.bsky.social', 'cdn.bsky.app', 'npmx.social', 'images.jsworldconference.com', 'www.middlesbroughfe.co.uk', 'res.cloudinary.com', 'cityjsconf.org', 'vuejsnation.com', 'vueconf.us', 'media.graphassets.com', 'secure.meetupstatic.com', 'cdn.evbuc.com', 'conf.vuejs.de', 'pragvue.com', 'www.scotlandis.com', 'conf.vuejs.de', 'perfnow.nl', 'www.vuetoronto.com', 'gdg.community.dev', 'www.lambdatest.com', ], screens: { logo: 40, avatar: 70, 380: 380, 760: 760, }, },
plausible: { domain: 'roe.dev', apiHost: 'https://v.roe.dev', },
prerender: { crawlLinks: true, routes: ['/rss.xml', '/llms.txt', '/llms-full.txt'], },
scripts: { defaultScriptOptions: { bundle: true, }, },
security: { headers: { crossOriginEmbedderPolicy: false, contentSecurityPolicy: { 'script-src-attr': ['\'self\'', '\'unsafe-inline\''], 'script-src': ['\'self\'', '\'unsafe-inline\'', 'https://static.cloudflareinsights.com'], 'img-src': ['\'self\'', 'data:', 'https://avatars.githubusercontent.com', 'https://www.google.com', 'https://*.gstatic.com', 'https://cdn.bsky.app', 'https://npmx.social'], }, }, },
social: { networks: { bluesky: { identifier: 'danielroe.dev', }, mastodon: { identifier: 'daniel@roe.dev', }, linkedin: { identifier: 'daniel-roe', }, }, },})