Files
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

84 lines
4.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.