# SEO & i18n Quality Audit — advmfr-next

**Date:** 2026-06-22
**Branch:** `audit/seo-i18n`
**Scope:** Static-code audit of SEO quality and i18n (next-intl) management.
**Method:** Two parallel deep code-audit passes (i18n + SEO) cross-checked, plus manual spot-verification of the top findings. Runtime crawl via d3k was attempted but blocked by the sandbox (see "Runtime verification" below).

---

## Verdict

The internationalization and SEO foundations are **high quality and well-disciplined**: next-intl is configured textbook-correctly (`as-needed` prefixing, localized pathnames, locale-aware navigation everywhere, full static rendering), 661/661 message keys are at parity EN/FR, and the SEO layer already ships hreflang + x-default, per-page metadata, JSON-LD, OG images, RSS feeds and a manifest.

The problems are **not architectural** — they are mostly **next-intl starter-template leftovers that shipped to production** plus a handful of high-value SEO gaps. The single most damaging issue is that the **homepage's English title/description are still the next-intl demo placeholder text**.

---

## Prioritized action list

### 🔴 CRITICAL
1. **Homepage EN metadata is next-intl boilerplate.** `messages/en.json:258-259` → `IndexPage.seo.title = "jewellery manufacturing"`, `metaDescription = "A basic example that demonstrates the usage of next-intl with the Next.js App Router."` This is the demo placeholder, shipped to the most important page. The FR side is real copy. Correct values exist in the source of truth `~/Documents/Advmfr/website/pages/home/en/index.meta.json` (title `"Fine Jewellery Manufacturer in Thailand | Advance Mfrs"`; description about 30+ yrs OEM/ODM, RJC certified). **Fix `IndexPage.seo` in `messages/en.json`.**

### 🟠 HIGH
2. **`metadataBase`/`site.url` can resolve to `localhost`.** `src/config/site.ts:8` falls back to `http://localhost:${port}` when neither `NEXT_PUBLIC_SITE_URL` nor `VERCEL_PROJECT_PRODUCTION_URL` is set. The guard only fires for `VERCEL_ENV === "production"` (`site.ts:10`), so any non-Vercel/static/preview build with no env emits `localhost` canonicals, hreflang, OG URLs and sitemap URLs. **Ensure `NEXT_PUBLIC_SITE_URL` is always set in the deploy pipeline** (`.env.example` ships it empty).
3. **`/pathnames` demo route shipped & indexed.** `src/app/[locale]/pathnames/page.tsx` is the unmodified next-intl example page (body copy in `messages/en.json:329-331` is demo text). It has full SEO metadata + JSON-LD **and is listed in the sitemap** (`src/app/sitemap.ts:15`) and routing (`routing.ts:9`). **Remove the route + its `routing.ts`/`sitemap.ts` entries + the `PathnamesPage` namespace in both message files** (or, at minimum, add it to `seo.sitemap.exclude`).
4. **Demo promo links render on the production 404 & error pages.** `src/components/PageLayout.tsx:23-34` unconditionally renders next-intl "Docs" (→ next-intl.dev) and "Source code" (→ next-intl GitHub example) links — message keys `PageLayout.links.*` (`messages/*.json:312-325`). `PageLayout` backs `NotFoundPage.tsx` and `[locale]/error.tsx:20`, so every 404/error shows off-brand next-intl promo links in the demo's slate/cyan styling. **Strip or rebrand these links.**
5. **No `LocalBusiness`/geo/telephone schema for a physical Bangkok B2B factory.** `src/config/seo/structured-data.ts:20-39` emits Organization with email + locality/country only — no `telephone`, street, geo, or `LocalBusiness`/`JewelryStore` type. Phone is rendered in the UI (`ContactInfo.tsx:50`) but absent from `site.contact` (`site.ts:35-39`) and all JSON-LD. Source of truth explicitly requests "Organization + LocalBusiness (Bangkok geo)". **High-value local-SEO miss.**

