Skip to content

Add proper language tags, sitemap, robots.txt to the site#45

Merged
Marv51 merged 7 commits into
mainfrom
claude/portfolio-bilingual-seo-97047e
Jul 12, 2026
Merged

Add proper language tags, sitemap, robots.txt to the site#45
Marv51 merged 7 commits into
mainfrom
claude/portfolio-bilingual-seo-97047e

Conversation

@Marv51

@Marv51 Marv51 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

What & why

Fixes the bilingual SEO plumbing: the EN (/) and DE (/de) trees didn't reference each other, so Google treated them as unrelated pages. Adds reciprocal hreflang + canonical links, a generated sitemap, and a robots.txt.

Changes

  • app/seo-links.ts — single source of truth for SEO URLs:
    • SITE_URL and a PAGES map of EN↔DE equivalents (//de, /en/imprint/de/impressum).
    • bilingualLinks(page, lang) returns rel="canonical" + rel="alternate" hreflang links for en, de, and x-default (defaults to the English URL).
    • renderSitemap() / sitemapPaths() derive the sitemap and its URL list from the same PAGES map, so canonical, hreflang, and the sitemap can't drift apart.
  • Wired into the four indexable routes via Route.LinksFunction (home, home-de, imprint, imprint-de), each passing its PAGES entry.
  • The home routes' Open Graph / Twitter og:url and og:image now derive from SITE_URL + PAGES.home instead of hardcoding https://ruehe.me, so og:url stays in sync with the canonical URL.
  • app/routes/sitemap.tsx — a resource route (loader only) that returns renderSitemap() as application/xml. It prerenders to build/client/sitemap.xml and is also served live under npm run dev. Registered as route("sitemap.xml", ...) in routes.ts.
  • react-router.config.tsprerender uses the function form to cross-check route discovery: getStaticPaths() is compared against PAGES + an exclude list (/404, /sitemap.xml, and the two standalone app privacy policies), and throws to fail the build if any prerendered page is unaccounted for — so the sitemap can't silently fall out of sync with the routes.
  • public/robots.txt — allow-all + Sitemap: pointer. The previously hand-written public/sitemap.xml was removed.
  • Removed the bogus { lang: "en" } / { lang: "de" } entries from the meta() arrays in layout.tsx, layout-de.tsx, and both imprint routes — lang isn't a valid meta descriptor; <html lang> is set in root.tsx.

Verification

  • npm run typecheck and npm run build pass.
  • The prerendered build/client/sitemap.xml has the 4 URLs, each with en/de/x-default alternates.
  • npm run dev serves /sitemap.xml200, Content-Type: application/xml, 4 URLs.
  • Confirmed the drift check fails the build (exit 1) when a prerendered route is added without updating PAGES/exclude list.
  • Prerendered HTML for / and /en/imprint emits the correct canonical + hreflang links; on the home pages og:url matches canonical and og:image is unchanged, both now derived from SITE_URL.

Reviewer notes

  • React Router renders the attribute as hrefLang (camelCase) in the static HTML. HTML attribute names are case-insensitive, so crawlers (including Googlebot) parse it identically to hreflang.
  • The resource route emits a sitemap.xml.data sidecar and a tiny client chunk alongside the file — accepted in exchange for dev serving. Nothing links to the sidecar and crawlers never request it.
  • The drift check is fatal: adding a route without updating PAGES or the exclude list will fail the build (and the deploy) until it's accounted for.
  • SITE_URL is a plain constant; it could read import.meta.env.VITE_SITE_URL if a staging domain is ever needed. public/robots.txt and public/CNAME still spell the domain out literally because they're static files, not TS modules.

Marv51 and others added 7 commits July 11, 2026 20:55
Serif "MR" monogram on the accent-purple background, generated at
SVG plus 16/32px PNG, apple-touch (180px), and legacy .ico. Wired
into every page via the root links export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derive the sitemap from the route config rather than a hand-written file: a
`buildEnd` hook writes build/client/sitemap.xml from a single PAGES map in
seo-links.ts, and the `prerender` hook cross-checks getStaticPaths() so a
prerendered page that's missing from both the sitemap and the exclude list
warns at build time.

Centralize SITE_URL and the EN<->DE page pairing in seo-links.ts so canonical,
hreflang, and the sitemap can't drift apart; the route link functions now take
a PAGES entry. Remove the now-redundant public/sitemap.xml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move sitemap generation from the buildEnd hook into a resource route
(app/routes/sitemap.tsx) whose loader returns renderSitemap() as
application/xml. It still prerenders to build/client/sitemap.xml and now also
serves live under `npm run dev`, at the cost of a sitemap.xml.data sidecar and
a tiny client chunk.

The prerender hook keeps the getStaticPaths() cross-check but now throws
instead of warning, so a prerendered page missing from both PAGES and the
exclude list fails the build. /sitemap.xml is added to the exclude list since
the resource route makes it a discovered path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Open Graph/Twitter meta in the home routes hardcoded https://ruehe.me,
duplicating the domain that canonical/hreflang already derive from SITE_URL.
og:url in particular must match the canonical URL; build both from SITE_URL +
PAGES.home so a domain change can't leave them out of sync. Output is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Marv51
Marv51 merged commit f9c3b52 into main Jul 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant