feat: deep SEO optimization — metadata, OG tags, sitemap, structured data, GA4
All checks were successful
Build & Push / build-and-push (push) Successful in 1m28s
All checks were successful
Build & Push / build-and-push (push) Successful in 1m28s
- 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>
This commit is contained in:
151
docs/superpowers/specs/2026-04-07-seo-optimization-design.md
Normal file
151
docs/superpowers/specs/2026-04-07-seo-optimization-design.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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`:
|
||||
```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
|
||||
```ts
|
||||
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)
|
||||
```json
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "LetsBe.",
|
||||
"url": "https://letsbe.biz",
|
||||
"logo": "https://letsbe.biz/images/letsbe-logo-short.png",
|
||||
"sameAs": ["linkedin-url", "x-url"]
|
||||
}
|
||||
```
|
||||
|
||||
### WebSite (homepage)
|
||||
```json
|
||||
{
|
||||
"@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.
|
||||
|
||||
## 5. Google Analytics + Consent Mode
|
||||
|
||||
### 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'`)
|
||||
|
||||
### Consent Mode v2
|
||||
Set default consent state before GA loads:
|
||||
```js
|
||||
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` |
|
||||
Reference in New Issue
Block a user