Files
sass-mohfarawati/docs/SEO.md
T
moh dc21c33867 ADDED - Admin SEO page, robots/sitemap hardening and media/maintenance security fixes
SEO
- New Settings > SEO admin page (seo_settings in app_config): indexing switch,
  Google/Bing verification, X handle, JSON-LD identity (Person/Organization,
  sameAs), per-locale keywords, readiness checklist and open links for
  sitemap.xml / robots.txt / manifest.
- robots.txt is now dynamic: disallows admin, api, success and coming-soon
  paths; blocks everything while indexing is off or maintenance is on.
- sitemap.xml carries hreflang alternates per URL, lists only categories with
  published projects, and is empty while hidden.
- Metadata: robots + verification meta, og:locale in de_DE/en_US/ar_AR form,
  alternateLocale, twitter site/creator, project cover as OG image with
  article type, noindex on /success and /coming-soon.
- JSON-LD: WebSite + publisher graph on all public pages, CreativeWork per
  project (view-mode independent).

Security
- Maintenance bypass now requires a correctly signed admin cookie; the
  middleware previously only checked the cookie existed. Token helpers moved
  to lib/admin-session-token.ts (shared by proxy.ts and lib/admin-auth.ts).
- Media uploads: magic-byte validation against the declared type, SVG
  sanitization (script/handlers/foreignObject/javascript: rejected), upload
  folder sanitized, kind inferred from the real file.
- Media route: fixed prefix-based path check that accepted sibling
  directories, unknown extensions return 404, nosniff header, CSP sandbox on
  SVG, gif content type added.
- External media URLs: protocol-relative (//host) URLs rejected.

Portfolio
- Project and category slugs share /portfolio/[slug]; saving now rejects a
  slug already used on the other side instead of silently shadowing it.

Tooling/docs
- Lint: ignore scripts/legacy-prisma-seed.cjs, drop unused import.
- New docs/SEO.md; FEATURES, ARCHITECTURE (Drizzle instead of Prisma), admin
  spec and CLAUDE.md updated.
- Tests for all of the above (unit + integration); suite green.
2026-09-20 21:36:16 +02:00

4.3 KiB
Raw Blame History

SEO

How search visibility works in this project and where each piece is controlled. Everything is data-driven from the admin; no code change is needed to adjust titles, descriptions, indexing, verification, or structured data.

Admin: Settings → SEO (/site-settings/seo)

Canonical page: app/_admin/site-settings/seo/page.tsx (mirrored under app/admin-internal/ and app/root/). Form: components/admin/seo-settings-form.tsx. Action: saveSeoSettingsAction in app/_admin/site-settings/actions.ts.

Stored as one JSON blob in app_config under key seo_settings (lib/seo-settings.ts parses/normalizes; lib/app-config.ts exposes getSeoSettings / updateSeoSettings).

Field Effect
Indexierung erlauben Off → noindex,nofollow meta on every page, robots.txt disallows /, sitemap.xml becomes empty. Maintenance mode forces the same automatically.
Google / Bing Verification <meta name="google-site-verification"> and <meta name="msvalidate.01"> on all pages. Tokens are restricted to [A-Za-z0-9_-].
X / Twitter Handle twitter:site + twitter:creator.
Strukturierte Daten Type (Person / Organization), name, job title/slogan, sameAs profile URLs → JSON-LD publisher on every public page.
Keywords (per locale) <meta name="keywords"> per language.

The page also shows a checklist (lib/seo-report.ts) — indexing state, public URL, meta description length per locale, OG image, favicon, verification, structured data, published projects, sitemap URL count — and open buttons for /sitemap.xml, /robots.txt, /manifest.webmanifest.

Titles, descriptions and the title template per locale live under Settings → Localization; logos, favicon and the default OG image under Settings → Brand.

Generated files

  • app/robots.ts/robots.txt. Indexable: allow /, disallow admin (/admin-internal, /root), /api/, /success, /coming-soon (+ locale variants), plus the sitemap URL. Not indexable (setting off or maintenance): disallow everything.
  • app/sitemap.ts/sitemap.xml. One entry per locale for home, about, portfolio, contact, every category that has published projects, and every published project — each with xhtml:link hreflang alternates and x-default. Empty while maintenance mode is on or indexing is disabled.
  • app/manifest.ts/manifest.webmanifest (icons from Brand settings).

Per-page metadata (lib/metadata.ts)

  • buildAppMetadata() — root layout: metadataBase, robots, verification, keywords, icons, manifest, OG (og:locale as de_DE/en_US/ar_AR + alternateLocale), Twitter.
  • buildLocalizedMetadata({...}) — every public page: templated title, description (≤300 chars), canonical + hreflang alternates, robots, OG, Twitter. Options: image (page-specific share image), noIndex, type: "article", publishedTime.
  • Portfolio project pages pass the project cover as OG image and article type. This is independent of the project's view mode (GRID / STORY / CASE_STUDY), so new view modes inherit full SEO automatically.
  • /success and /coming-soon are noindex.

Structured data (JSON-LD)

Rendered via components/seo/json-ld.tsx (server component; < is escaped).

  • Site layout: WebSite + publisher (Person or Organization) graph linked by @id (buildSiteJsonLd).
  • Project page: CreativeWork with url, headline, description, image, genre (category), keywords (service label, year), datePublished, author @id, client as sourceOrganization (buildProjectJsonLd).

Slugs

Categories and projects share /portfolio/[slug]; categories win at resolve time. The admin therefore rejects a project slug that equals an existing category slug and vice versa (app/_admin/portfolio/actions.ts).

Operational checklist before launch

  1. NEXT_PUBLIC_SITE_URL must be the public https:// origin (canonical base).
  2. Settings → Localization: site name + 50160 char description in DE/EN/AR.
  3. Settings → Brand: default OG image (1200×630) + favicon.
  4. Settings → SEO: indexing on, verification codes, Person/Organization data.
  5. Maintenance mode off. Verify /robots.txt and /sitemap.xml from the SEO page.
  6. Submit the sitemap in Google Search Console / Bing Webmaster.