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.
- Wrap #smooth-wrapper in an .app-shell: a fixed, inset, rounded, bordered
panel. Its transform makes it the containing block for the fixed wrapper, so
ScrollSmoother sizes the scroll viewport to the shell (content scrolls inside
it) without fighting ScrollSmoother's inline styles.
- Dock and corner controls stay outside the shell, on a brand-tinted 'desktop'
wallpaper painted on body.
- Reduced-motion path: the shell scrolls its content natively.
A template.tsx is re-created on every navigation, resetting AnimatePresence
so neither enter nor exit ran. Move the transition into a PageTransition
client component rendered in the persistent (site) layout, so AnimatePresence
survives navigations and the old page animates out before the new one in.
- Dock icons are now real SVG files under public/dock/ (macOS squircle
look, active variants), so they can be replaced with custom art later.
- Full-bleed icons (DockIcon padding 0), fixed logo src to stop the
refresh flash.
- Remove the top menu bar entirely; relocate theme/language/sound (+admin)
into a corner control dock that expands on click.
Swap the pill navigation for a macOS-style dock as the primary nav and a
slim top menu bar for utilities:
- components/ui/dock.tsx: Magic UI Dock/DockIcon (magnification via
useSpring/useTransform), ported to the project's framer-motion.
- components/layout/site-dock.tsx: bottom dock with the logo as the first
icon, each item a real next/link (SEO preserved, active state from the
pathname), monochrome squircle tiles, hover tooltips and a "soon" badge
for Products; top menu bar keeps locale/sound/theme/admin + a live clock.
- (site)/layout.tsx: render SiteDock instead of SiteHeader.
Home bento grid and all page content are unchanged.