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.
4.3 KiB
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 withxhtml:link hreflangalternates andx-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:localeasde_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
articletype. This is independent of the project's view mode (GRID/STORY/CASE_STUDY), so new view modes inherit full SEO automatically. /successand/coming-soonarenoindex.
Structured data (JSON-LD)
Rendered via components/seo/json-ld.tsx (server component; < is escaped).
- Site layout:
WebSite+ publisher (PersonorOrganization) graph linked by@id(buildSiteJsonLd). - Project page:
CreativeWorkwith url, headline, description, image, genre (category), keywords (service label, year),datePublished, author@id, client assourceOrganization(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
NEXT_PUBLIC_SITE_URLmust be the publichttps://origin (canonical base).- Settings → Localization: site name + 50–160 char description in DE/EN/AR.
- Settings → Brand: default OG image (1200×630) + favicon.
- Settings → SEO: indexing on, verification codes, Person/Organization data.
- Maintenance mode off. Verify
/robots.txtand/sitemap.xmlfrom the SEO page. - Submit the sitemap in Google Search Console / Bing Webmaster.