### 🟡 MEDIUM
6. **Dead `redirect("/en")` root page.** `src/app/page.tsx:5` redirects `/` → `/en`, contradicting `as-needed` (EN home must be at `/` with no prefix; the proxy already handles `/`). It's shadowed by middleware today — leftover from the static-export example. **Delete it.**
7. **Dead structured-data builders / missed rich results.** `articleJsonLd` and `eventJsonLd` (`structured-data.ts`) are defined but never used. `news-events/page.tsx` (Jewellery Trade Fairs 2026) emits only WebPage + BreadcrumbList — an ideal `Event` schema candidate. No `FAQPage` schema despite a `DesignFaq` component and source-of-truth FAQ flag.
8. **`/[category]` declared but unimplemented.** `routing.ts:21` declares `"/[category]"` but there is no `src/app/[locale]/[category]/page.tsx` (only `[category]/[slug]`). Bare category URLs fall to the `[...rest]` catch-all → 404. No indexable category landing pages (lost long-tail SEO); the routing declaration is misleading.
9. **SERP truncation.** After the `| ADV MFR` template (`meta.ts:13`), several titles exceed ~70 chars (Contact ~85, Gallery ~72) and several descriptions exceed ~160 chars (Sustainability 210, Contact 204, News 198, Production 163). **Tighten in `messages/en.json`.** Note: article titles skip the brand template entirely (`[category]/[slug]/page.tsx:57`) — inconsistent.
10. **Route-file conventions (project rule §3).** `loading.tsx` is missing everywhere (incl. `[locale]/`); content segments (about-us, gallery, contact-us, blog, etc.) have no own `loading/error/not-found` — they inherit the `[locale]` ones, so baseline UX is intact but the project rule is violated.
11. **Hardcoded a11y strings (FR screen-reader gap).** `sheet.tsx:68` (`"Close"`), `breadcrumb.tsx:88` (`"More"`), `CategorySection.tsx:67,76` (`aria-label="Previous"/"Next"`). Route through next-intl.

### 🟢 LOW
12. Breadcrumb "home" crumb uses the EN path on FR static pages (`about-us/page.tsx:63` passes `path: "/"`); the article route does it correctly (`[category]/[slug]/page.tsx:136`).
13. `BlogPosting.author` is always an Organization (`structured-data.ts:231`) despite real `BLOG_AUTHORS` — `Person` would strengthen E-E-A-T.
14. Article-route hreflang is hand-rolled with hardcoded `["en","fr"]` (`[category]/[slug]/page.tsx:49-54`) instead of `routing.locales`.
15. Manifest omits the existing `android-chrome-192/512` PNGs (`src/app/manifest.ts`); `public/icon.svg` not referenced in `metadata.icons` (no `app/icon.tsx` convention file).
16. `noai`/`noimageai` (`robots.ts:9-10`) are non-standard directives (harmless; the `X-Robots-Tag` on `/_next/image` in `next.config.ts:49-52` is the reliable signal).

---

## i18n detail (next-intl)

