The CGWire marketing site. Nuxt 4, Vue 3, fully statically generated, deployed to Cloudflare Workers and Netlify.
For the broader workspace (Kitsu, Zou, gazu and how they relate to this site), see ../CLAUDE.md. The conventions below apply specifically to this repo.
- Nuxt 4 with
nuxt generate—preset: "static", no SSR at runtime. - Vue 3 with
<script setup>. - Nuxt Content v3 for the content collections (markdown + JSON sources), backed by
better-sqlite3. - Nuxt i18n v10 for EN / FR / JA with custom localized routes.
- Bulma 1 plus Stylus for styling.
app/
components/ Reusable Vue components (Footer, ComparisonTable, Trial, UserLogos, ...)
composables/ usePage, useI18NSlug, useSEO, useStudio, useTestimonials, ...
data/ Static JS data (e.g. self-hosted-features.js)
layouts/ Page layouts (tool-alternative, studio, ...)
pages/ Top-level routes; dynamic ones via [slug].vue patterns
assets/styles/ Global Stylus and CSS
content/
{en,fr,ja}_pages.json Main per-locale collections: pages, alternatives, features,
audiences, studios, testimonials, customer_stories
pages/{en,fr,ja}/*.md Long-form markdown pages (privacy, terms, self-hosted, ...)
faq/{en,fr,ja}/*.md FAQ entries
tools/{en,fr,ja}/*.md Free-tool landing pages
i18n/
routes.js Localized URL templates per named route
locales/{en,fr,ja}.json UI strings and per-slug URL translations
content.config.ts Defines the jsonPages collection (custom source from *_pages.json)
nuxt.config.ts Build config + the explicit prerender route generator
scripts/ Tooling that is not part of the runtime
deepl-translate.js DeepL CLI wrapper used for first-pass translations
Four content shapes coexist:
jsonPages— customdefineCollectionSourcereading fromcontent/{lang}_pages.json. Each top-level key (pages,alternatives,audiences,features,studios,testimonials,customer_stories) becomes apageType. The object key is the slug.markdownPages— long-form markdown undercontent/pages/{lang}/.faq— markdown undercontent/faq/{lang}/.tools— markdown undercontent/tools/{lang}/.
In content.config.ts, pagesSource.getItem returns a JSON-stringified payload, not a JS object. Nuxt Content uses the returned content string to compute a checksum for its dev cache; returning an object would coerce to "[object Object]" and the cache would never invalidate on edits. See content.config.ts for the rationale comment.
- Default locale:
en(no URL prefix). FR uses/fr/..., JA uses/ja/.... - Per-route URL templates live in
i18n/routes.js(e.g.slug-alternative,features-slug). - Per-slug translations live in
i18n/locales/{lang}.jsonunderslugs.*. useI18NSlug.jsresolves the URL slug back to the canonical English slug for the query. It useste()to fall back silently when no translation exists (e.g., studio slugs are universal across locales).
npm run buildrunsnuxt generate. Output lives in.output/public/only —preset: "static", noserver/directory.nuxt.config.tsbuilds the full list of dynamic routes to prerender by readingcontent/en_pages.json, the FAQ/tools markdown filenames, and applying i18n slug translations. The native crawler misses many dynamic routes (notably the studios pages) when they are not linked from already-prerendered pages, so this list is the authoritative one.failOnError: trueis set on the prerenderer: a render failure aborts the build instead of dropping the page silently from the output.- Deploys: Cloudflare Workers via
wrangler deploy, and Netlify (zero-config, picks upnuxt generateautomatically).
- Nuxt Content's dev cache lives outside
.nuxt. It is in.data/content/contents.sqlite. When editing files undercontent/,rm -rf .nuxt .data && npm run devis the reliable reset.rm -rf .nuxtalone leaves the SQLite cache untouched and your edits invisible. - JSON content edits do not hot-reload the collections in dev. Restart the dev server after editing
*_pages.json. - Slug translations are bidirectional in
i18n/locales/{lang}.json:slugs.casting-managementmaps to the FR slug, andslugs.gestion-des-castingsmaps back to the canonical EN slug. Both directions are required for the URL → query lookup.
For user-facing copy on this site:
- No em dashes (
—). Use commas, parentheses, colons, or short sentences. - No marketing superlatives (
best,ultimate,leading,revolutionary). - Direct, factual, confident without being aggressive.
- Acknowledge competitors' strengths factually; do not attack.
- French and Japanese versions are kept in sync with the English source. The English version is rewritten first, reviewed, and only then translated.
Prefix every commit subject with a lowercase bracketed scope, then a description:
[scope] What changed in a few words
Scopes used in this repo include: chore, i18n, content, qa, index, partners, self-hosted, shotgrid, ftrack, studios, pricing, footer, seo, build. Add a new scope if needed; keep it lowercase and short.
Examples:
[i18n] Fix broken locale entries[shotgrid] Rewrite EN page with stronger angles[chore] Remove dead CSS[content] Tighten subtitle on alternatives pages[build] Prerender all dynamic routes explicitly
Body (optional) explains the why, not the what. Co-author lines are welcome.
DeepL API wrapper for translation work. Requires DEEPL_API_KEY in the env (free-tier keys end in :fx and route to api-free.deepl.com; paid keys go to api.deepl.com).
DEEPL_API_KEY=... node scripts/deepl-translate.js --to JA "Hello, world"
echo "long text" | DEEPL_API_KEY=... node scripts/deepl-translate.js --to JA --formality prefer_more
DEEPL_API_KEY=... node scripts/deepl-translate.js --to FR --from EN --html "<p>text</p>"
Intended for first-pass Japanese translations of new copy. Always review the output against the editorial conventions above and the rest of the site before committing — DeepL is faithful to meaning but neutral on brand voice.