Portfolio page data loading
Portfolio, content, and Writing pages use useContentAsyncData (app/composables/useContentAsyncData.ts) instead of bare useAsyncData.
Nuxt Content collections are loaded through fetchContentCollection → GET /api/content/:collection (Nitro + Content DB). Do not call client queryCollection() in page setup: the WASM adapter can race and return empty rows, which triggers createError and leaves Suspense stuck on the previous page until a hard refresh.
The content API allowlists collection names server-side (home, profile, resume, product, gallery, code, writing, docs, caseStudies, decisionCards) and returns a constant 404 Unknown collection (no raw router-param echo) for anything outside the set. Optional query params: mode=first|all, slug, path (used by technical docs). Keep ContentCollectionName in fetchContentCollection.ts in sync with that allowlist — /code and /blog both fail with empty content if a collection is missing from either side. The code collection also stores snippet-browser rows (samples in content/code.json) — not a MessageStore table.
In-app hubs: /gallery (social feed + grid; primary nav) and /docs (markdown from the repo docs/ tree; Work sub-nav, not primary). Writing shelf is /blog (primary label Writing).
Content DB vs app data stores
| Layer | local / development |
test / production (Lambda) |
|---|---|---|
| Nuxt Content (pages, case studies, resume) | Default file SQLite (dev) | In-memory SQLite (filename: ':memory:') restored from the build dump — no Content SQLite file on Lambda |
| MessageStore / BlogPostStore | SQLite / Postgres | DynamoDB only |
Do not put Content or app stores on Postgres for test/prod. Do not use /tmp/*.sqlite for Content on Lambda (still SQLite-on-serverless). See data-stores.md.
SPA navigation requirements
- No
pageTransition/layoutTransitionwithmode: 'out-in'— Nuxt wraps async<script setup>pages inSuspense. Vue’sTransition(mode="out-in")+Suspensecan leave the entering page unmounted on client navigations until a full reload (nuxt/nuxt#32371). This app disables both transitions inconfig-properties/app-prop.ts(no enter/leave CSS was defined anyway). NuxtPagekeyed byroute.fullPathinapp.vueso each path gets a fresh page instance.- Fetch with
useContentAsyncData+fetchContentCollection(or$fetchfor Writing//api/postsnotes) — reuse SSR payload only while hydrating; refetch on later client navigations. Handlers still run through a global lock as a defensive serialiser. - Bind templates to
computed()wrappers (or the asyncdataref), not one-time.valuecopies. - Dynamic routes (
work/[slug],blog/[slug],docs/[...slug]) use a reactive key +watchon the route param. - Avoid
Promise.allof multiple content loads on one page; prefer sequentialuseContentAsyncDataawaits. - Pug +
<script setup>: helpers imported only for template use can be elided (Vue cannot see Pug references). Call them in script — e.g. GallerygridTiles, CodereposwithlanguageLabelfromcodeLanguageLabel— and bind precomputed fields in the template.