Files
sass-mohfarawati/tests/metadata.test.ts
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

141 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildDefaultSiteSettings } from "../lib/site-settings";
import {
applyTitleTemplateFn,
buildAppMetadataFromConfig,
buildLocaleAlternates,
buildLocalizedMetadataFromConfig,
} from "../lib/metadata";
describe("metadata helpers", () => {
it("applies the configured title template", () => {
expect(applyTitleTemplateFn("About", "{pageTitle} | {siteName}", "Studio Moh")).toBe(
"About | Studio Moh",
);
expect(applyTitleTemplateFn("About", "Studio Moh", "Studio Moh")).toBe("About | Studio Moh");
});
it("builds root metadata with dynamic icons and social preview", () => {
const settings = buildDefaultSiteSettings("Studio Moh");
settings.defaultLocale = "ar";
settings.locales.ar.siteName = "Studio Moh AR";
const metadata = buildAppMetadataFromConfig(settings, {
siteLogoLight: null,
siteLogoDark: null,
favicon: {
assetId: "fav",
url: "/uploads/media/site-settings/favicon.svg",
version: "v1",
},
defaultOgImage: {
assetId: "og",
url: "/uploads/media/site-settings/default-og.png",
version: "v1",
},
});
expect(metadata.title).toBe("Studio Moh AR");
expect(metadata.icons).toEqual({
icon: [{ url: "/favicon.ico?v=v1" }],
shortcut: [{ url: "/favicon.ico?v=v1" }],
apple: [{ url: "/apple-icon.png?v=v1" }],
});
expect(metadata.openGraph).toMatchObject({
locale: "ar_AR",
url: "https://mohfarawati.de/",
});
expect(metadata.twitter).toMatchObject({
card: "summary_large_image",
});
});
it("falls back to localized site description and omits icons when unset", () => {
const settings = buildDefaultSiteSettings("Studio Moh");
settings.locales.en.siteDescription = "English fallback description";
settings.locales.en.titleTemplate = "{pageTitle} | {siteName} | Freelancer";
const metadata = buildLocalizedMetadataFromConfig({
settings,
bindings: {
siteLogoLight: null,
siteLogoDark: null,
favicon: null,
defaultOgImage: null,
},
locale: "en",
pathname: "/about",
title: "About",
});
expect(metadata.title).toBe("About | Studio Moh | Freelancer");
expect(metadata.description).toBe("English fallback description");
expect(metadata.openGraph?.siteName).toBe("Studio Moh");
expect(metadata.twitter).toMatchObject({
card: "summary",
});
});
it("can skip the title template for the homepage", () => {
const settings = buildDefaultSiteSettings("Studio Moh");
settings.locales.ar.siteName = "اسم الموقع";
settings.locales.ar.titleTemplate = "{pageTitle} | {siteName}";
const metadata = buildLocalizedMetadataFromConfig({
settings,
bindings: {
siteLogoLight: null,
siteLogoDark: null,
favicon: null,
defaultOgImage: null,
},
locale: "ar",
pathname: "/",
title: "اسم الموقع",
description: "وصف",
applyTitleTemplate: false,
});
expect(metadata.title).toBe("اسم الموقع");
});
it("builds alternates and canonical from the runtime default locale", () => {
const alternates = buildLocaleAlternates("/about", "ar");
expect(alternates.canonical).toBe("https://mohfarawati.de/about");
expect(alternates.languages.ar).toBe("https://mohfarawati.de/about");
expect(alternates.languages.de).toBe("https://mohfarawati.de/de/about");
expect(alternates.languages["x-default"]).toBe("https://mohfarawati.de/about");
});
it("builds localized metadata urls against the configured default locale", () => {
const settings = buildDefaultSiteSettings("Studio Moh");
settings.defaultLocale = "ar";
const metadata = buildLocalizedMetadataFromConfig({
settings,
bindings: {
siteLogoLight: null,
siteLogoDark: null,
favicon: null,
defaultOgImage: null,
},
locale: "de",
pathname: "/about",
title: "About",
});
expect(metadata.alternates).toMatchObject({
canonical: "https://mohfarawati.de/about",
languages: {
ar: "https://mohfarawati.de/about",
de: "https://mohfarawati.de/de/about",
en: "https://mohfarawati.de/en/about",
"x-default": "https://mohfarawati.de/about",
},
});
expect(metadata.openGraph).toMatchObject({
url: "https://mohfarawati.de/de/about",
});
});
});