Files
marketplaces/src/app/features/content-management/models/content-page.model.ts
sdarbinyan 4861990551 feat(static-pages): extend data model with route/enabled/status/media/SEO
Milestone 1 of the Static Pages Module sprint.

- StaticPageConfig / ContentPage / ContentPageBootstrapInput gain: explicit
  editable route (defaults from slug, independently overridable), enabled
  (master on/off), status: 'draft'|'published' (per-page publish lifecycle,
  independent of the whole-bootstrap draft/publish cycle), customTemplate,
  heroImage/thumbnail/gallery, and seo.robots.
- ContentPageService: normalizePage/normalizePages default missing
  enabled/status to enabled+published so existing bootstrap data never gets
  silently un-published; only the editor's createPage() opts a brand-new page
  into 'draft'. Legacy array-format pages get the same treatment.
- resolvePage now returns null (storefront 404) for a disabled or draft page,
  regardless of whether the surrounding bootstrap itself is published -
  affects the storefront static-page route AND the auto-generated footer nav
  group (both go through this same resolver), which is the correct behavior.
- validatePages extended: duplicateRoutes (route can now diverge from slug),
  invalidHtml, invalidSeo (canonical/ogImage URL shape, known robots tokens).
- New schema/validators/primitives.validateHtml: stack-based tag-balance
  check (void/self-closing elements skipped, comments stripped). Caught and
  fixed a real bug during its own spec run: the initial implementation popped
  the stack back to the nearest matching ancestor on a mismatched closing
  tag, which silently swallowed a genuinely unclosed inner tag instead of
  flagging it - now a closing tag must match the top of the stack exactly.
- toBootstrapRecord serializes the new fields; visible mirrors enabled so any
  reader of the older field name stays truthful.
- Specs: content-page.service.spec.ts (new), primitives.spec.ts (validateHtml).

Gate: tsc --noEmit, npm test (57/57), arch:check, build all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:43:00 +04:00

79 lines
2.3 KiB
TypeScript

import { LocalizedHtmlContent, LocalizedTextContent } from '../../../shared/models/config';
export interface ContentPageSeoConfig {
title?: string;
description?: string;
keywords?: string;
canonical?: string;
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
robots?: string;
}
export interface ContentPageTranslation {
title?: string;
html?: string;
seo?: ContentPageSeoConfig;
}
export type ContentPageStatus = 'draft' | 'published';
export interface ContentPage {
id: string;
slug: string;
/** Editable independently of `slug`; defaults from it but may diverge (e.g. legacy redirects). */
route: string;
title: string;
icon?: string;
order: number;
showInFooter: boolean;
showInHeader: boolean;
showInSitemap: boolean;
visibility: {
desktop?: boolean;
tablet?: boolean;
mobile?: boolean;
};
requiresAuthentication: boolean;
footerGroup?: string;
translations: Record<string, ContentPageTranslation>;
seo?: ContentPageSeoConfig;
/** Master on/off switch; a disabled page never resolves on the storefront. */
enabled: boolean;
/** Per-page publish lifecycle, independent of the whole-bootstrap draft/publish cycle. */
status: ContentPageStatus;
customTemplate?: string;
heroImage?: string;
thumbnail?: string;
gallery?: string[];
}
export interface ContentPageBootstrapInput {
id: string;
slug: string;
route?: string;
title?: string | LocalizedTextContent;
showInFooter?: boolean;
showInHeader?: boolean;
showInSitemap?: boolean;
icon?: string;
order?: number;
visibility?: {
desktop?: boolean;
tablet?: boolean;
mobile?: boolean;
};
requiresAuthentication?: boolean;
footerGroup?: string;
translations?: Record<string, ContentPageTranslation>;
html?: string | LocalizedHtmlContent;
seo?: ContentPageSeoConfig | { title?: string | LocalizedTextContent; description?: string | LocalizedTextContent; keywords?: string; canonical?: string; ogTitle?: string | LocalizedTextContent; ogDescription?: string | LocalizedTextContent; ogImage?: string; robots?: string; metaTags?: Array<{ name?: string; property?: string; content: string }>; };
enabled?: boolean;
status?: ContentPageStatus;
customTemplate?: string;
heroImage?: string;
thumbnail?: string;
gallery?: string[];
}