Marketing & SEO
Assemble the prerendered landing page and ship structured data, sitemaps, and robots from config.
The marketing surface is driven by @repo/config - the config.seo block, the config.indexable master switch, and packages/config/src/landing.ts - with SEO output (JSON-LD, sitemap, robots) emitted by @nuxtjs/seo. Marketing routes are prerendered at build.
What you get
- Prerendered landing page (
app/pages/index.vue) assembled from section components inapps/web-nuxt/app/components/landing/- in order: Hero (landing/hero/), StatsRow, DemoVideo, Showcase, FeaturesListicle, HowItWorks, ProblemSolution, TestimonialsGrid, Pricing, PricingComparison, FAQ, FinalCTA. Most are lazy +hydrate-on-visible. - Prerendered marketing routes:
/,/pricing,/contact, plus legal pages/terms,/privacy,/cookies,/refund- enumerated innitro.prerender.routesper locale (and/blogwhenconfig.blog.enabled). - Config-driven structured data: site-wide
Organization+WebSiteJSON-LD built entirely from config; per-pageWebPage+BreadcrumbListviausePageSeo(). - Flag-aware footer: blog links drop from the footer when
config.blog.enabledis false (each markedenabled: config.blog.enabledinpackages/config/src/footer.ts).
Editing landing copy
All visible copy is i18n - there is no hardcoded marketing text.
- Section copy lives under
landing.*keys; title/description underhome.meta. Editpackages/i18n/translations/en/web.json(other locales auto-generate). - In templates use
$t('landing.…'); in<script setup>useuseI18n(). - Hero avatars are the one shared data export,
socialProofAvatarsinpackages/config/src/landing.ts(five/avatars/avatar-*.webp). Replace the array and the files inpublic/avatars/when rebranding. - Legal pages are Markdown in
packages/content/{locale}/{terms,privacy,cookies,refund}.md, rendered via@nuxt/content. Identity is injected through MDC tokens -:site-name,:business-name,:business-address,:registration-number,:base-url,:support-email- resolved from@repo/configby components inapp/components/content/. Edit the Markdown, not the tokens.
SEO sources
Every SEO artifact is config-derived. This table maps each output to the config it reads.
| Output | Source | Notes |
|---|---|---|
| Title template | config.siteName via app.head.titleTemplate | "%s %separator %siteName" in nuxt.config.ts |
Twitter site handle | getTwitterHandle(config.social?.x) → seo.meta.twitterSite | extracts @handle from the X profile URL |
| OG image | static /og.png (1200×630, summary_large_image) set globally in app.vue | blog posts override with their featured image |
| Organization JSON-LD | config.business?.name, config.baseUrl, config.email.senders.support.email, config.logo?.main, config.seo?.foundingDate (generated as a current-year placeholder), config.phone, config.business?.address | via schemaOrg.identity |
JSON-LD sameAs | non-empty URLs in config.social | |
description in JSON-LD + RSS | config.seo.description | not the HTML meta description |
WebSite + SearchAction | useSchemaOrg(defineWebSite(...)) in default layout | SearchAction added only when config.blog.enabled |
Per-page WebPage + BreadcrumbList | usePageSeo() on public pages | |
| robots.txt + sitemap | config.indexable | see below |
config.seo.description is not your HTML meta description - it feeds the Organization JSON-LD and blog RSS feed. The HTML meta description comes from home.meta / per-page SEO.
The indexable switch
config.indexable (default true) is the master switch wired into @nuxtjs/seo via site.indexable and the sitemap block in nuxt.config.ts.
| State | robots.txt | sitemap |
|---|---|---|
true (production) | Allow: / + sitemap pointer | one entry per route × locale with hreflang |
false (demos/staging) | Disallow: / | skipped entirely |
Private routes get a per-route noindex meta tag (not a robots.txt Disallow - deliberate, so crawlers read the directive and de-index instead of leaving stub entries), independent of the switch:
| Route group | Mechanism |
|---|---|
/dashboard, /admin, /settings, /notifications | useRobotsRule({ noindex, nofollow }) in layouts/dashboard.vue |
/auth, /onboarding, /accept-invitation, /unsubscribe | useRobotsRule(...) in layouts/clean.vue |
/blog/search | useRobotsRule(...) in pages/blog/search.vue |
These same paths are also in the sitemap exclude list.
Edit config.business, config.phone, config.social, and config.seo before launch - the defaults are placeholders that ship straight into your structured data.
Frequently asked questions
Why isn't my config.seo.description showing as the page meta description?
By design - it populates the Organization JSON-LD and RSS feed, not the HTML <meta name="description">. Set page-level descriptions through home.meta and usePageSeo().
How do I keep a staging deploy out of Google?
Set config.indexable = false. robots.txt swaps to Disallow: / and no sitemap is generated.
Where does the pricing section get its data?
From pricingConfig.plans - see Plans & pricing. The marketing pricing section is not gated on config.payment.enabled; CTAs always route signed-out visitors to /auth (then on to /settings/billing to check out).
Do I have to edit Vue files to change the headline?
No. All copy is i18n - edit landing.* in packages/i18n/translations/en/web.json.