GenerateSaaS

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 in apps/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 in nitro.prerender.routes per locale (and /blog when config.blog.enabled).
  • Config-driven structured data: site-wide Organization + WebSite JSON-LD built entirely from config; per-page WebPage + BreadcrumbList via usePageSeo().
  • Flag-aware footer: blog links drop from the footer when config.blog.enabled is false (each marked enabled: config.blog.enabled in packages/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 under home.meta. Edit packages/i18n/translations/en/web.json (other locales auto-generate).
  • In templates use $t('landing.…'); in <script setup> use useI18n().
  • Hero avatars are the one shared data export, socialProofAvatars in packages/config/src/landing.ts (five /avatars/avatar-*.webp). Replace the array and the files in public/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/config by components in app/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.

OutputSourceNotes
Title templateconfig.siteName via app.head.titleTemplate"%s %separator %siteName" in nuxt.config.ts
Twitter site handlegetTwitterHandle(config.social?.x)seo.meta.twitterSiteextracts @handle from the X profile URL
OG imagestatic /og.png (1200×630, summary_large_image) set globally in app.vueblog posts override with their featured image
Organization JSON-LDconfig.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?.addressvia schemaOrg.identity
JSON-LD sameAsnon-empty URLs in config.social
description in JSON-LD + RSSconfig.seo.descriptionnot the HTML meta description
WebSite + SearchActionuseSchemaOrg(defineWebSite(...)) in default layoutSearchAction added only when config.blog.enabled
Per-page WebPage + BreadcrumbListusePageSeo() on public pages
robots.txt + sitemapconfig.indexablesee 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.

Staterobots.txtsitemap
true (production)Allow: / + sitemap pointerone 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 groupMechanism
/dashboard, /admin, /settings, /notificationsuseRobotsRule({ noindex, nofollow }) in layouts/dashboard.vue
/auth, /onboarding, /accept-invitation, /unsubscribeuseRobotsRule(...) in layouts/clean.vue
/blog/searchuseRobotsRule(...) 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.

On this page