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.
141 lines
4.4 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|