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.
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
# 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 + 50–160 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.
|
||||
Reference in New Issue
Block a user