Files
LetsBeBiz-Site/docs/superpowers/specs/2026-04-07-seo-optimization-design.md
Matt 518f86265e
All checks were successful
Build & Push / build-and-push (push) Successful in 1m28s
feat: deep SEO optimization — metadata, OG tags, sitemap, structured data, GA4
- Add metadataBase and localized generateMetadata to all pages (EN/FR)
- Add Open Graph and Twitter card defaults with branded OG image
- Add canonical URLs and hreflang alternates on every page
- Create robots.ts (allow all, block /admin/ and /api/)
- Create sitemap.ts with all routes x 2 locales and hreflang
- Add Organization, WebSite, and CreativeWork JSON-LD structured data
- Add GA4 (G-LD348WF8EM) with Consent Mode v2 (default denied for EEA)
- Add llms.txt for AI discoverability
- Add production nginx config for letsbe.biz

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:38:52 -04:00

5.1 KiB

SEO Optimization — Design Spec

Date: 2026-04-07 Domain: letsbe.biz Stack: Next.js 16 App Router, next-intl (en/fr, as-needed prefix), Payload CMS

Current State

The site has a single metadata export in the layout with a generic title/description. No robots.txt, no sitemap, no OG tags, no hreflang, no structured data, no analytics. The services page has a static English-only metadata export. Case study pages and the homepage have no metadata at all.

1. Metadata Foundation

metadataBase

Add metadataBase: new URL('https://letsbe.biz') to src/app/(frontend)/[locale]/layout.tsx. Required for OG images and canonical URLs to resolve as absolute URLs.

i18n SEO keys

Add a meta section to both en.json and fr.json:

{
  "meta": {
    "home": { "title": "...", "description": "..." },
    "about": { "title": "...", "description": "..." },
    "services": { "title": "...", "description": "..." },
    "work": {
      "monaco-ocean": { "title": "...", "description": "..." },
      "port-nimara": { "title": "...", "description": "..." },
      "port-amador": { "title": "...", "description": "..." }
    }
  }
}

Per-page generateMetadata

Convert every page to use generateMetadata({ params }):

  • Read locale from params
  • Call getTranslations({ locale, namespace: 'meta' }) for localized title/description
  • Set alternates.canonical (e.g., https://letsbe.biz/about for en, https://letsbe.biz/fr/about for fr)
  • Set alternates.languages for hreflang:
    • en → unprefixed path
    • fr/fr/ prefixed path
    • x-default → unprefixed (English)

Layout metadata

Convert the layout's static metadata to generateMetadata() so it can set locale-aware defaults, metadataBase, and default OG/Twitter config.

2. Social Sharing (Open Graph / Twitter)

Default OG in layout

openGraph: {
  type: 'website',
  siteName: 'LetsBe.',
  locale: locale === 'fr' ? 'fr_FR' : 'en_US',
  images: [{ url: '/images/og-default.png', width: 1200, height: 630 }],
}
twitter: {
  card: 'summary_large_image',
}

OG image

Create a 1200x630 OG default image using sharp — the logo centered on a branded background (the site's surface color with the blue accent). Place at public/images/og-default.png.

Per-page OG

Case study pages override with their hero image. Other pages use the default.

3. Crawl Infrastructure

robots.ts

src/app/robots.ts:

  • Allow all user agents
  • Disallow /admin/, /api/
  • Sitemap: https://letsbe.biz/sitemap.xml

sitemap.ts

src/app/sitemap.ts:

  • Static routes: /, /about, /services
  • Dynamic routes: /work/monaco-ocean, /work/port-nimara, /work/port-amador
  • Each entry includes both locale variants in alternates.languages
  • lastModified set to build date
  • changeFrequency and priority set appropriately

4. Structured Data (JSON-LD)

Inject via <script type="application/ld+json"> in layout and page components.

Organization (layout)

{
  "@type": "Organization",
  "name": "LetsBe.",
  "url": "https://letsbe.biz",
  "logo": "https://letsbe.biz/images/letsbe-logo-short.png",
  "sameAs": ["linkedin-url", "x-url"]
}

WebSite (homepage)

{
  "@type": "WebSite",
  "name": "LetsBe.",
  "url": "https://letsbe.biz"
}

Service (services page)

One Service entry per pillar (Design, Software, Infrastructure).

CreativeWork (case study pages)

Per-project with title, description, image, creator → Organization.

GA4 integration

  • Measurement ID: G-LD348WF8EM
  • Use Next.js @next/third-parties/google GoogleAnalytics component (or next/script if the package isn't available)
  • Place in the layout, render only in production (process.env.NODE_ENV === 'production')

Set default consent state before GA loads:

gtag('consent', 'default', {
  analytics_storage: 'denied',
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  wait_for_update: 500,
});

This lets GA4 run in cookieless mode (modeled conversions, basic pageviews) without requiring a cookie banner. A full consent banner can be added later to unlock full measurement.

Environment variable

NEXT_PUBLIC_GA_ID=G-LD348WF8EM in .env / .env.production.

Files to Create/Modify

File Action
src/app/(frontend)/[locale]/layout.tsx Convert to generateMetadata, add OG, add JSON-LD Organization
src/app/(frontend)/[locale]/page.tsx Add generateMetadata, add JSON-LD WebSite
src/app/(frontend)/[locale]/about/page.tsx Add generateMetadata
src/app/(frontend)/[locale]/services/page.tsx Replace static metadata with generateMetadata, add JSON-LD Service
src/app/(frontend)/[locale]/work/[slug]/page.tsx Add generateMetadata, add JSON-LD CreativeWork
src/i18n/messages/en.json Add meta section
src/i18n/messages/fr.json Add meta section
src/app/robots.ts Create
src/app/sitemap.ts Create
public/images/og-default.png Create (generated via sharp)
src/components/analytics/GoogleAnalytics.tsx Create (GA4 + consent mode)
.env.production Add NEXT_PUBLIC_GA_ID