| Dimension | Result |
|---|---|
| Message parity EN/FR | ✅ 661/661 keys, zero missing, zero empty. Identical EN==FR values are legitimate (proper nouns, contact data, trade-show names, brand tokens) — except demo links (#4). |
| Routing & middleware | ✅ `routing.ts`/`request.ts`/`navigation.ts` textbook. Middleware correctly in `src/proxy.ts` (Next 16 rename). Matcher excludes dotted paths, so `/feed.xml` & `/fr/feed.xml` bypass locale rewrite. Pathnames ↔ folders match exactly. |
| `src/app/fr/` anomaly | ✅ **Not a conflict** — contains only `fr/feed.xml/route.ts`, the FR RSS feed (EN at `/feed.xml`, FR at `/fr/feed.xml`). Deliberate per `src/lib/blog/rss.ts:14-22`. Route Handlers, not pages; no collision with `[locale]`. Worth a one-line code comment so it isn't mistaken for a conflict later. |
| Locale-aware navigation | ✅ Zero violations — no raw `next/link`, no raw internal `<a>`. 44 files use `@/i18n/navigation`. `LanguageSelect.tsx:46` switches locale via the intl router (correct for `as-needed` + localized pathnames). |
| Static rendering | ✅ `generateStaticParams` + `setRequestLocale` on every page; `getTranslations` always called with explicit `{ locale }`; blog route `dynamicParams = false`; no `cookies()/headers()/force-dynamic` leaks. |
| Demo leftovers | ❌ `/pathnames` route (#3), `PageLayout` demo links (#4), dead `redirect("/en")` (#6). |
| Hardcoded strings | ⚠️ Only 4, all a11y-only (#11). |

## SEO detail

| Dimension | Result |
|---|---|
| Per-page metadata | ⚠️ All routes have `generateMetadata` with unique title/description, **but** home EN is placeholder (#1) and lengths overflow (#9). |
| Canonical & hreflang | ✅ Relative canonical + hreflang resolved via `metadataBase` (`layout.tsx:60`); x-default → default locale. ⚠️ localhost fallback risk (#2). |
| Sitemap | ✅ Static routes + blog index + all non-draft articles with paired hreflang + x-default. ❌ includes `/pathnames` (#3); no category landing pages (#8). |
| Robots | ✅ Sitemap referenced; only `/api/` disallowed; sane `index,follow`. ⚠️ non-standard `noai` (#16). |
| Structured data | ✅ Good coverage (Organization, WebSite, WebPage, AboutPage, ContactPage, ImageGallery, Blog, BlogPosting, BreadcrumbList). ❌ no LocalBusiness (#5); dead Event/Article builders + no FAQPage (#7); FR breadcrumb path (#12); author as Organization (#13). |
| OpenGraph / Twitter / images | ✅ Global `opengraph-image.tsx` (1200×630, per-locale), `summary_large_image`, per-article OG image + `article` type. ⚠️ static pages share the one fallback card; icons slightly off-convention (#15). |
| Route conventions | ⚠️ Missing `loading.tsx` everywhere; content segments lack own error/not-found (inherited) (#10). |
| next/image & perf | ✅ avif/webp, deviceSizes/imageSizes, qualities, 1yr TTL; local images (no remotePatterns needed); heroes use `priority` + `sizes`. Spot-check that no below-fold image sets `priority`. |
| feed.xml & manifest | ✅ Valid per-locale RSS 2.0 with atom:self; advertised via `alternates.types`. The `feed.xml` vs `fr/feed.xml` split is the **correct** pattern for `as-needed`. Manifest valid (⚠️ PNG icons omitted, #15). |

---

## Runtime verification (d3k) — not completed in this environment

d3k was launched in `servers-only` mode (`bun run dev` on `127.0.0.1:3000`). Next.js booted ("Ready in 158ms"), but d3k's reachability probe and `curl` to `127.0.0.1` were both **blocked by the sandbox network policy**, so d3k could not confirm the server and the runtime crawl could not run here. This is the documented sandbox limitation in the d3k skill.

**Recommended follow-up (run locally, outside the sandbox):**
```bash
d3k                                   # launch app + monitored browser
d3k crawl --depth all                 # discover & exercise all URLs
d3k errors --context                  # surface runtime/console errors
```
Then visually confirm rendered `<title>`, `<meta name="description">`, `<link rel="alternate" hreflang>`, canonical, and JSON-LD on `/`, `/fr`, `/blog`, an article, and a 404 — and validate the JSON-LD in Google's Rich Results Test.

---

## Suggested fix order (quick wins first)

1. Replace home EN `IndexPage.seo` placeholder (#1) — 1 file, highest impact.
2. Delete `/pathnames` route + entries + namespace (#3) and dead `redirect("/en")` (#6).
3. Strip/rebrand `PageLayout` demo links (#4).
4. Add phone to `site.contact` + emit `LocalBusiness` JSON-LD with Bangkok geo (#5).
5. Guard `site.url` for all non-prod builds / set `NEXT_PUBLIC_SITE_URL` in CI (#2).
6. Tighten over-length titles/descriptions (#9); wire `Event`/`FAQPage` schema (#7).
7. Add `loading.tsx` per project rule (#10); translate the 4 a11y strings (#11).
