feat: implement website core pages IA
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m10s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m10s
This commit is contained in:
parent
f884b16061
commit
27d520b4aa
5
.github/agents/copilot-instructions.md
vendored
5
.github/agents/copilot-instructions.md
vendored
@ -212,6 +212,8 @@ ## Active Technologies
|
||||
- Static filesystem content, styles, assets, and content collections under `apps/website/src` and `apps/website/public`; no database (214-website-visual-foundation)
|
||||
- Markdown governance artifacts, JSON Schema plus logical OpenAPI planning contracts, and Bash-backed SpecKit scripts inside a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `docs/ui/operator-ux-surface-standards.md`, and Specs 196 through 200 (201-enforcement-review-guardrails)
|
||||
- Repository-owned markdown and contract artifacts under `.specify/` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/201-enforcement-review-guardrails/`; no product database persistence (201-enforcement-review-guardrails)
|
||||
- Astro 6.0.0 templates + TypeScript 5.9 stric + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro layout/primitive/content helpers, Playwright smoke tests (215-website-core-pages)
|
||||
- Static filesystem pages, content modules, and Astro content collections under `apps/website/src` and `apps/website/public`; no database (215-website-core-pages)
|
||||
|
||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||
|
||||
@ -246,9 +248,8 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 215-website-core-pages: Added Astro 6.0.0 templates + TypeScript 5.9 stric + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro layout/primitive/content helpers, Playwright smoke tests
|
||||
- 214-website-visual-foundation: Added Astro 6.0.0 templates + TypeScript 5.9 strict + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro component primitives, Playwright browser smoke tests
|
||||
- 201-enforcement-review-guardrails: Added Markdown governance artifacts, JSON Schema plus logical OpenAPI planning contracts, and Bash-backed SpecKit scripts inside a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `docs/ui/operator-ux-surface-standards.md`, and Specs 196 through 200
|
||||
- 213-website-foundation-v0: Added Astro 6.0.0 templates + TypeScript 5.x (explicit setup in `apps/website`) + Astro 6, Tailwind CSS v4, custom Astro component primitives (shadcn-inspired), lightweight Playwright browser smoke tests
|
||||
- 200-filament-surface-rules: Added Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001`
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { footerNavigationGroups, getFooterLead, siteMetadata } from '@/lib/site';
|
||||
import { getFooterLead, getFooterNavigationGroups, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
@ -10,6 +10,7 @@ interface Props {
|
||||
const { currentPath: _currentPath } = Astro.props;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const footerLead = getFooterLead(_currentPath);
|
||||
const footerNavigationGroups = await getFooterNavigationGroups();
|
||||
---
|
||||
|
||||
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={footerLead.intent}>
|
||||
@ -28,7 +29,7 @@ const footerLead = getFooterLead(_currentPath);
|
||||
<PrimaryCTA cta={footerLead.primaryCta} size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-3">
|
||||
<div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{
|
||||
footerNavigationGroups.map((group) => (
|
||||
<div>
|
||||
@ -51,7 +52,7 @@ const footerLead = getFooterLead(_currentPath);
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 py-6 text-sm text-[var(--color-copy)] sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="m-0">© {currentYear} {siteMetadata.siteName}. Public product site v0 foundation.</p>
|
||||
<p class="m-0">© {currentYear} {siteMetadata.siteName}. Core public route foundation.</p>
|
||||
<p class="m-0">
|
||||
Built as a static Astro track with no platform auth, session, or API coupling.
|
||||
</p>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { getHeaderCta, isActiveNavigationPath, primaryNavigation, siteMetadata } from '@/lib/site';
|
||||
import { getHeaderCta, getPrimaryNavigation, isActiveNavigationPath, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
@ -9,6 +9,7 @@ interface Props {
|
||||
|
||||
const { currentPath } = Astro.props;
|
||||
const headerCta = getHeaderCta(currentPath);
|
||||
const primaryNavigation = await getPrimaryNavigation();
|
||||
---
|
||||
|
||||
<header class="sticky top-0 z-30 px-[var(--space-page-x)] pt-4 sm:pt-6">
|
||||
|
||||
@ -29,9 +29,13 @@ const pageDefinition = getPageDefinition(currentPath);
|
||||
>
|
||||
<div
|
||||
class="foundation-page site-shell"
|
||||
data-canonical-path={pageDefinition.canonicalPath}
|
||||
data-page-family={pageDefinition.family}
|
||||
data-page-priority={pageDefinition.priority}
|
||||
data-page-role={pageDefinition.pageRole}
|
||||
data-shell-tone={pageDefinition.shellTone}
|
||||
data-surface-group={pageDefinition.surfaceGroup}
|
||||
data-journey-stage={pageDefinition.journeyStage}
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.68),transparent_28%),radial-gradient(circle_at_top_right,rgba(47,111,183,0.14),transparent_26%)]"
|
||||
|
||||
@ -1,24 +1,36 @@
|
||||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const futureContentSchema = z.object({
|
||||
const baseContentSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
publishedAt: z.coerce.date().optional(),
|
||||
});
|
||||
|
||||
const changelogSchema = baseContentSchema.extend({
|
||||
draft: z.boolean().default(false),
|
||||
publishedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const optionalContentSchema = baseContentSchema.extend({
|
||||
draft: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const editorialInventorySchema = baseContentSchema.extend({
|
||||
draft: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
articles: defineCollection({
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/articles' }),
|
||||
schema: futureContentSchema,
|
||||
schema: editorialInventorySchema,
|
||||
}),
|
||||
changelog: defineCollection({
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/changelog' }),
|
||||
schema: futureContentSchema,
|
||||
schema: changelogSchema,
|
||||
}),
|
||||
resources: defineCollection({
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/resources' }),
|
||||
schema: futureContentSchema,
|
||||
schema: optionalContentSchema,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Initial core pages and IA realignment
|
||||
description: Published the canonical Product, Trust, Changelog, Contact, Privacy, and Imprint route model, added the new Trust and Changelog surfaces, and demoted legacy supporting routes out of primary navigation.
|
||||
publishedAt: 2026-04-19
|
||||
draft: false
|
||||
---
|
||||
|
||||
This update establishes the first deliberate public route set for the website.
|
||||
|
||||
- Product, Trust, Changelog, and Contact now define the core buyer journey.
|
||||
- Privacy and Imprint remain part of the canonical legal baseline.
|
||||
- Solutions, Integrations, Legal, and Terms stay published as retained secondary surfaces instead of top-level core routes.
|
||||
- The legacy `/security-trust` path now resolves to the canonical `/trust` surface.
|
||||
29
apps/website/src/content/pages/changelog.ts
Normal file
29
apps/website/src/content/pages/changelog.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { HeroContent, PageSeo } from '@/types/site';
|
||||
|
||||
export const changelogSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Changelog',
|
||||
description:
|
||||
'TenantAtlas uses a dedicated changelog to show dated public progress without pretending a broader editorial or resources program is already live.',
|
||||
path: '/changelog',
|
||||
};
|
||||
|
||||
export const changelogHero: HeroContent = {
|
||||
eyebrow: 'Visible product progress',
|
||||
title: 'Visible product progress through dated updates, not placeholder routes.',
|
||||
description:
|
||||
'The changelog gives returning visitors one explicit place to verify progress. It should be concrete, dated, and connected to the product and contact path instead of acting like a placeholder blog.',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Start the working session',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/product',
|
||||
label: 'See the product model',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Dated updates beat vague progress claims.',
|
||||
'Resources remain hidden until substantive content exists.',
|
||||
'The changelog supports the product read instead of replacing it.',
|
||||
],
|
||||
};
|
||||
@ -3,13 +3,13 @@ import type { HeroContent, LegalSection, PageSeo } from '@/types/site';
|
||||
export const contactSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Contact',
|
||||
description:
|
||||
'TenantAtlas uses a qualified working-session path instead of a generic demo pitch so serious buyers can frame the right conversation early.',
|
||||
'TenantAtlas uses one clear contact path for serious product, trust, and rollout conversations instead of splitting first contact across vague demo flows.',
|
||||
path: '/contact',
|
||||
};
|
||||
|
||||
export const contactHero: HeroContent = {
|
||||
eyebrow: 'Contact / Demo',
|
||||
title: 'Start a qualified working session instead of a generic demo request.',
|
||||
eyebrow: 'Primary conversion route',
|
||||
title: 'Start the contact path with context instead of a generic demo request.',
|
||||
description:
|
||||
'The contact path should help serious buyers explain who they are, what governance questions they are trying to solve, and what kind of follow-up would actually be useful.',
|
||||
primaryCta: {
|
||||
@ -18,14 +18,14 @@ export const contactHero: HeroContent = {
|
||||
variant: 'primary',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/legal',
|
||||
label: 'Read the legal surface',
|
||||
href: '/trust',
|
||||
label: 'Review the trust posture',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Use the page to qualify the conversation, not to force a form funnel.',
|
||||
'Set expectations for what the session covers and what happens next.',
|
||||
'Keep privacy and terms visible before anyone shares evaluation details.',
|
||||
'Keep trust, privacy, terms, and imprint visible before anyone shares evaluation details.',
|
||||
'The route stays obvious from Home, Product, Trust, and Changelog.',
|
||||
],
|
||||
};
|
||||
|
||||
@ -59,7 +59,7 @@ export const contactLegalSections: LegalSection[] = [
|
||||
title: 'Before you reach out',
|
||||
body: [
|
||||
'Use the legal links below before sharing evaluation details so the contact path stays trustworthy and unsurprising.',
|
||||
'The legal hub, privacy page, and public website terms remain reachable from the contact flow and the global footer.',
|
||||
'Trust, privacy, terms, imprint, and the retained legal hub remain reachable from the contact flow and the global footer.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -10,41 +10,41 @@ import type {
|
||||
export const homeSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Governance of record for Microsoft tenant operations',
|
||||
description:
|
||||
'Trust-first public framing for a Microsoft tenant governance product that connects backup, restore, version history, drift, findings, evidence, and reviews.',
|
||||
'TenantAtlas helps teams understand Microsoft tenant change history, restore posture, trust boundaries, and the next evaluation step without a bloated public sitemap.',
|
||||
path: '/',
|
||||
};
|
||||
|
||||
export const homeHero: HeroContent = {
|
||||
eyebrow: 'Public website v0',
|
||||
title: 'TenantAtlas frames Microsoft tenant governance as a calm, reviewable operating model instead of a loud feature wall.',
|
||||
eyebrow: 'Core public route set',
|
||||
title: 'TenantAtlas keeps Microsoft tenant change history, restore posture, and review context inside one operating record.',
|
||||
description:
|
||||
'TenantAtlas gives MSP and enterprise teams one clear operating model for understanding what changed, what drifted, what needs review, and what can be restored without turning governance into a loose collection of disconnected tools.',
|
||||
'TenantAtlas gives MSP and enterprise teams a calmer way to understand what changed, what drifted, what can be restored, and what needs review without turning governance into a loose collection of disconnected screens.',
|
||||
primaryCta: {
|
||||
href: '/product',
|
||||
label: 'See the product model',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/security-trust',
|
||||
href: '/trust',
|
||||
label: 'Review the trust posture',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Static, readable, and separate from app runtime concerns.',
|
||||
'Built for trust conversations before a demo ever starts.',
|
||||
'Structured so landing, trust, and long-form pages still feel like one website.',
|
||||
'Product, trust, and changelog each do one explicit job in the buyer journey.',
|
||||
'The public site stays static, readable, and separate from platform runtime concerns.',
|
||||
'One clear contact path remains visible without pricing, docs, or placeholder routes taking over.',
|
||||
],
|
||||
};
|
||||
|
||||
export const homeMetrics: MetricItem[] = [
|
||||
{
|
||||
value: '1',
|
||||
label: 'Connected model',
|
||||
description: 'Inventory, snapshots, review evidence, and restore posture stay in one narrative.',
|
||||
label: 'Primary next step',
|
||||
description: 'Contact stays singular and obvious while product, trust, and changelog remain nearby.',
|
||||
},
|
||||
{
|
||||
value: '7+',
|
||||
label: 'Core public surfaces',
|
||||
description: 'Visitors can move from explanation to trust and contact without dead ends.',
|
||||
value: '4',
|
||||
label: 'Core buyer questions',
|
||||
description: 'What is it, why it matters, why trust it, and what to do next each map to a named route.',
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
@ -55,68 +55,68 @@ export const homeMetrics: MetricItem[] = [
|
||||
|
||||
export const homePillars: FeatureItemContent[] = [
|
||||
{
|
||||
eyebrow: 'Inventory',
|
||||
title: 'Normalize what the tenant really looks like right now.',
|
||||
eyebrow: 'Product truth',
|
||||
title: 'Show the connected governance model before any audience-specific detours.',
|
||||
description:
|
||||
'Start with the observed state so teams can inspect the current configuration baseline before they talk about restore or enforcement.',
|
||||
meta: 'Last observed truth',
|
||||
'The first pass should explain how inventory, immutable history, restore safety, findings, evidence, and reviews fit together before it asks a buyer to infer the model alone.',
|
||||
meta: 'Product before route sprawl',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Snapshots',
|
||||
title: 'Keep immutable history instead of vague memory.',
|
||||
eyebrow: 'Trust visibility',
|
||||
title: 'Make the credibility surface obvious while public claims stay bounded.',
|
||||
description:
|
||||
'Version history stays queryable by tenant, operator, and moment in time so teams can explain what changed and why.',
|
||||
meta: 'Reproducible versions',
|
||||
'Trust belongs in top-level navigation because tenant isolation, access handling, and operating discipline are first-read questions for this category.',
|
||||
meta: 'Trust is top-level visible',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Drift & findings',
|
||||
title: 'Surface drift, exceptions, and review needs in the same language.',
|
||||
eyebrow: 'Visible progress',
|
||||
title: 'Publish dated progress instead of implying motion through vague copy.',
|
||||
description:
|
||||
'Operational questions move from “what broke?” to “what changed, what matters, and what review is due?”',
|
||||
meta: 'Review-oriented visibility',
|
||||
'A changelog surface gives returning visitors one concrete route for current product movement without forcing a blog or resources hub into the first navigation layer.',
|
||||
meta: 'Real changelog, no placeholder hub',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Restore',
|
||||
title: 'Treat rollback and restore as governed actions, not panic buttons.',
|
||||
eyebrow: 'Clear action path',
|
||||
title: 'Keep contact visible without turning the site into a demo funnel.',
|
||||
description:
|
||||
'Preview, validation, and operator confirmation stay central so risky changes are reversible without becoming casual.',
|
||||
meta: 'Safer execution',
|
||||
'The initial IA leads to one working-session route so serious buyers can move forward without guessing whether contact, demo, or sales are different flows.',
|
||||
meta: 'One clear conversion route',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Evidence',
|
||||
title: 'Connect reviews, findings, and evidence without a second reporting layer.',
|
||||
eyebrow: 'Outcome explanation',
|
||||
title: 'Explain who the product helps without forcing a dedicated Solutions hub into the header.',
|
||||
description:
|
||||
'Teams can show why a configuration is acceptable, where exceptions exist, and how review decisions stay attributable.',
|
||||
meta: 'Audit-ready context',
|
||||
'Home and Product should carry the buyer-outcome explanation well enough that retained supporting pages stay secondary instead of becoming required orientation routes.',
|
||||
meta: 'Outcome clarity without route inflation',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Operations',
|
||||
title: 'Keep velocity without hiding risk.',
|
||||
eyebrow: 'Static-first delivery',
|
||||
title: 'Preserve the website as a static public track with no platform coupling.',
|
||||
description:
|
||||
'The product is built for admins who need speed and auditability at the same time, not for dashboards that only summarize after the fact.',
|
||||
meta: 'Operator-first workflows',
|
||||
'The IA work stays local to the Astro website so trust, product, and legal surfaces remain reviewable without adding auth, API, or admin runtime obligations.',
|
||||
meta: 'Website-only contract',
|
||||
},
|
||||
];
|
||||
|
||||
export const homeProofBlocks: CalloutContent[] = [
|
||||
{
|
||||
eyebrow: 'Positioning',
|
||||
title: 'Governance of record for Microsoft tenant operations.',
|
||||
eyebrow: 'First read',
|
||||
title: 'A serious buyer should understand the product and the route model in one pass.',
|
||||
description:
|
||||
'The site makes the category legible up front: not just backup, not just reporting, and not a second admin portal trying to mirror every Microsoft screen.',
|
||||
'The site needs a stable answer to what the product is, how trust is handled, and how to continue the evaluation before optional surfaces earn any prominence.',
|
||||
tone: 'accent',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Why it matters now',
|
||||
title: 'Microsoft tenant change volume keeps climbing while operator certainty keeps shrinking.',
|
||||
eyebrow: 'Trust boundary',
|
||||
title: 'Trust claims should live on one explicit surface instead of leaking across marketing copy.',
|
||||
description:
|
||||
'When policy history, restore posture, findings, and evidence live in separate conversations, teams lose time exactly when they need clarity.',
|
||||
'Claims about isolation, operating discipline, access handling, or hosting must route back to a dedicated trust page that supports and bounds them clearly.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Public promise',
|
||||
title: 'No inflated compliance or automation claims.',
|
||||
eyebrow: 'No placeholder prestige pages',
|
||||
title: 'Leave docs, pricing, and resources out of the spotlight until they are real.',
|
||||
description:
|
||||
'The public story stays grounded in what the product can honestly support at launch: version truth, safer restore flows, drift visibility, and review support.',
|
||||
'The public story stays more credible when optional hubs remain unpublished and visible progress comes from the changelog instead of speculative route growth.',
|
||||
tone: 'subtle',
|
||||
},
|
||||
];
|
||||
@ -125,21 +125,21 @@ export const homeEcosystem: IntegrationEntry[] = [
|
||||
{
|
||||
category: 'Microsoft',
|
||||
name: 'Microsoft Graph',
|
||||
summary: 'Graph-backed inventory and restore direction without pretending the website depends on live tenant access.',
|
||||
summary: 'Graph-backed inventory and restore direction without implying that the public website depends on live tenant access.',
|
||||
},
|
||||
{
|
||||
category: 'Identity',
|
||||
name: 'Entra ID',
|
||||
summary: 'Identity and access context remain part of the governance narrative where they matter to change control.',
|
||||
summary: 'Identity context matters where change control, tenant access, and review posture intersect.',
|
||||
},
|
||||
{
|
||||
category: 'Endpoint',
|
||||
name: 'Intune',
|
||||
summary: 'Configuration state, backup, and restore posture stay central to the public product story.',
|
||||
summary: 'Configuration state, backup, restore posture, and drift visibility stay central to the public product story.',
|
||||
},
|
||||
{
|
||||
category: 'Review',
|
||||
name: 'Evidence workflows',
|
||||
summary: 'Review packs, exceptions, and evidence stay connected to operational reality instead of becoming detached reporting artifacts.',
|
||||
category: 'Governance',
|
||||
name: 'Review workflows',
|
||||
summary: 'Exceptions, evidence, and reviews stay connected to operational reality instead of becoming detached reporting artifacts.',
|
||||
},
|
||||
];
|
||||
|
||||
50
apps/website/src/content/pages/imprint.ts
Normal file
50
apps/website/src/content/pages/imprint.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { HeroContent, LegalSection, PageSeo } from '@/types/site';
|
||||
|
||||
export const imprintSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Imprint',
|
||||
description:
|
||||
'TenantAtlas uses the Imprint route as the canonical public legal notice and publisher baseline for the website.',
|
||||
path: '/imprint',
|
||||
};
|
||||
|
||||
export const imprintHero: HeroContent = {
|
||||
eyebrow: 'Canonical legal notice',
|
||||
title: 'Imprint and public legal notice baseline for the TenantAtlas website.',
|
||||
description:
|
||||
'This route is the canonical public notice surface for publisher identity and jurisdiction-specific disclosure details. During controlled evaluation, it also makes clear which launch-ready fields still need to be finalized before broader publication.',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Return to contact',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/privacy',
|
||||
label: 'Review privacy',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Imprint is the canonical notice route, not a footer afterthought.',
|
||||
'The legal hub remains secondary while this route carries the notice baseline.',
|
||||
'Controlled-evaluation gaps should stay explicit instead of being hidden.',
|
||||
],
|
||||
};
|
||||
|
||||
export const imprintSections: LegalSection[] = [
|
||||
{
|
||||
title: 'Current publication status',
|
||||
body: [
|
||||
'TenantAtlas is still tightening its launch-ready publisher and jurisdiction details for the broader public website.',
|
||||
'Until those fields are finalized, this route makes the intended legal-notice location explicit so the public IA stays honest about where the canonical notice belongs.',
|
||||
],
|
||||
bullets: [
|
||||
'Publisher identity, registration details, and jurisdiction-specific notices should publish here before broad public launch.',
|
||||
'Privacy, Terms, and the retained Legal route stay linked so the public legal baseline remains coherent.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Questions in the meantime',
|
||||
body: [
|
||||
'If you need current operator or publication details during controlled evaluation, use the contact path and state the trust, legal, or procurement question you need answered.',
|
||||
'The website should update this route before or at the same time as any material change to the published notice baseline.',
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -8,22 +8,22 @@ import type {
|
||||
export const integrationsSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Integrations',
|
||||
description:
|
||||
'TenantAtlas describes the Microsoft-centric ecosystem it fits today without turning the page into a public wishlist.',
|
||||
'TenantAtlas keeps ecosystem-fit detail available as a supporting page without pretending integrations belong in the primary navigation.',
|
||||
path: '/integrations',
|
||||
};
|
||||
|
||||
export const integrationsHero: HeroContent = {
|
||||
eyebrow: 'Ecosystem fit',
|
||||
title: 'Stay clear about the ecosystem fit without turning the page into a wishlist.',
|
||||
eyebrow: 'Retained supporting page',
|
||||
title: 'Keep ecosystem fit detail visible without pretending it is the first thing every buyer needs.',
|
||||
description:
|
||||
'This page should show the real systems TenantAtlas is built around, the workflows it expects to support, and the deliberate boundaries where speculative integration language would create more noise than trust.',
|
||||
'This page shows the real systems TenantAtlas is built around, the workflows it expects to support, and the deliberate boundaries where speculative integration language would create more noise than trust.',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Plan the working session',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/product',
|
||||
label: 'Revisit the product model',
|
||||
label: 'See the product model',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
|
||||
@ -3,42 +3,42 @@ import type { HeroContent, LegalSection, PageSeo } from '@/types/site';
|
||||
export const legalSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Legal',
|
||||
description:
|
||||
'The TenantAtlas legal surface keeps privacy, website terms, and public legal-notice routing accessible before or during buyer conversations.',
|
||||
'The TenantAtlas legal baseline keeps privacy, terms, imprint, and trust routing accessible without promoting the legal hub into the primary navigation.',
|
||||
path: '/legal',
|
||||
};
|
||||
|
||||
export const legalHero: HeroContent = {
|
||||
eyebrow: 'Legal surface',
|
||||
title: 'Legal access should stay one click away from the contact path.',
|
||||
eyebrow: 'Retained legal surface',
|
||||
title: 'Legal access should stay one click away from the trust and contact path.',
|
||||
description:
|
||||
'The legal hub keeps privacy, website terms, and public legal-notice information discoverable from the footer and the conversion flow so visitors do not have to guess where those basics live.',
|
||||
'The legal hub keeps privacy, website terms, imprint details, and trust routing discoverable from the footer and the conversion flow so visitors do not have to guess where those basics live.',
|
||||
primaryCta: {
|
||||
href: '/privacy',
|
||||
label: 'Privacy',
|
||||
href: '/imprint',
|
||||
label: 'Imprint',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/terms',
|
||||
label: 'Terms',
|
||||
href: '/trust',
|
||||
label: 'Trust',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Public legal basics stay reachable before a visitor shares evaluation context.',
|
||||
'The site separates website disclosures from future product-commercial paperwork.',
|
||||
'Jurisdiction-specific notice has a dedicated home in the legal surface.',
|
||||
'Imprint remains the canonical notice surface while this hub stays a secondary index.',
|
||||
],
|
||||
};
|
||||
|
||||
export const legalNoticeSections: LegalSection[] = [
|
||||
{
|
||||
title: 'Public legal notice',
|
||||
title: 'Why this route still exists',
|
||||
body: [
|
||||
'This v0 legal surface reserves the public location for operating-entity, registration, address, and jurisdiction-specific disclosure details that must be published before a broad public launch.',
|
||||
'During controlled evaluation, legal and privacy inquiries can be routed through the public contact path while those final publisher details are being finalized.',
|
||||
'This retained legal hub exists so visitors can still find the legal baseline quickly while the canonical notice itself lives on Imprint and the core trust story lives on Trust.',
|
||||
'During controlled evaluation, legal and privacy inquiries can be routed through the public contact path while launch-ready publisher details continue to tighten.',
|
||||
],
|
||||
bullets: [
|
||||
'Operating entity and jurisdictional disclosure fields belong in this legal hub before launch.',
|
||||
'Privacy and website terms stay published as standalone routes now.',
|
||||
'The legal hub remains the single public path for future launch-required disclosures.',
|
||||
'Trust stays top-level visible in the header, not buried inside legal pages.',
|
||||
'Privacy, Terms, and Imprint remain directly reachable from the footer.',
|
||||
'The legal hub stays published without becoming part of the initial core navigation.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -17,8 +17,8 @@ export const privacyHero: HeroContent = {
|
||||
label: 'Return to contact',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/terms',
|
||||
label: 'Review website terms',
|
||||
href: '/imprint',
|
||||
label: 'Review the imprint',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
@ -53,7 +53,7 @@ export const privacySections: LegalSection[] = [
|
||||
{
|
||||
title: 'Questions and updates',
|
||||
body: [
|
||||
'Privacy questions can be routed through the public contact path until the final launch legal notice publishes the full operating-entity details for privacy correspondence.',
|
||||
'Privacy questions can be routed through the public contact path until the final launch imprint publishes the full operating-entity details for privacy correspondence.',
|
||||
'If the public-site data handling model changes materially, this page should be updated before or at the same time as the change.',
|
||||
],
|
||||
},
|
||||
|
||||
@ -9,36 +9,36 @@ import type {
|
||||
export const productSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Product',
|
||||
description:
|
||||
'TenantAtlas connects inventory, snapshots, restore safety, drift visibility, findings, exceptions, and evidence into one governance model.',
|
||||
'TenantAtlas explains backup, restore, version history, drift, findings, evidence, and reviews as one operating model rather than a loose feature list.',
|
||||
path: '/product',
|
||||
};
|
||||
|
||||
export const productHero: HeroContent = {
|
||||
eyebrow: 'Product model',
|
||||
title: 'One operating model for change history, drift visibility, and review readiness.',
|
||||
title: 'Explain the product as one operating model before asking a buyer to trust the route map around it.',
|
||||
description:
|
||||
'TenantAtlas treats Microsoft tenant governance as one connected system: observe the current state, preserve immutable history, detect meaningful change, and support reviews or restores with the context operators actually need.',
|
||||
primaryCta: {
|
||||
href: '/solutions',
|
||||
label: 'See audience fit',
|
||||
href: '/trust',
|
||||
label: 'Review the trust posture',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Talk through your current operating model',
|
||||
label: 'Start the working session',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Inventory first, snapshots second.',
|
||||
'Restore flows stay previewable and attributable.',
|
||||
'Evidence and review posture stay connected to real change history.',
|
||||
'Backup, restore, versioning, auditability, drift, and evidence belong in one explanation layer.',
|
||||
'Trust and changelog should deepen the product read, not replace it.',
|
||||
'Audience-fit detail remains secondary until the core model is understood.',
|
||||
],
|
||||
};
|
||||
|
||||
export const productMetrics: MetricItem[] = [
|
||||
{
|
||||
value: '4',
|
||||
label: 'Operator questions',
|
||||
description: 'What changed? Why does it matter? What can be restored? What needs review now?',
|
||||
value: '6',
|
||||
label: 'Capability areas',
|
||||
description: 'Backup, restore, versioning, audit, drift visibility, and governance workflows are made legible as one system.',
|
||||
},
|
||||
{
|
||||
value: '100%',
|
||||
@ -49,38 +49,38 @@ export const productMetrics: MetricItem[] = [
|
||||
|
||||
export const productModelBlocks: FeatureItemContent[] = [
|
||||
{
|
||||
eyebrow: 'Connected governance model',
|
||||
title: 'Inventory creates the starting point for every other decision.',
|
||||
eyebrow: 'Inventory and drift',
|
||||
title: 'Current-state inventory and drift visibility establish what the tenant actually looks like now.',
|
||||
description:
|
||||
'The product begins with the last observed tenant state so teams can compare real configuration truth instead of relying on partial memory or exported spreadsheets.',
|
||||
'The product starts with last-observed tenant state so teams can compare real configuration truth instead of relying on partial memory or exported spreadsheets.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Connected governance model',
|
||||
title: 'Snapshots add immutable history without replacing current truth.',
|
||||
eyebrow: 'Backup and versioning',
|
||||
title: 'Snapshots and versions preserve immutable history without replacing present-tense truth.',
|
||||
description:
|
||||
'Backups and versions are explicit artifacts. They preserve what was seen at a point in time while keeping the present-tense inventory readable.',
|
||||
'Backups and versions are explicit artifacts tied to tenant context, operators, and timing so the history remains reproducible and queryable.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Connected governance model',
|
||||
eyebrow: 'Restore safety',
|
||||
title: 'Restore is handled as a governed operation, not as a blind push.',
|
||||
description:
|
||||
'Preview, validation, selective scope, and confirmation reduce the risk of turning a recovery step into a new incident.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Drift visibility',
|
||||
eyebrow: 'Audit and review',
|
||||
title: 'Differences become reviewable signals instead of noisy raw deltas.',
|
||||
description:
|
||||
'Human-readable summaries and structured differences help operators and reviewers decide what changed and what needs action.',
|
||||
'Human-readable summaries and structured differences help operators and reviewers decide what changed, who needs to know, and what deserves follow-up.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Exceptions & evidence',
|
||||
eyebrow: 'Findings and evidence',
|
||||
title: 'Findings, exceptions, and evidence stay anchored to operational truth.',
|
||||
description:
|
||||
'Governance discussions stay attached to the real object, version, and review context instead of drifting into separate manual trackers.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Operator safety',
|
||||
title: 'Auditability is part of the product shape, not a later add-on.',
|
||||
eyebrow: 'Baselines and governance',
|
||||
title: 'Baselines, reviews, and operator safety belong to the same workflow.',
|
||||
description:
|
||||
'The product is built so teams can explain actions afterward, not just execute them quickly in the moment.',
|
||||
},
|
||||
@ -88,20 +88,20 @@ export const productModelBlocks: FeatureItemContent[] = [
|
||||
|
||||
export const productNarrative: CalloutContent[] = [
|
||||
{
|
||||
eyebrow: 'Why it is not a feature list',
|
||||
eyebrow: 'What it is',
|
||||
title: 'The point is not “backup plus reporting plus restore.”',
|
||||
description:
|
||||
'The point is to reduce operator uncertainty by keeping those capabilities connected through the same source material and the same decision flow.',
|
||||
tone: 'accent',
|
||||
},
|
||||
{
|
||||
eyebrow: 'What teams get',
|
||||
eyebrow: 'What it is for',
|
||||
title: 'A calmer path from observation to action.',
|
||||
description:
|
||||
'Teams can move from understanding the current tenant state to comparing history, planning remediation, or reviewing restore options without leaving the product model behind.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'What teams avoid',
|
||||
eyebrow: 'What it is not',
|
||||
title: 'No generic dashboard theater.',
|
||||
description:
|
||||
'The product story avoids pretending that another alerting page or compliance badge alone solves governance discipline.',
|
||||
|
||||
@ -8,28 +8,28 @@ import type {
|
||||
export const solutionsSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Solutions',
|
||||
description:
|
||||
'TenantAtlas fits MSP and enterprise IT teams differently, and the public story should make those operating-model differences explicit.',
|
||||
'TenantAtlas keeps MSP and enterprise outcome framing available as a supporting page without requiring it for the first public read.',
|
||||
path: '/solutions',
|
||||
};
|
||||
|
||||
export const solutionsHero: HeroContent = {
|
||||
eyebrow: 'Audience fit',
|
||||
title: 'Show how TenantAtlas fits MSP delivery teams and enterprise operators without collapsing them into one generic story.',
|
||||
eyebrow: 'Retained supporting page',
|
||||
title: 'Keep MSP and enterprise audience-fit detail published without letting it crowd the core route set.',
|
||||
description:
|
||||
'The product helps different organizations answer similar governance questions, but the surrounding workflow, accountability, and evidence needs are not identical. The site should acknowledge that directly while keeping one calm visual rhythm.',
|
||||
'The product helps different organizations answer similar governance questions, but the surrounding workflow, accountability, and evidence needs are not identical. This page stays available without becoming required orientation for every first-time visitor.',
|
||||
primaryCta: {
|
||||
href: '/integrations',
|
||||
label: 'Review the ecosystem fit',
|
||||
href: '/product',
|
||||
label: 'See the product model',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Talk through your evaluation path',
|
||||
label: 'Start the working session',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Separate MSP and enterprise language on purpose.',
|
||||
'Keep the product story stable while the buying context changes.',
|
||||
'Avoid forcing every visitor through the same generic motion.',
|
||||
'Stay secondary to Product, Trust, Changelog, and Contact.',
|
||||
],
|
||||
};
|
||||
|
||||
@ -61,7 +61,7 @@ export const solutionsAudiences: AudienceRowContent[] = [
|
||||
'Keep restore and remediation conversations grounded in the current tenant state and the relevant history.',
|
||||
],
|
||||
cta: {
|
||||
href: '/security-trust',
|
||||
href: '/trust',
|
||||
label: 'Inspect the trust posture',
|
||||
variant: 'secondary',
|
||||
},
|
||||
|
||||
@ -17,8 +17,8 @@ export const termsHero: HeroContent = {
|
||||
label: 'Return to contact',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/privacy',
|
||||
label: 'Review privacy',
|
||||
href: '/imprint',
|
||||
label: 'Review the imprint',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
@ -54,7 +54,7 @@ export const termsSections: LegalSection[] = [
|
||||
title: 'Questions',
|
||||
body: [
|
||||
'Questions about the public website terms, privacy, or future product legal materials can be routed through the public contact path.',
|
||||
'The legal hub remains the public anchor for later launch-ready legal disclosures.',
|
||||
'The legal hub remains published, while Imprint carries the canonical notice baseline.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
84
apps/website/src/content/pages/trust.ts
Normal file
84
apps/website/src/content/pages/trust.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type {
|
||||
CalloutContent,
|
||||
HeroContent,
|
||||
PageSeo,
|
||||
TrustPrincipleContent,
|
||||
} from '@/types/site';
|
||||
|
||||
export const trustSeo: PageSeo = {
|
||||
title: 'TenantAtlas | Trust',
|
||||
description:
|
||||
'TenantAtlas uses the Trust surface to explain tenant isolation, access boundaries, operating discipline, and the limits of public claims in one explicit place.',
|
||||
path: '/trust',
|
||||
};
|
||||
|
||||
export const trustHero: HeroContent = {
|
||||
eyebrow: 'Canonical trust surface',
|
||||
title: 'Review the trust posture in the language of operator safeguards, access boundaries, and explicit limits.',
|
||||
description:
|
||||
'The Trust page exists so public claims about isolation, hosting, update discipline, and safer change handling have one bounded supporting surface instead of leaking across product copy.',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Start the working session',
|
||||
},
|
||||
secondaryCta: {
|
||||
href: '/imprint',
|
||||
label: 'Read the imprint',
|
||||
variant: 'secondary',
|
||||
},
|
||||
highlights: [
|
||||
'Tenant isolation, access handling, and safer restore posture are explained directly.',
|
||||
'Public claims stay narrower than internal ambition or future certification plans.',
|
||||
'Trust still routes to a human conversation instead of becoming a dead-end policy wall.',
|
||||
],
|
||||
};
|
||||
|
||||
export const trustPrinciples: TrustPrincipleContent[] = [
|
||||
{
|
||||
title: 'Tenant isolation is a product boundary, not a marketing adjective.',
|
||||
description:
|
||||
'The public trust story should make it clear that tenant-scoped truth, access, and workflow boundaries are deliberate product rules, not hand-wavy positioning language.',
|
||||
note: 'Isolation must stay attributable.',
|
||||
},
|
||||
{
|
||||
title: 'Sensitive access remains bounded, reviewable, and purpose-specific.',
|
||||
description:
|
||||
'The product depends on Microsoft tenant-facing access where governance work requires it, but the public site should explain those boundaries without pretending the website itself is part of that runtime path.',
|
||||
note: 'Bound the trust story carefully.',
|
||||
},
|
||||
{
|
||||
title: 'Restore and other high-risk actions are framed around preview and confirmation.',
|
||||
description:
|
||||
'Safer changes come from validation, selective scope, and explicit confirmation rather than from confidence theater about one-click remediation.',
|
||||
note: 'Safer execution is part of credibility.',
|
||||
},
|
||||
{
|
||||
title: 'Operating discipline matters as much as storage or hosting claims.',
|
||||
description:
|
||||
'Change history, evidence linkage, review posture, and update discipline all contribute to whether a serious buyer can trust the product story.',
|
||||
note: 'Trust includes how the team operates.',
|
||||
},
|
||||
];
|
||||
|
||||
export const trustNotes: CalloutContent[] = [
|
||||
{
|
||||
eyebrow: 'Hosting and residency claims',
|
||||
title: 'Hosting-region or residency language must stay qualified and current.',
|
||||
description:
|
||||
'If the product makes a public claim about where data or workloads run, the Trust page should explain the scope, boundary, and any conditions instead of leaving the claim unbounded.',
|
||||
tone: 'accent',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Update discipline',
|
||||
title: 'Operational rigor needs a public explanation even before formal certifications exist.',
|
||||
description:
|
||||
'A serious trust page should explain update posture, review expectations, and how the product treats risky changes without falling back on empty compliance theater.',
|
||||
},
|
||||
{
|
||||
eyebrow: 'What we will not imply',
|
||||
title: 'No absolute legal-compliance claims and no vague “fully automated governance” language.',
|
||||
description:
|
||||
'Trust weakens quickly when a public page substitutes slogans for the real controls and workflows a buyer will later inspect.',
|
||||
tone: 'subtle',
|
||||
},
|
||||
];
|
||||
@ -35,6 +35,7 @@ const openGraphDescription = Astro.props.openGraphDescription ?? description;
|
||||
<meta property="og:title" content={openGraphTitle} />
|
||||
<meta property="og:description" content={openGraphDescription} />
|
||||
<meta property="og:type" content="website" />
|
||||
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={openGraphTitle} />
|
||||
<meta name="twitter:description" content={openGraphDescription} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { coreRoutes, getPageDefinition, siteMetadata } from '@/lib/site';
|
||||
import { getCanonicalPath, getPageDefinition, publishedSitemapRoutes, siteMetadata } from '@/lib/site';
|
||||
import type { PageFamily, PageRole, PageSeo, ShellTone } from '@/types/site';
|
||||
|
||||
export interface ResolvedSeo extends PageSeo {
|
||||
@ -12,7 +12,7 @@ export interface ResolvedSeo extends PageSeo {
|
||||
}
|
||||
|
||||
export function buildCanonicalUrl(path: string): string {
|
||||
return new URL(path, siteMetadata.siteUrl).toString();
|
||||
return new URL(getCanonicalPath(path), siteMetadata.siteUrl).toString();
|
||||
}
|
||||
|
||||
export function resolveSeo(seo: PageSeo): ResolvedSeo {
|
||||
@ -30,6 +30,6 @@ export function resolveSeo(seo: PageSeo): ResolvedSeo {
|
||||
};
|
||||
}
|
||||
|
||||
export function sitemapEntries(): string[] {
|
||||
return [...coreRoutes].map((path) => buildCanonicalUrl(path));
|
||||
export async function sitemapEntries(): Promise<string[]> {
|
||||
return [...publishedSitemapRoutes].map((path) => buildCanonicalUrl(path));
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import type {
|
||||
CollectionName,
|
||||
CtaLink,
|
||||
FooterLead,
|
||||
FooterNavigationGroup,
|
||||
@ -6,8 +9,9 @@ import type {
|
||||
PageDefinition,
|
||||
PageFamily,
|
||||
ShellTone,
|
||||
SiteMetadata,
|
||||
SitePath,
|
||||
SiteMetadata,
|
||||
SurfaceAvailability,
|
||||
VisualFoundationContract,
|
||||
} from '@/types/site';
|
||||
|
||||
@ -52,40 +56,15 @@ export const visualFoundationContract: VisualFoundationContract = {
|
||||
surfaceLayers: ['page-background', 'default-content', 'card', 'elevated', 'muted-inset', 'highlighted'],
|
||||
};
|
||||
|
||||
export const primaryNavigation: NavigationItem[] = [
|
||||
{ href: '/product', label: 'Product', description: 'Understand the operating model.' },
|
||||
{ href: '/solutions', label: 'Solutions', description: 'See the fit for MSP and enterprise teams.' },
|
||||
{ href: '/security-trust', label: 'Security & Trust', description: 'Review the product posture.' },
|
||||
{ href: '/integrations', label: 'Integrations', description: 'Inspect the real ecosystem fit.' },
|
||||
{ href: '/contact', label: 'Contact', description: 'Reach the team for a working session.' },
|
||||
];
|
||||
type CollectionGatedNavigationItem = NavigationItem & {
|
||||
collection?: CollectionName;
|
||||
};
|
||||
|
||||
export const footerNavigationGroups: FooterNavigationGroup[] = [
|
||||
{
|
||||
title: 'Explore',
|
||||
items: [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/product', label: 'Product' },
|
||||
{ href: '/solutions', label: 'Solutions' },
|
||||
{ href: '/security-trust', label: 'Security & Trust' },
|
||||
{ href: '/integrations', label: 'Integrations' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Next step',
|
||||
items: [
|
||||
{ href: '/contact', label: 'Contact / Demo' },
|
||||
{ href: '/legal', label: 'Legal' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Legal',
|
||||
items: [
|
||||
{ href: '/privacy', label: 'Privacy' },
|
||||
{ href: '/terms', label: 'Terms' },
|
||||
],
|
||||
},
|
||||
];
|
||||
interface FooterGroupSeed {
|
||||
collection?: CollectionName;
|
||||
items: CollectionGatedNavigationItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const contactCta: CtaLink = {
|
||||
href: '/contact',
|
||||
@ -97,17 +76,17 @@ export const contactCta: CtaLink = {
|
||||
const footerLeadByFamily: Record<PageFamily, FooterLead> = {
|
||||
landing: {
|
||||
eyebrow: 'Keep the next move obvious',
|
||||
title: 'A calmer public surface should still lead somewhere concrete.',
|
||||
title: 'Product, trust, progress, and contact should stay connected.',
|
||||
description:
|
||||
'Landing pages should move visitors from orientation into a product, trust, or contact path without competing CTAs or dead ends.',
|
||||
'Landing pages should move visitors from orientation into product understanding, trust review, changelog proof, or the contact path without fake maturity signals.',
|
||||
intent: 'conversion',
|
||||
primaryCta: contactCta,
|
||||
},
|
||||
trust: {
|
||||
eyebrow: 'Trust stays actionable',
|
||||
title: 'Security, legal, and contact paths should reinforce one another.',
|
||||
title: 'Trust, legal context, and the next conversation should reinforce one another.',
|
||||
description:
|
||||
'Trust-oriented pages should keep legal context, product posture, and the working-session route connected instead of turning caution into friction.',
|
||||
'Trust-oriented pages should keep legal context, product posture, and the working-session route connected instead of turning diligence into friction.',
|
||||
intent: 'guidance',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
@ -118,9 +97,9 @@ const footerLeadByFamily: Record<PageFamily, FooterLead> = {
|
||||
},
|
||||
content: {
|
||||
eyebrow: 'Reading should still progress',
|
||||
title: 'Long-form pages need the same action hierarchy as landing pages.',
|
||||
title: 'Supporting pages should still route visitors back into the core journey.',
|
||||
description:
|
||||
'Contact, legal, privacy, and terms routes should feel deliberate and connected to the evaluation path instead of behaving like detached documents.',
|
||||
'Changelog, contact, legal, privacy, imprint, and retained secondary routes should feel deliberate and connected to the evaluation path instead of behaving like detached documents.',
|
||||
intent: 'legal',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
@ -146,30 +125,251 @@ const headerCtaByFamily: Record<PageFamily, CtaLink> = {
|
||||
},
|
||||
content: {
|
||||
href: '/contact',
|
||||
label: 'Start the working session',
|
||||
label: 'Start the contact path',
|
||||
helper: 'Turn the reading path into a concrete next step.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
export const canonicalCoreRoutes: SitePath[] = [
|
||||
'/',
|
||||
'/product',
|
||||
'/trust',
|
||||
'/changelog',
|
||||
'/contact',
|
||||
'/privacy',
|
||||
'/imprint',
|
||||
];
|
||||
|
||||
export const retainedSecondaryRoutes: SitePath[] = ['/legal', '/terms', '/solutions', '/integrations'];
|
||||
export const compatibilityRoutes: SitePath[] = ['/security-trust'];
|
||||
export const primaryConversionRoute: SitePath = '/contact';
|
||||
export const unpublishedEditorialCollections: CollectionName[] = ['articles'];
|
||||
|
||||
export async function getSurfaceAvailability(): Promise<SurfaceAvailability> {
|
||||
const changelog = await getCollection('changelog');
|
||||
|
||||
return {
|
||||
articles: false,
|
||||
changelog: changelog.some((entry) => entry.data.draft !== true),
|
||||
resources: false,
|
||||
};
|
||||
}
|
||||
|
||||
const primaryNavigationSeeds: CollectionGatedNavigationItem[] = [
|
||||
{ href: '/product', label: 'Product', description: 'See how the product connects backup, restore, drift, and evidence.' },
|
||||
{ href: '/trust', label: 'Trust', description: 'Review the operating posture and bounded public claims.' },
|
||||
{ href: '/changelog', label: 'Changelog', description: 'Inspect dated product progress instead of placeholder content.' },
|
||||
{ href: '/resources', label: 'Resources', description: 'Optional deeper content when substantive material exists.', collection: 'resources' },
|
||||
{ href: '/contact', label: 'Contact', description: 'Move into a working session with one clear next step.' },
|
||||
];
|
||||
|
||||
const footerNavigationGroupSeeds: FooterGroupSeed[] = [
|
||||
{
|
||||
title: 'Product',
|
||||
items: [
|
||||
{ href: '/product', label: 'Product' },
|
||||
{ href: '/changelog', label: 'Changelog' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Trust & Legal',
|
||||
items: [
|
||||
{ href: '/trust', label: 'Trust' },
|
||||
{ href: '/privacy', label: 'Privacy' },
|
||||
{ href: '/imprint', label: 'Imprint' },
|
||||
{ href: '/terms', label: 'Terms' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Contact',
|
||||
items: [{ href: '/contact', label: 'Contact' }],
|
||||
},
|
||||
{
|
||||
title: 'Content',
|
||||
collection: 'resources',
|
||||
items: [{ href: '/resources', label: 'Resources', collection: 'resources' }],
|
||||
},
|
||||
];
|
||||
|
||||
function filterCollectionGatedItems(
|
||||
items: CollectionGatedNavigationItem[],
|
||||
availability: SurfaceAvailability,
|
||||
): NavigationItem[] {
|
||||
return items
|
||||
.filter((item) => {
|
||||
if (!item.collection) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return availability[item.collection];
|
||||
})
|
||||
.map(({ collection: _collection, ...item }) => item);
|
||||
}
|
||||
|
||||
export async function getPrimaryNavigation(): Promise<NavigationItem[]> {
|
||||
const availability = await getSurfaceAvailability();
|
||||
|
||||
return filterCollectionGatedItems(primaryNavigationSeeds, availability);
|
||||
}
|
||||
|
||||
export async function getFooterNavigationGroups(): Promise<FooterNavigationGroup[]> {
|
||||
const availability = await getSurfaceAvailability();
|
||||
|
||||
return footerNavigationGroupSeeds
|
||||
.filter((group) => (group.collection ? availability[group.collection] : true))
|
||||
.map(({ collection: _collection, items, title }) => ({
|
||||
title,
|
||||
items: filterCollectionGatedItems(items, availability),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
}
|
||||
|
||||
export const pageDefinitions: Record<SitePath, PageDefinition> = {
|
||||
'/': { path: '/', pageRole: 'home', family: 'landing', shellTone: 'brand' },
|
||||
'/product': { path: '/product', pageRole: 'product', family: 'landing', shellTone: 'brand' },
|
||||
'/solutions': { path: '/solutions', pageRole: 'solutions', family: 'landing', shellTone: 'brand' },
|
||||
'/security-trust': { path: '/security-trust', pageRole: 'trust', family: 'trust', shellTone: 'trust' },
|
||||
'/integrations': { path: '/integrations', pageRole: 'integrations', family: 'landing', shellTone: 'neutral' },
|
||||
'/contact': { path: '/contact', pageRole: 'contact', family: 'content', shellTone: 'neutral' },
|
||||
'/legal': { path: '/legal', pageRole: 'legal', family: 'trust', shellTone: 'trust' },
|
||||
'/privacy': { path: '/privacy', pageRole: 'privacy', family: 'content', shellTone: 'neutral' },
|
||||
'/terms': { path: '/terms', pageRole: 'terms', family: 'content', shellTone: 'neutral' },
|
||||
'/': {
|
||||
path: '/',
|
||||
canonicalPath: '/',
|
||||
pageRole: 'home',
|
||||
family: 'landing',
|
||||
shellTone: 'brand',
|
||||
priority: 'required',
|
||||
journeyStage: 'entry',
|
||||
surfaceGroup: 'core',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/product': {
|
||||
path: '/product',
|
||||
canonicalPath: '/product',
|
||||
pageRole: 'product',
|
||||
family: 'landing',
|
||||
shellTone: 'brand',
|
||||
priority: 'required',
|
||||
journeyStage: 'first-clarification',
|
||||
surfaceGroup: 'core',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/trust': {
|
||||
path: '/trust',
|
||||
canonicalPath: '/trust',
|
||||
pageRole: 'trust',
|
||||
family: 'trust',
|
||||
shellTone: 'trust',
|
||||
priority: 'required',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'core',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/changelog': {
|
||||
path: '/changelog',
|
||||
canonicalPath: '/changelog',
|
||||
pageRole: 'changelog',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'recommended',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'core',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/contact': {
|
||||
path: '/contact',
|
||||
canonicalPath: '/contact',
|
||||
pageRole: 'contact',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'required',
|
||||
journeyStage: 'action',
|
||||
surfaceGroup: 'core',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/privacy': {
|
||||
path: '/privacy',
|
||||
canonicalPath: '/privacy',
|
||||
pageRole: 'privacy',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'required',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'legal',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/imprint': {
|
||||
path: '/imprint',
|
||||
canonicalPath: '/imprint',
|
||||
pageRole: 'imprint',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'required',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'legal',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/legal': {
|
||||
path: '/legal',
|
||||
canonicalPath: '/legal',
|
||||
pageRole: 'legal',
|
||||
family: 'content',
|
||||
shellTone: 'trust',
|
||||
priority: 'secondary',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'legal',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/terms': {
|
||||
path: '/terms',
|
||||
canonicalPath: '/terms',
|
||||
pageRole: 'terms',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'secondary',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'legal',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/solutions': {
|
||||
path: '/solutions',
|
||||
canonicalPath: '/solutions',
|
||||
pageRole: 'solutions',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'secondary',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'supporting',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/integrations': {
|
||||
path: '/integrations',
|
||||
canonicalPath: '/integrations',
|
||||
pageRole: 'integrations',
|
||||
family: 'content',
|
||||
shellTone: 'neutral',
|
||||
priority: 'secondary',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'supporting',
|
||||
inSitemap: true,
|
||||
},
|
||||
'/security-trust': {
|
||||
path: '/security-trust',
|
||||
canonicalPath: '/trust',
|
||||
pageRole: 'trust',
|
||||
family: 'trust',
|
||||
shellTone: 'trust',
|
||||
priority: 'compatibility',
|
||||
journeyStage: 'deepening',
|
||||
surfaceGroup: 'compatibility',
|
||||
inSitemap: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const coreRoutes = Object.keys(pageDefinitions) as SitePath[];
|
||||
export const publishedSitemapRoutes = [...canonicalCoreRoutes, ...retainedSecondaryRoutes];
|
||||
|
||||
export function getPageDefinition(path: string): PageDefinition {
|
||||
return pageDefinitions[path as SitePath] ?? pageDefinitions['/'];
|
||||
}
|
||||
|
||||
export function getCanonicalPath(path: string): SitePath {
|
||||
return getPageDefinition(path).canonicalPath;
|
||||
}
|
||||
|
||||
export function getPageFamily(path: string): PageFamily {
|
||||
return getPageDefinition(path).family;
|
||||
}
|
||||
|
||||
61
apps/website/src/pages/changelog.astro
Normal file
61
apps/website/src/pages/changelog.astro
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Section from '@/components/primitives/Section.astro';
|
||||
import SectionHeader from '@/components/primitives/SectionHeader.astro';
|
||||
import CTASection from '@/components/sections/CTASection.astro';
|
||||
import PageHero from '@/components/sections/PageHero.astro';
|
||||
import { changelogHero, changelogSeo } from '@/content/pages/changelog';
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en', { dateStyle: 'long' });
|
||||
const entries = (await getCollection('changelog'))
|
||||
.filter((entry) => entry.data.draft !== true)
|
||||
.sort((left, right) => right.data.publishedAt.valueOf() - left.data.publishedAt.valueOf());
|
||||
---
|
||||
|
||||
<PageShell currentPath="/changelog" title={changelogSeo.title} description={changelogSeo.description}>
|
||||
<PageHero
|
||||
hero={changelogHero}
|
||||
calloutTitle="Visible progress should be dated and concrete."
|
||||
calloutDescription="The changelog exists so returning visitors can verify product motion without mistaking placeholder resources or editorial ambitions for current release truth."
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Published updates"
|
||||
title="Dated updates, not vague momentum signals."
|
||||
description="Each entry should explain what changed in a concrete way and connect back to product understanding, trust, or the contact path."
|
||||
/>
|
||||
|
||||
<div class="grid gap-6">
|
||||
{entries.map((entry) => (
|
||||
<Card as="article" class="space-y-4">
|
||||
<p class="m-0 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-brand)]">
|
||||
{formatter.format(entry.data.publishedAt)}
|
||||
</p>
|
||||
<h2 class="m-0 font-[var(--font-display)] text-3xl text-[var(--color-ink-900)]">
|
||||
{entry.data.title}
|
||||
</h2>
|
||||
<p class="m-0 max-w-3xl text-base leading-7 text-[var(--color-copy)]">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Use visible progress to start a more concrete evaluation conversation."
|
||||
description="Once the route model and recent progress are clear, the next move should stay obvious: inspect the product model again or start the contact path."
|
||||
primary={{ href: '/contact', label: 'Start the working session' }}
|
||||
secondary={{ href: '/product', label: 'See the product model', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
@ -58,13 +58,14 @@ import {
|
||||
<Card variant="accent">
|
||||
<SectionHeader
|
||||
eyebrow="Before you share details"
|
||||
title="Legal basics stay visible from the contact flow."
|
||||
description="Visitors should be able to inspect privacy and terms before they continue."
|
||||
title="Trust and legal basics stay visible from the contact flow."
|
||||
description="Visitors should be able to inspect trust, privacy, terms, and imprint before they continue."
|
||||
/>
|
||||
<Cluster class="mt-6">
|
||||
<SecondaryCTA cta={{ href: '/trust', label: 'Trust', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/privacy', label: 'Privacy', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/terms', label: 'Terms', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/legal', label: 'Legal', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/imprint', label: 'Imprint', variant: 'secondary' }} size="sm" />
|
||||
</Cluster>
|
||||
<div class="mt-6">
|
||||
<RichText sections={contactLegalSections} />
|
||||
@ -76,9 +77,9 @@ import {
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Move from a public introduction into the legal and product details that support a real evaluation."
|
||||
description="The contact route should never strand a serious buyer. The next path stays visible whether they need product context, privacy details, or website terms."
|
||||
primary={{ href: '/legal', label: 'Read the legal surface' }}
|
||||
title="Keep the contact path connected to product truth and trust review."
|
||||
description="The contact route should never strand a serious buyer. The next path stays visible whether they need product context, trust detail, or the website legal baseline."
|
||||
primary={{ href: '/trust', label: 'Review the trust posture' }}
|
||||
secondary={{ href: '/product', label: 'Revisit the product model', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
31
apps/website/src/pages/imprint.astro
Normal file
31
apps/website/src/pages/imprint.astro
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
import RichText from '@/components/content/RichText.astro';
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Section from '@/components/primitives/Section.astro';
|
||||
import CTASection from '@/components/sections/CTASection.astro';
|
||||
import PageHero from '@/components/sections/PageHero.astro';
|
||||
import { imprintHero, imprintSections, imprintSeo } from '@/content/pages/imprint';
|
||||
---
|
||||
|
||||
<PageShell currentPath="/imprint" title={imprintSeo.title} description={imprintSeo.description}>
|
||||
<PageHero
|
||||
hero={imprintHero}
|
||||
calloutTitle="The legal-notice route should be explicit even before launch details are final."
|
||||
calloutDescription="Imprint is the canonical public notice surface, so the website can stay honest about where publisher and jurisdiction-specific details belong."
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="measure">
|
||||
<RichText sections={imprintSections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<CTASection
|
||||
eyebrow="Continue"
|
||||
title="Return to trust, privacy, or contact once the notice baseline is clear."
|
||||
description="Imprint should support the public trust and legal story without becoming a dead-end page."
|
||||
primary={{ href: '/contact', label: 'Return to contact' }}
|
||||
secondary={{ href: '/trust', label: 'Review the trust posture', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
@ -34,9 +34,9 @@ import {
|
||||
/>
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Product pillars"
|
||||
title="Carry one visual language from product orientation into proof."
|
||||
description="Each section should make the operating model easier to follow without switching into a louder or more decorative visual mode."
|
||||
eyebrow="Core route jobs"
|
||||
title="Understand the product, the trust posture, and the next step without route sprawl."
|
||||
description="Home should explain the operating model, show where trust lives, and surface visible product progress without pushing buyers into placeholder routes."
|
||||
items={homePillars}
|
||||
/>
|
||||
|
||||
@ -45,8 +45,8 @@ import {
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public proof"
|
||||
title="A credible first read should answer the next question without another layout mode."
|
||||
description="Why is this category needed, why is the story believable, and why should a serious buyer keep reading?"
|
||||
title="Answer the next question before a visitor has to hunt for another top-level route."
|
||||
description="Why this category matters, where trust claims live, and why the changelog exists should all be obvious from the first read."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
{homeProofBlocks.map((block) => <Callout content={block} />)}
|
||||
@ -57,9 +57,9 @@ import {
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Move from category clarity into the deeper product and trust path."
|
||||
description="From the Home page, visitors should be able to inspect the product model, review trust framing, or reach the contact path without guessing where to go next."
|
||||
primary={{ href: '/product', label: 'See the product model' }}
|
||||
title="Move from first read into product detail, trust review, or a working session."
|
||||
description="From Home, a serious buyer should be able to inspect the product model, verify public progress, or reach the contact path without guessing where to go next."
|
||||
primary={{ href: '/changelog', label: 'Read the changelog' }}
|
||||
secondary={{ href: '/contact', label: 'Start the working session', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
<PageHero
|
||||
hero={integrationsHero}
|
||||
calloutTitle="Real direction beats a longer wishlist."
|
||||
calloutDescription="The integrations page should reinforce where the product actually fits today and why those boundaries improve trust rather than limit it."
|
||||
calloutDescription="This supporting route should reinforce where the product actually fits today without pretending ecosystem detail belongs in primary navigation."
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
@ -28,7 +28,7 @@ import {
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Current direction"
|
||||
title="Keep ecosystem direction sharp, current, and bounded."
|
||||
title="Keep ecosystem fit visible without pretending it belongs in primary navigation."
|
||||
description="This page should stay focused on the contracts and ecosystems that matter to Microsoft tenant governance work now."
|
||||
/>
|
||||
<Grid cols="2">
|
||||
@ -48,8 +48,8 @@ import {
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Turn ecosystem fit into a practical evaluation conversation."
|
||||
description="Once a buyer sees the Microsoft-centric fit, the next useful step is a working session about their current environment, governance needs, and rollout questions."
|
||||
description="Once a buyer sees the Microsoft-centric fit, the next useful step is a working session or a return to the core product explanation."
|
||||
primary={{ href: '/contact', label: 'Plan the working session' }}
|
||||
secondary={{ href: '/product', label: 'Revisit the product model', variant: 'secondary' }}
|
||||
secondary={{ href: '/product', label: 'See the product model', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -14,8 +14,8 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
<PageShell currentPath="/legal" title={legalSeo.title} description={legalSeo.description}>
|
||||
<PageHero
|
||||
hero={legalHero}
|
||||
calloutTitle="Public legal basics belong in one obvious place."
|
||||
calloutDescription="The legal hub keeps the conversion path honest by making privacy, terms, and notice routing easy to find before or during evaluation conversations."
|
||||
calloutTitle="The legal baseline should stay explicit without taking over the route hierarchy."
|
||||
calloutDescription="The retained legal hub helps visitors find the privacy, terms, and notice baseline quickly while Trust and Contact remain easier to discover."
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
@ -23,10 +23,19 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Available now"
|
||||
title="Use one legal surface for privacy, terms, and notice routing."
|
||||
description="The legal hub should work as an index and as the public home for launch-required legal notices."
|
||||
title="Keep the legal baseline explicit without promoting it into the main navigation."
|
||||
description="The retained legal hub should work as an index for Trust, Privacy, Terms, and Imprint while staying secondary to the core buyer journey."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
<Grid cols="2">
|
||||
<Card>
|
||||
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Trust</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">
|
||||
Review the canonical public trust surface for tenant isolation, access boundaries, and operating discipline.
|
||||
</p>
|
||||
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="/trust">
|
||||
Trust
|
||||
</a>
|
||||
</Card>
|
||||
<Card>
|
||||
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Privacy</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">
|
||||
@ -46,12 +55,21 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
</a>
|
||||
</Card>
|
||||
<Card variant="accent">
|
||||
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Public legal notice</h3>
|
||||
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Imprint</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">
|
||||
This hub also owns the future launch-ready operator identity and jurisdiction-specific disclosure section.
|
||||
Review the canonical notice baseline for publisher identity and jurisdiction-specific disclosure details.
|
||||
</p>
|
||||
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="#public-legal-notice">
|
||||
Legal notice
|
||||
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="/imprint">
|
||||
Imprint
|
||||
</a>
|
||||
</Card>
|
||||
<Card>
|
||||
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Terms</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">
|
||||
Read the website terms that explain the public-site scope and why marketing pages do not replace signed agreements.
|
||||
</p>
|
||||
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="/terms">
|
||||
Terms
|
||||
</a>
|
||||
</Card>
|
||||
</Grid>
|
||||
@ -59,7 +77,7 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Section id="public-legal-notice" density="compact" layer="3">
|
||||
<Section density="compact" layer="3">
|
||||
<Container width="measure">
|
||||
<RichText sections={legalNoticeSections} />
|
||||
</Container>
|
||||
@ -67,9 +85,9 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
|
||||
<CTASection
|
||||
eyebrow="Continue"
|
||||
title="Return to the contact path once the legal basics are clear."
|
||||
title="Return to trust or contact once the legal baseline is clear."
|
||||
description="The legal surface should support a qualified conversation, not interrupt it."
|
||||
primary={{ href: '/contact', label: 'Return to contact' }}
|
||||
secondary={{ href: '/product', label: 'Revisit the product model', variant: 'secondary' }}
|
||||
secondary={{ href: '/trust', label: 'Review the trust posture', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -23,9 +23,9 @@ import { privacyHero, privacySections, privacySeo } from '@/content/pages/privac
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Return to the product or contact flow after reviewing public-site privacy."
|
||||
title="Return to trust or contact once the privacy baseline is clear."
|
||||
description="Visitors should be able to move back into the evaluation path without losing context."
|
||||
primary={{ href: '/contact', label: 'Return to contact' }}
|
||||
secondary={{ href: '/terms', label: 'Review website terms', variant: 'secondary' }}
|
||||
secondary={{ href: '/imprint', label: 'Review the imprint', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -27,7 +27,7 @@ import {
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Connected governance model"
|
||||
title="Make the operating model legible before the feature list."
|
||||
title="Explain what the product does before asking for buyer trust."
|
||||
description="This page should explain how the pieces fit together so visitors do not mistake the product for a loose collection of backup, reporting, and restore features."
|
||||
items={productModelBlocks}
|
||||
/>
|
||||
@ -37,8 +37,8 @@ import {
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Narrative"
|
||||
title="Keep the path from observation to action readable."
|
||||
description="The public product page should make it obvious how the product helps a team move from current-state understanding into reviewable action."
|
||||
title="Keep the path from product truth into trust and action readable."
|
||||
description="The public product page should make it obvious how the product helps a team move from current-state understanding into trust review, visible progress, and reviewable action."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
{productNarrative.map((block) => <Callout content={block} />)}
|
||||
@ -49,12 +49,12 @@ import {
|
||||
|
||||
<CTASection
|
||||
eyebrow="Continue"
|
||||
title="Inspect whether the operating model fits your audience and workflow."
|
||||
description="The next useful questions are who the product is for, how trust claims stay grounded, and what a working conversation with the team should cover."
|
||||
primary={{ href: '/solutions', label: 'See audience fit' }}
|
||||
title="Trust review and visible progress should follow the product explanation cleanly."
|
||||
description="Once the product model is clear, the next useful moves are to inspect the Trust surface, verify current progress, and start the contact path."
|
||||
primary={{ href: '/changelog', label: 'Read the changelog' }}
|
||||
secondary={{
|
||||
href: '/contact',
|
||||
label: 'Talk through your current operating model',
|
||||
label: 'Start the working session',
|
||||
variant: 'secondary',
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,59 +1,3 @@
|
||||
---
|
||||
import Callout from '@/components/content/Callout.astro';
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Grid from '@/components/primitives/Grid.astro';
|
||||
import Section from '@/components/primitives/Section.astro';
|
||||
import SectionHeader from '@/components/primitives/SectionHeader.astro';
|
||||
import CTASection from '@/components/sections/CTASection.astro';
|
||||
import PageHero from '@/components/sections/PageHero.astro';
|
||||
import TrustGrid from '@/components/sections/TrustGrid.astro';
|
||||
import {
|
||||
securityPrinciples,
|
||||
securityTrustHero,
|
||||
securityTrustNotes,
|
||||
securityTrustSeo,
|
||||
} from '@/content/pages/security-trust';
|
||||
return Astro.redirect('/trust', 308);
|
||||
---
|
||||
|
||||
<PageShell
|
||||
currentPath="/security-trust"
|
||||
title={securityTrustSeo.title}
|
||||
description={securityTrustSeo.description}
|
||||
>
|
||||
<PageHero
|
||||
hero={securityTrustHero}
|
||||
calloutTitle="Trust-first, not trust-theater."
|
||||
calloutDescription="The page should help technical buyers see the operator safeguards and the intentional limits of the launch story before they hear a sales pitch."
|
||||
/>
|
||||
|
||||
<TrustGrid
|
||||
eyebrow="Product posture"
|
||||
title="Show operator safeguards with restrained public claims."
|
||||
description="The trust page should explain the guardrails that matter to a serious buyer: previewability, attributable change history, evidence linkage, and restrained public claims."
|
||||
items={securityPrinciples}
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public messaging"
|
||||
title="Substantiated posture should stay narrower than internal ambition."
|
||||
description="Keep the public trust story within the set of claims the team can support at launch."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
{securityTrustNotes.map((note) => <Callout content={note} />)}
|
||||
</Grid>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Legal clarity and conversation path should stay reachable from the trust page."
|
||||
description="A buyer evaluating trust should be able to move directly to public legal information or a working discussion without friction."
|
||||
primary={{ href: '/legal', label: 'Read the legal surface' }}
|
||||
secondary={{ href: '/contact', label: 'Discuss trust requirements', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -2,8 +2,8 @@ import type { APIRoute } from 'astro';
|
||||
|
||||
import { sitemapEntries } from '@/lib/seo';
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
const urls = sitemapEntries()
|
||||
export const GET: APIRoute = async () => {
|
||||
const urls = (await sitemapEntries())
|
||||
.map((url) => ` <url><loc>${url}</loc></url>`)
|
||||
.join('\n');
|
||||
|
||||
|
||||
@ -19,8 +19,8 @@ import {
|
||||
<PageShell currentPath="/solutions" title={solutionsSeo.title} description={solutionsSeo.description}>
|
||||
<PageHero
|
||||
hero={solutionsHero}
|
||||
calloutTitle="Audience-specific fit without product sprawl."
|
||||
calloutDescription="The public site can speak differently to MSP and enterprise visitors while staying anchored to the same product truth."
|
||||
calloutTitle="Audience detail can stay available without owning the first visit."
|
||||
calloutDescription="This supporting route stays published for deeper buyers without displacing Product, Trust, Changelog, or Contact from the core journey."
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
@ -28,8 +28,8 @@ import {
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Operating models"
|
||||
title="Review MSP and enterprise fit without changing the product story."
|
||||
description="Visitors should be able to recognize themselves in the page quickly, without translating a generic story into their own workflow."
|
||||
title="Keep audience fit visible without promoting this page into the core route set."
|
||||
description="Visitors should be able to recognize themselves here quickly without needing this page to understand the initial product story."
|
||||
/>
|
||||
<Grid cols="2">
|
||||
{solutionsAudiences.map((item) => <AudienceRow item={item} />)}
|
||||
@ -47,9 +47,9 @@ import {
|
||||
|
||||
<CTASection
|
||||
eyebrow="Continue"
|
||||
title="Inspect the ecosystem fit after you understand the audience fit."
|
||||
description="Once a visitor sees the product reflected in their operating model, the next useful question is how it fits the surrounding Microsoft tenant environment."
|
||||
primary={{ href: '/integrations', label: 'Review the ecosystem fit' }}
|
||||
secondary={{ href: '/contact', label: 'Talk through your evaluation path', variant: 'secondary' }}
|
||||
title="Route deeper readers back into the core product and trust path."
|
||||
description="Once a visitor sees the audience fit, the next useful moves are to revisit the product model, inspect trust, or start a working session."
|
||||
primary={{ href: '/contact', label: 'Start the working session' }}
|
||||
secondary={{ href: '/trust', label: 'Review the trust posture', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
@ -23,9 +23,9 @@ import { termsHero, termsSections, termsSeo } from '@/content/pages/terms';
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Move back into privacy or contact once the website terms are clear."
|
||||
title="Move back into privacy, trust, or contact once the website terms are clear."
|
||||
description="The legal path should stay connected to the rest of the evaluation journey."
|
||||
primary={{ href: '/contact', label: 'Return to contact' }}
|
||||
secondary={{ href: '/privacy', label: 'Review privacy', variant: 'secondary' }}
|
||||
secondary={{ href: '/imprint', label: 'Review the imprint', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
|
||||
50
apps/website/src/pages/trust.astro
Normal file
50
apps/website/src/pages/trust.astro
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
import Callout from '@/components/content/Callout.astro';
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Grid from '@/components/primitives/Grid.astro';
|
||||
import Section from '@/components/primitives/Section.astro';
|
||||
import SectionHeader from '@/components/primitives/SectionHeader.astro';
|
||||
import CTASection from '@/components/sections/CTASection.astro';
|
||||
import PageHero from '@/components/sections/PageHero.astro';
|
||||
import TrustGrid from '@/components/sections/TrustGrid.astro';
|
||||
import { trustHero, trustNotes, trustPrinciples, trustSeo } from '@/content/pages/trust';
|
||||
---
|
||||
|
||||
<PageShell currentPath="/trust" title={trustSeo.title} description={trustSeo.description}>
|
||||
<PageHero
|
||||
hero={trustHero}
|
||||
calloutTitle="Trust belongs on one explicit surface."
|
||||
calloutDescription="Public trust language should point back to operator safeguards, access boundaries, and explicit limits instead of being implied across unrelated marketing sections."
|
||||
/>
|
||||
|
||||
<TrustGrid
|
||||
eyebrow="Operator safeguards"
|
||||
title="Ground public trust claims in operator safeguards and explicit boundaries."
|
||||
description="The Trust page should explain the guardrails that matter to a serious buyer: tenant isolation, bounded access, safer restore posture, and operating discipline."
|
||||
items={trustPrinciples}
|
||||
/>
|
||||
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public messaging"
|
||||
title="Keep the public trust story narrower than the internal roadmap."
|
||||
description="A trust page becomes more credible when it explains what is supported now and what language is intentionally bounded."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
{trustNotes.map((note) => <Callout content={note} />)}
|
||||
</Grid>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Trust review should still lead to a real conversation."
|
||||
description="A visitor evaluating isolation, access handling, or operating discipline should be able to move directly into the contact path or back to dated product progress."
|
||||
primary={{ href: '/contact', label: 'Start the working session' }}
|
||||
secondary={{ href: '/changelog', label: 'Read the changelog', variant: 'secondary' }}
|
||||
/>
|
||||
</PageShell>
|
||||
@ -3,25 +3,34 @@ export type PageFamily = 'landing' | 'trust' | 'content';
|
||||
export type PageRole =
|
||||
| 'home'
|
||||
| 'product'
|
||||
| 'solutions'
|
||||
| 'trust'
|
||||
| 'integrations'
|
||||
| 'changelog'
|
||||
| 'contact'
|
||||
| 'legal'
|
||||
| 'privacy'
|
||||
| 'imprint'
|
||||
| 'solutions'
|
||||
| 'integrations'
|
||||
| 'legal'
|
||||
| 'terms';
|
||||
export type SitePath =
|
||||
| '/'
|
||||
| '/product'
|
||||
| '/solutions'
|
||||
| '/security-trust'
|
||||
| '/trust'
|
||||
| '/changelog'
|
||||
| '/integrations'
|
||||
| '/solutions'
|
||||
| '/contact'
|
||||
| '/legal'
|
||||
| '/privacy'
|
||||
| '/terms';
|
||||
| '/imprint'
|
||||
| '/terms'
|
||||
| '/security-trust';
|
||||
export type ShellTone = 'brand' | 'neutral' | 'trust';
|
||||
export type FooterIntent = 'conversion' | 'guidance' | 'legal';
|
||||
export type SurfacePriority = 'required' | 'recommended' | 'secondary' | 'compatibility';
|
||||
export type JourneyStage = 'entry' | 'first-clarification' | 'deepening' | 'action';
|
||||
export type SurfaceGroup = 'core' | 'supporting' | 'legal' | 'compatibility';
|
||||
export type CollectionName = 'articles' | 'changelog' | 'resources';
|
||||
|
||||
export interface CtaLink {
|
||||
href: string;
|
||||
@ -37,6 +46,12 @@ export interface NavigationItem {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceAvailability {
|
||||
articles: boolean;
|
||||
changelog: boolean;
|
||||
resources: boolean;
|
||||
}
|
||||
|
||||
export interface FooterNavigationGroup {
|
||||
items: NavigationItem[];
|
||||
title: string;
|
||||
@ -51,12 +66,17 @@ export interface FooterLead {
|
||||
}
|
||||
|
||||
export interface PageDefinition {
|
||||
canonicalPath: SitePath;
|
||||
family: PageFamily;
|
||||
footerLead?: Partial<FooterLead>;
|
||||
headerCta?: Partial<CtaLink>;
|
||||
inSitemap: boolean;
|
||||
journeyStage: JourneyStage;
|
||||
pageRole: PageRole;
|
||||
path: SitePath;
|
||||
priority: SurfacePriority;
|
||||
shellTone: ShellTone;
|
||||
surfaceGroup: SurfaceGroup;
|
||||
}
|
||||
|
||||
export interface SiteMetadata {
|
||||
|
||||
40
apps/website/tests/smoke/changelog-core-ia.spec.ts
Normal file
40
apps/website/tests/smoke/changelog-core-ia.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('changelog publishes dated progress without displacing the contact path', async ({ page }) => {
|
||||
await visitPage(page, '/changelog');
|
||||
await expectShell(page, /changelog|product progress|visible progress/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(page.getByText('Initial core pages and IA realignment')).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Start the working session' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('core IA keeps optional and deferred surfaces out of the published navigation contract', async ({ page }) => {
|
||||
await visitPage(page, '/');
|
||||
|
||||
const header = page.getByRole('banner');
|
||||
const footer = page.getByRole('contentinfo');
|
||||
|
||||
await expect(header.getByRole('link', { name: 'Resources' })).toHaveCount(0);
|
||||
await expect(header.getByRole('link', { name: 'Solutions' })).toHaveCount(0);
|
||||
await expect(header.getByRole('link', { name: 'Integrations' })).toHaveCount(0);
|
||||
await expect(header.getByRole('link', { name: 'Security & Trust' })).toHaveCount(0);
|
||||
|
||||
await expect(footer.getByRole('link', { name: 'Resources' })).toHaveCount(0);
|
||||
await expect(footer.getByRole('link', { name: 'Articles' })).toHaveCount(0);
|
||||
await expect(footer.getByRole('link', { name: 'Security & Trust' })).toHaveCount(0);
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
coreRoutePaths,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
@ -11,11 +12,9 @@ import {
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
const coreRoutes = ['/', '/product', '/solutions', '/security-trust', '/integrations', '/contact'] as const;
|
||||
|
||||
test('contact page qualifies the conversation and keeps legal links reachable', async ({ page }) => {
|
||||
await visitPage(page, '/contact');
|
||||
await expectShell(page, 'Start a qualified working session instead of a generic demo request.');
|
||||
await expectShell(page, /working session|contact path|qualified/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
@ -28,33 +27,40 @@ test('contact page qualifies the conversation and keeps legal links reachable',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Email the TenantAtlas team' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Review the trust posture' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Imprint' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('legal, privacy, and terms routes are published and linked', async ({ page }) => {
|
||||
test('legal, privacy, imprint, and terms routes are published and linked', async ({ page }) => {
|
||||
await visitPage(page, '/legal');
|
||||
await expectShell(page, 'Legal access should stay one click away from the contact path.');
|
||||
await expectPageFamily(page, 'trust');
|
||||
await expectShell(page, /Legal access should stay one click away from the trust and contact path\./);
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Use one legal surface for privacy, terms, and notice routing.' }),
|
||||
page.getByRole('heading', { name: 'Keep the legal baseline explicit without promoting it into the main navigation.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Trust' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Imprint' }).first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/privacy');
|
||||
await expectShell(page, 'Public-site privacy overview for TenantAtlas inquiries.');
|
||||
await expectShell(page, /Public-site privacy overview|privacy/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
|
||||
await visitPage(page, '/imprint');
|
||||
await expectShell(page, /Imprint|legal notice/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
|
||||
await visitPage(page, '/terms');
|
||||
await expectShell(page, 'Website terms for the public TenantAtlas surface.');
|
||||
await expectShell(page, /Website terms|terms/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
});
|
||||
|
||||
test('core pages keep contact and legal paths within reach', async ({ page }) => {
|
||||
for (const path of coreRoutes) {
|
||||
for (const path of coreRoutePaths) {
|
||||
await visitPage(page, path);
|
||||
await expectFooterLinks(page);
|
||||
|
||||
@ -72,7 +78,10 @@ test.describe('mobile navigation', () => {
|
||||
await openMobileNavigation(page);
|
||||
await expect(page.locator('[data-mobile-nav]').first()).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('link', { name: /Contact/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('link', { name: 'Trust' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('link', { name: 'Changelog' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Privacy' })).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Terms' })).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Imprint' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -24,10 +24,12 @@ test('home uses the landing foundation to explain the product category with one
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: 'Carry one visual language from product orientation into proof.',
|
||||
name: 'Understand the product, the trust posture, and the next step without route sprawl.',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expectCtaHierarchy(page, 'See the product model', 'Review the trust posture');
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the changelog' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Start the working session' }).first()).toBeVisible();
|
||||
|
||||
const skipLink = page.getByRole('link', { name: 'Skip to content' });
|
||||
|
||||
@ -39,7 +41,7 @@ test('product keeps the connected operating model readable without collapsing in
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/product');
|
||||
await expectShell(page, 'One operating model for change history, drift visibility, and review readiness.');
|
||||
await expectShell(page, /operating model|restore posture|governance/i);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
@ -47,7 +49,8 @@ test('product keeps the connected operating model readable without collapsing in
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Make the operating model legible before the feature list.' }),
|
||||
page.getByRole('heading', { name: 'Explain what the product does before asking for buyer trust.' }),
|
||||
).toBeVisible();
|
||||
await expectCtaHierarchy(page, 'See audience fit', 'Talk through your current operating model');
|
||||
await expectCtaHierarchy(page, 'Review the trust posture', 'Start the working session');
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the changelog' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
@ -1,20 +1,31 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export const primaryNavigationLabels = [
|
||||
'Product',
|
||||
export const coreRoutePaths = ['/', '/product', '/trust', '/changelog', '/contact', '/privacy', '/imprint'] as const;
|
||||
export const secondaryRoutePaths = ['/legal', '/terms', '/solutions', '/integrations'] as const;
|
||||
|
||||
export const primaryNavigationLabels = ['Product', 'Trust', 'Changelog', 'Contact'] as const;
|
||||
export const hiddenPrimaryNavigationLabels = [
|
||||
'Solutions',
|
||||
'Security & Trust',
|
||||
'Integrations',
|
||||
'Contact',
|
||||
'Security & Trust',
|
||||
'Resources',
|
||||
'Articles',
|
||||
] as const;
|
||||
|
||||
export const footerLabels = ['Legal', 'Privacy', 'Terms', 'Contact / Demo'] as const;
|
||||
export const footerLabels = ['Product', 'Changelog', 'Trust', 'Privacy', 'Imprint', 'Terms', 'Contact'] as const;
|
||||
export const hiddenFooterLabels = ['Resources', 'Articles', 'Security & Trust', 'Contact / Demo'] as const;
|
||||
|
||||
export async function visitPage(page: Page, path: string): Promise<void> {
|
||||
await page.goto(path);
|
||||
await expect(page).toHaveURL(new RegExp(path === '/' ? '/?$' : `${path}$`));
|
||||
}
|
||||
|
||||
export async function expectCompatibilityRedirect(page: Page, legacyPath: string, canonicalPath: string): Promise<void> {
|
||||
await page.goto(legacyPath);
|
||||
await page.waitForURL(new RegExp(canonicalPath === '/' ? '/?$' : `${canonicalPath}$`));
|
||||
await expect(page).toHaveURL(new RegExp(canonicalPath === '/' ? '/?$' : `${canonicalPath}$`));
|
||||
}
|
||||
|
||||
export async function expectShell(page: Page, heading: string | RegExp): Promise<void> {
|
||||
await expect(page.getByRole('banner')).toBeVisible();
|
||||
await expect(page.getByRole('main')).toBeVisible();
|
||||
@ -30,16 +41,24 @@ export async function expectPrimaryNavigation(page: Page): Promise<void> {
|
||||
const header = page.getByRole('banner');
|
||||
|
||||
for (const label of primaryNavigationLabels) {
|
||||
const link = header.getByRole('link', { name: label }).first();
|
||||
const link = header.getByRole('link', { name: label, exact: true }).first();
|
||||
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute('data-nav-link');
|
||||
}
|
||||
|
||||
for (const label of hiddenPrimaryNavigationLabels) {
|
||||
await expect(header.getByRole('link', { name: label, exact: true })).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectFooterLinks(page: Page): Promise<void> {
|
||||
for (const label of footerLabels) {
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: label })).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: label, exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
for (const label of hiddenFooterLabels) {
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: label, exact: true })).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCompatibilityRedirect,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
@ -12,26 +13,26 @@ import {
|
||||
|
||||
test('solutions keeps MSP and enterprise audience fit inside one landing-page rhythm', async ({ page }) => {
|
||||
await visitPage(page, '/solutions');
|
||||
await expectShell(page, /MSP|enterprise/i);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectShell(page, /MSP|enterprise|outcome/i);
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Review MSP and enterprise fit without changing the product story.' }),
|
||||
page.getByRole('heading', { name: 'Keep audience fit visible without promoting this page into the core route set.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'MSP operating model' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Enterprise IT operating model' })).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Review the ecosystem fit' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'See the product model' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('security and trust stays grounded in substantiated product posture and layered disclosure', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/security-trust');
|
||||
await expectShell(page, /trust posture|trust-first/i);
|
||||
await visitPage(page, '/trust');
|
||||
await expectShell(page, /trust posture|trust|operating discipline/i);
|
||||
await expectPageFamily(page, 'trust');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
@ -39,22 +40,27 @@ test('security and trust stays grounded in substantiated product posture and lay
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Show operator safeguards with restrained public claims.' }),
|
||||
page.getByRole('heading', { name: 'Ground public trust claims in operator safeguards and explicit boundaries.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the legal surface' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Start the working session' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the imprint' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('legacy security trust route redirects to the canonical trust surface', async ({ page }) => {
|
||||
await expectCompatibilityRedirect(page, '/security-trust', '/trust');
|
||||
});
|
||||
|
||||
test('integrations shows real ecosystem direction without wishlist claims or shell drift', async ({ page }) => {
|
||||
await visitPage(page, '/integrations');
|
||||
await expectShell(page, /ecosystem fit|integrations/i);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Keep ecosystem direction sharp, current, and bounded.' }),
|
||||
page.getByRole('heading', { name: 'Keep ecosystem fit visible without pretending it belongs in primary navigation.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('Microsoft Graph')).toBeVisible();
|
||||
await expect(page.getByText('Entra ID')).toBeVisible();
|
||||
|
||||
@ -21,13 +21,13 @@ test('representative pages route CTA, badge, surface, and input semantics throug
|
||||
await expect(page.locator('[data-interaction="button"]').filter({ hasText: 'See the product model' }).first()).toBeVisible();
|
||||
await expect(page.locator('[data-badge-tone]').first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/security-trust');
|
||||
await expectShell(page, /trust posture|trust-first/i);
|
||||
await visitPage(page, '/trust');
|
||||
await expectShell(page, /trust posture|trust/i);
|
||||
await expect(page.locator('[data-surface="accent"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-badge-tone]').first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/contact');
|
||||
await expectShell(page, 'Start a qualified working session instead of a generic demo request.');
|
||||
await expectShell(page, /contact path|working session|qualified/i);
|
||||
await expect(page.locator('[data-interaction="input"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-interaction="textarea"]').first()).toBeVisible();
|
||||
await expect(
|
||||
|
||||
35
specs/215-website-core-pages/checklists/requirements.md
Normal file
35
specs/215-website-core-pages/checklists/requirements.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Specification Quality Checklist: Website Information Architecture / Core Pages
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-19
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Validation pass 1 completed on 2026-04-19.
|
||||
- The spec remains strictly local to `apps/website`; required repository-governance metadata does not introduce any platform obligation.
|
||||
@ -0,0 +1,257 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: TenantAtlas Public Website IA Contract
|
||||
version: 0.1.0
|
||||
summary: Canonical route and navigation contract for the initial `apps/website` core pages.
|
||||
description: >-
|
||||
This contract defines the required public HTML routes, navigation limits,
|
||||
optional-surface publication rules, and compatibility-path expectations for
|
||||
Spec 215. The feature does not add backend APIs; the contract is about the
|
||||
public website route and discoverability model.
|
||||
servers:
|
||||
- url: http://localhost:{port}
|
||||
description: Local Astro development or preview server
|
||||
variables:
|
||||
port:
|
||||
default: "4321"
|
||||
tags:
|
||||
- name: Core Public Pages
|
||||
description: Canonical public HTML routes required by the initial website IA
|
||||
- name: Secondary Public Pages
|
||||
description: Retained supporting routes that remain published without becoming part of the initial core IA
|
||||
- name: Compatibility Routes
|
||||
description: Temporary or secondary routes that preserve continuity during IA cleanup
|
||||
x-information-architecture:
|
||||
scope: apps/website
|
||||
requiredCoreRoutes:
|
||||
- /
|
||||
- /product
|
||||
- /trust
|
||||
- /changelog
|
||||
- /contact
|
||||
- /privacy
|
||||
- /imprint
|
||||
optionalRoutesWhenSubstantive:
|
||||
- /resources
|
||||
secondaryRoutes:
|
||||
- /legal
|
||||
- /terms
|
||||
- /solutions
|
||||
- /integrations
|
||||
deferredRoutes:
|
||||
- /pricing
|
||||
- /docs
|
||||
- /solutions/*
|
||||
- /customers
|
||||
- /compare
|
||||
- /careers
|
||||
- /status
|
||||
primaryNavigation:
|
||||
maxInformationalItems: 5
|
||||
required:
|
||||
- /product
|
||||
- /trust
|
||||
- /changelog
|
||||
- /contact
|
||||
optionalWhenSubstantive:
|
||||
- /resources
|
||||
forbiddenUntilMature:
|
||||
- /pricing
|
||||
- /docs
|
||||
primaryCta:
|
||||
canonicalRoute: /contact
|
||||
allowedLabels:
|
||||
- Request demo
|
||||
- Contact
|
||||
- Request a working session
|
||||
footerGroups:
|
||||
Product:
|
||||
- /product
|
||||
- /changelog
|
||||
TrustLegal:
|
||||
- /trust
|
||||
- /privacy
|
||||
- /imprint
|
||||
- /terms
|
||||
Contact:
|
||||
- /contact
|
||||
Content:
|
||||
- /resources
|
||||
publicationRules:
|
||||
trustTopLevelVisible: true
|
||||
brandRoutesHome: true
|
||||
placeholderRoutesForbidden: true
|
||||
unpublishedCollectionsRemainHidden:
|
||||
- articles
|
||||
buyerOutcomeExplanationRequiredOn:
|
||||
- /
|
||||
- /product
|
||||
compatibility:
|
||||
legacyCanonicalPairs:
|
||||
- legacy: /security-trust
|
||||
canonical: /trust
|
||||
strategy: redirect
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getHomePage
|
||||
summary: Home page
|
||||
description: Entry surface that frames the product, routes visitors toward Product and Trust, and exposes the primary next step.
|
||||
responses:
|
||||
"200":
|
||||
description: Home page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/product:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getProductPage
|
||||
summary: Product page
|
||||
description: Canonical product-understanding surface for capabilities, positioning, and buyer outcomes.
|
||||
responses:
|
||||
"200":
|
||||
description: Product page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/trust:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getTrustPage
|
||||
summary: Trust page
|
||||
description: Canonical trust surface for security posture, isolation, operational discipline, and bounded public claims.
|
||||
responses:
|
||||
"200":
|
||||
description: Trust page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/changelog:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getChangelogPage
|
||||
summary: Changelog page
|
||||
description: Dated product-progress surface that shows visible public development without acting as a blog substitute.
|
||||
responses:
|
||||
"200":
|
||||
description: Changelog page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/contact:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getContactPage
|
||||
summary: Contact page
|
||||
description: Primary public conversion surface for the next evaluation step. Any later demo route is secondary unless a future spec changes the primary conversion path.
|
||||
responses:
|
||||
"200":
|
||||
description: Contact page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/legal:
|
||||
get:
|
||||
tags: [Secondary Public Pages]
|
||||
operationId: getLegalIndexPage
|
||||
summary: Secondary legal index page
|
||||
description: Supporting legal overview that may remain published without being part of the required initial core.
|
||||
responses:
|
||||
"200":
|
||||
description: Legal index page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/privacy:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getPrivacyPage
|
||||
summary: Privacy page
|
||||
description: Canonical privacy disclosure required by the public legal baseline.
|
||||
responses:
|
||||
"200":
|
||||
description: Privacy page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/imprint:
|
||||
get:
|
||||
tags: [Core Public Pages]
|
||||
operationId: getImprintPage
|
||||
summary: Imprint page
|
||||
description: Canonical public legal notice required by the initial website legal baseline.
|
||||
responses:
|
||||
"200":
|
||||
description: Imprint page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/terms:
|
||||
get:
|
||||
tags: [Secondary Public Pages]
|
||||
operationId: getTermsPage
|
||||
summary: Secondary terms page
|
||||
description: Supporting legal disclosure that may remain published without becoming part of the required initial core.
|
||||
responses:
|
||||
"200":
|
||||
description: Terms page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/solutions:
|
||||
get:
|
||||
tags: [Secondary Public Pages]
|
||||
operationId: getSolutionsSupportPage
|
||||
summary: Secondary solutions support page
|
||||
description: Supporting outcome or audience-fit page that may remain published without top-level core prominence.
|
||||
responses:
|
||||
"200":
|
||||
description: Solutions page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/integrations:
|
||||
get:
|
||||
tags: [Secondary Public Pages]
|
||||
operationId: getIntegrationsSupportPage
|
||||
summary: Secondary integrations support page
|
||||
description: Supporting ecosystem-fit page that may remain published without top-level core prominence.
|
||||
responses:
|
||||
"200":
|
||||
description: Integrations page HTML
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HtmlDocument"
|
||||
/security-trust:
|
||||
get:
|
||||
tags: [Compatibility Routes]
|
||||
operationId: redirectLegacySecurityTrust
|
||||
summary: Legacy compatibility path for Trust
|
||||
description: Temporary compatibility route. The canonical public trust path is `/trust`.
|
||||
responses:
|
||||
"308":
|
||||
description: Permanent redirect to `/trust`
|
||||
headers:
|
||||
Location:
|
||||
description: Canonical Trust route
|
||||
schema:
|
||||
type: string
|
||||
const: /trust
|
||||
components:
|
||||
schemas:
|
||||
HtmlDocument:
|
||||
type: string
|
||||
description: Server-rendered static HTML document
|
||||
163
specs/215-website-core-pages/data-model.md
Normal file
163
specs/215-website-core-pages/data-model.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Data Model: Website Information Architecture / Core Pages
|
||||
|
||||
## Overview
|
||||
|
||||
This feature introduces no database schema. The model is a website-local route and discoverability contract expressed through page roles, navigation entries, publication rules, compatibility paths, and buyer-journey stages inside `apps/website`.
|
||||
|
||||
## Entities
|
||||
|
||||
### Website IA Contract
|
||||
|
||||
- **Purpose**: The canonical information-architecture contract for `apps/website`.
|
||||
- **Key fields**:
|
||||
- `scope` (`apps/website` only)
|
||||
- `requiredCoreRoutes`
|
||||
- `recommendedCoreRoutes`
|
||||
- `optionalSurfaceFamilies`
|
||||
- `deferredSurfaceFamilies`
|
||||
- `primaryConversionRoute`
|
||||
- `legalBaselineRoutes`
|
||||
- `maxPrimaryInformationalLinks`
|
||||
- **Relationships**:
|
||||
- Owns many `Public Surface` entries
|
||||
- Owns many `Navigation Entry` entries
|
||||
- Owns many `Footer Group` entries
|
||||
- Owns many `Content Publication Rule` entries
|
||||
- Owns many `Compatibility Path` entries
|
||||
- Owns many `Journey Stage` entries
|
||||
- **Validation rules**:
|
||||
- Must remain explicitly local to `apps/website`
|
||||
- Must keep Trust top-level visible
|
||||
- Must keep one clear primary conversion route
|
||||
- Must keep primary navigation at or below 5 informational links plus one CTA
|
||||
- Must forbid placeholder or thin-content top-level surfaces
|
||||
|
||||
### Public Surface
|
||||
|
||||
- **Purpose**: A named public page or route that performs one clear job in the website journey.
|
||||
- **Key fields**:
|
||||
- `path`
|
||||
- `role` (`home`, `product`, `trust`, `changelog`, `contact`, `privacy`, `imprint`, `secondary-supporting`, `secondary-legal`, `compatibility`)
|
||||
- `priority` (`required`, `recommended`, `optional`, `deferred`, `compatibility`)
|
||||
- `family` (`landing`, `trust`, `content`)
|
||||
- `jobStatement`
|
||||
- `buyerQuestionsAnswered`
|
||||
- `canonicalStatus` (`canonical`, `conditional`, `legacy`)
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- May appear in many `Navigation Entry` and `Footer Group` entries
|
||||
- May satisfy one or more `Journey Stage` entries
|
||||
- May be gated by one `Content Publication Rule`
|
||||
- **Validation rules**:
|
||||
- Every required surface must have a named job in the buyer journey
|
||||
- Required surfaces must be publishable without relying on deferred surfaces
|
||||
- Optional surfaces cannot appear in primary navigation without passing content-readiness rules
|
||||
- Compatibility surfaces must never become a second canonical truth
|
||||
|
||||
### Navigation Entry
|
||||
|
||||
- **Purpose**: A visible public link in the header, footer, brand, or CTA slot.
|
||||
- **Key fields**:
|
||||
- `location` (`brand`, `primary-nav`, `footer`, `primary-cta`, `secondary-cta`)
|
||||
- `label`
|
||||
- `href`
|
||||
- `prominence` (`informational`, `cta`, `legal`, `secondary`)
|
||||
- `visibilityRule`
|
||||
- `sourceSurfaceRole`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- References one `Public Surface`
|
||||
- May belong to one `Footer Group`
|
||||
- **Validation rules**:
|
||||
- Brand entry must route to `/`
|
||||
- Trust must have a visible `primary-nav` entry
|
||||
- Only one `primary-cta` route may be primary at a time
|
||||
- No `primary-nav` entry may point to a placeholder or deferred surface
|
||||
|
||||
### Footer Group
|
||||
|
||||
- **Purpose**: A semantic grouping of footer links that reinforces the public IA without inflating header navigation.
|
||||
- **Key fields**:
|
||||
- `title`
|
||||
- `purpose` (`product`, `trust-legal`, `contact`, `content`)
|
||||
- `items`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- Contains many `Navigation Entry` items
|
||||
- **Validation rules**:
|
||||
- Footer must expose Product, Trust/Legal, and Contact discoverability
|
||||
- Privacy and Imprint must remain directly reachable from the footer
|
||||
- Content groups such as `Resources`, later editorial surfaces, or Docs may only appear when those surfaces are actually published
|
||||
|
||||
### Content Publication Rule
|
||||
|
||||
- **Purpose**: The rule that determines whether an optional public surface becomes discoverable.
|
||||
- **Key fields**:
|
||||
- `surfaceFamily` (`resources`, `blog-editorial`, `docs`, `pricing`)
|
||||
- `contentSource`
|
||||
- `minimumSubstanceRule`
|
||||
- `primaryNavAllowed`
|
||||
- `footerAllowed`
|
||||
- `fallbackBehavior`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- May gate one or more `Public Surface` entries
|
||||
- **Validation rules**:
|
||||
- `Resources` may not be promoted until substantive content exists
|
||||
- The existing `articles` collection remains unpublished as `blog-editorial` inventory until a separate spec activates it
|
||||
- Docs and Pricing remain deferred until a later spec activates them
|
||||
- Changelog is exempt from optionality but still requires dated, substantive entries rather than an empty route
|
||||
|
||||
### Compatibility Path
|
||||
|
||||
- **Purpose**: A temporary or secondary path that preserves continuity during IA changes.
|
||||
- **Key fields**:
|
||||
- `legacyPath`
|
||||
- `canonicalPath`
|
||||
- `strategy` (`redirect`, `secondary`, `retire`)
|
||||
- `reason`
|
||||
- `expiryIntent`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- References one canonical `Public Surface`
|
||||
- **Validation rules**:
|
||||
- Compatibility paths must not appear in primary navigation
|
||||
- Compatibility paths should not stay permanent without a separate justification
|
||||
- Sitemap and canonical-link generation must point to the canonical route, not the legacy alias
|
||||
|
||||
### Journey Stage
|
||||
|
||||
- **Purpose**: The required stage in the public buyer journey that the IA must support.
|
||||
- **Key fields**:
|
||||
- `stage` (`entry`, `first-clarification`, `deepening`, `action`)
|
||||
- `primaryQuestion`
|
||||
- `allowedSurfaceRoles`
|
||||
- `requiredTransitions`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website IA Contract`
|
||||
- References many `Public Surface` roles
|
||||
- **Validation rules**:
|
||||
- Entry surfaces must route visitors to Product, Trust, Changelog, or Contact without dead ends
|
||||
- Deepening surfaces must preserve a path to the primary conversion route
|
||||
- Outcome explanation must be present before or during the first-clarification stage, not deferred to a later optional hub
|
||||
|
||||
## Relationship Summary
|
||||
|
||||
- `Website IA Contract` owns many `Public Surface`
|
||||
- `Website IA Contract` owns many `Navigation Entry`
|
||||
- `Website IA Contract` owns many `Footer Group`
|
||||
- `Website IA Contract` owns many `Content Publication Rule`
|
||||
- `Website IA Contract` owns many `Compatibility Path`
|
||||
- `Website IA Contract` owns many `Journey Stage`
|
||||
- `Navigation Entry` references one `Public Surface`
|
||||
- `Footer Group` contains many `Navigation Entry`
|
||||
- `Content Publication Rule` gates one or more optional `Public Surface`
|
||||
- `Compatibility Path` points to one canonical `Public Surface`
|
||||
- `Journey Stage` is satisfied by one or more `Public Surface` roles
|
||||
|
||||
## State / Lifecycle Notes
|
||||
|
||||
- No persisted runtime state is introduced.
|
||||
- The IA is repo-owned truth expressed through route files, route metadata, navigation config, and content publication decisions.
|
||||
- A surface becomes public when its publication rules are satisfied and it is included in the canonical navigation/footer contract.
|
||||
- Compatibility paths are transitional by design and should shrink over time rather than becoming a permanent parallel IA.
|
||||
220
specs/215-website-core-pages/plan.md
Normal file
220
specs/215-website-core-pages/plan.md
Normal file
@ -0,0 +1,220 @@
|
||||
# Implementation Plan: Website Information Architecture / Core Pages
|
||||
|
||||
**Branch**: `215-website-core-pages` | **Date**: 2026-04-19 | **Spec**: `specs/215-website-core-pages/spec.md`
|
||||
**Input**: Feature specification from `specs/215-website-core-pages/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||
|
||||
## Summary
|
||||
|
||||
- Keep `apps/website` fully local to the website track and preserve `@tenantatlas/website`, `WEBSITE_PORT`, and the root `dev:website` / `build:website` workflows.
|
||||
- Re-anchor the public-site IA in the existing Astro route and metadata layer (`src/lib/site.ts`, `src/types/site.ts`, `src/content/pages`, and published Astro pages) instead of introducing a CMS, router framework, or any `apps/platform` coupling.
|
||||
- Canonicalize the initial public core around Home, Product, Trust, Changelog, Contact, Privacy, and Imprint; keep one primary conversion path; standardize optional content discoverability on `Resources`; leave the editorial `articles` collection unpublished; and keep Pricing/Docs/Solutions-hub style expansion deferred.
|
||||
- Reconcile the current v0 topology with compatibility-safe changes by introducing `/trust`, `/changelog`, and `/imprint`, shrinking primary navigation to the Spec 215 core, and extending smoke coverage for the new IA contract.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Astro 6.0.0 templates + TypeScript 5.9 strict
|
||||
**Primary Dependencies**: Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro layout/primitive/content helpers, Playwright smoke tests
|
||||
**Storage**: Static filesystem pages, content modules, and Astro content collections under `apps/website/src` and `apps/website/public`; no database
|
||||
**Testing**: Root build proof via `corepack pnpm build:website` plus Playwright smoke coverage in `apps/website/tests/smoke`
|
||||
**Validation Lanes**: fast-feedback
|
||||
**Target Platform**: Static public website for modern desktop and mobile browsers
|
||||
**Project Type**: Web (standalone Astro app inside the monorepo)
|
||||
**Performance Goals**: Preserve static HTML output for canonical public routes, keep browsing flows zero-hydration by default, and avoid introducing JS-heavy navigation or runtime platform coupling
|
||||
**Constraints**: Preserve `@tenantatlas/website`, `WEBSITE_PORT`, and Astro static output mode; keep all IA changes local to `apps/website`; do not publish placeholder routes; keep one clear primary conversion path; keep Trust top-level visible; avoid premature promotion of `Resources`, later editorial surfaces, Docs, or Pricing
|
||||
**Scale/Scope**: Current public site ships 9 routes, 3 future content collections (`articles` as unpublished editorial inventory, `changelog`, `resources`), one route-definition source in `src/lib/site.ts`, and a small Playwright smoke suite that must expand to cover the Spec 215 IA contract
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: no operator-facing surface change
|
||||
- **Native vs custom classification summary**: N/A - public Astro website only
|
||||
- **Shared-family relevance**: none
|
||||
- **State layers in scope**: none
|
||||
- **Handling modes by drift class or surface**: N/A
|
||||
- **Repository-signal treatment**: report-only
|
||||
- **Special surface test profiles**: N/A
|
||||
- **Required tests or manual smoke**: manual-smoke plus browser smoke for public routes
|
||||
- **Exception path and spread control**: none
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first / Graph contract / deterministic capabilities / RBAC-UX / Filament surface rules: N/A for this feature because all work stays inside `apps/website` and introduces no `/admin`, `/admin/t/{tenant}/...`, or `/system` runtime behavior.
|
||||
- Read/write separation: Pass. The feature changes public route structure, navigation, and content discoverability only; Contact remains a public conversion surface, not a website-side backend workflow.
|
||||
- Workspace isolation: Pass. The website remains runtime-independent from `apps/platform`, and the plan preserves the explicit website working contract.
|
||||
- Data minimization: Pass. The feature only reorganizes public pages, content sources, and navigation metadata; it introduces no tenant data, secrets, auth state, or operational records.
|
||||
- Test governance (TEST-GOV-001): Pass. Validation stays in `fast-feedback` with build proof plus the local Playwright smoke suite, with no database, auth, provider, or heavy-suite defaults.
|
||||
- Proportionality / no premature abstraction: Pass. The plan reuses the existing `site.ts`, `types/site.ts`, Astro page routes, and content collections instead of introducing a CMS, server route layer, or generic routing framework.
|
||||
- Persisted truth / new state: Pass. No database schema, queued work, new domain state family, or persisted artifact is introduced.
|
||||
- UI semantics / few layers: Pass. The added semantics stay limited to website-local route roles, navigation groups, publication rules, and journey flow rather than becoming a cross-app framework.
|
||||
- Website working contract: Pass. The plan keeps all implementation local to `apps/website` and preserves `@tenantatlas/website`, `WEBSITE_PORT`, and root workflow compatibility.
|
||||
|
||||
Status: ✅ No constitution violations for this feature. The plan remains website-only, static-first, and scoped to the existing Astro website track.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
> **Fill for any runtime-changing or test-affecting feature. Docs-only or template-only work may state concise `N/A` or `none`.**
|
||||
|
||||
- **Test purpose / classification by changed surface**: Browser smoke coverage for required public routes, navigation/footer topology, compatibility routing, and optional-surface suppression plus static build proof
|
||||
- **Affected validation lanes**: fast-feedback
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The feature changes the runtime route topology and public-shell behavior of a static Astro site. Build proof catches route and artifact generation regressions; focused Playwright smoke coverage is the smallest realistic browser-level proof for link reachability, canonical paths, and the absence of placeholder navigation without adding backend or heavy end-to-end cost.
|
||||
- **Narrowest proving command(s)**: `corepack pnpm build:website` and `cd apps/website && corepack pnpm exec playwright test`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: none; public website pages require no database, auth, tenant, or provider setup
|
||||
- **Expensive defaults or shared helper growth introduced?**: no; any smoke-helper changes remain local to `apps/website/tests/smoke`
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||
- **Surface-class relief / special coverage rule**: N/A
|
||||
- **Closing validation and reviewer handoff**: Re-run the website build and smoke suite after route, shell, or navigation changes. Reviewers should verify that `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, and `/imprint` load; `/trust` and `/changelog` can still route visitors to `/contact` within the intended action flow; Trust is top-level visible; the brand links home; one primary CTA remains obvious; unpublished optional content hubs stay hidden; and any legacy route compatibility is explicit rather than silently duplicated.
|
||||
- **Budget / baseline / trend follow-up**: none beyond small website smoke-suite growth
|
||||
- **Review-stop questions**: Did any route rename break sitemap/canonical output? Did optional surfaces leak into primary nav without content? Did a compatibility path become permanent instead of transitional? Did any change introduce platform coupling or hidden runtime cost?
|
||||
- **Escalation path**: document-in-feature
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **Why no dedicated follow-up spec is needed**: The change is bounded to a small public-route contract and website shell behavior. The later page-structure specs handle content/detail work, so route and navigation proof can remain inside this feature.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/215-website-core-pages/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── public-site-ia.openapi.yaml
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
apps/website/
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
├── playwright.config.ts
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── layout/ # Navbar, Footer, PageShell
|
||||
│ │ ├── primitives/ # Container, Section, Button, Card, Input, Textarea, Grid, Stack
|
||||
│ │ ├── sections/ # PageHero, FeatureGrid, TrustGrid, CTASection, LogoStrip
|
||||
│ │ └── content/ # CTA wrappers, callouts, text blocks
|
||||
│ ├── content/
|
||||
│ │ ├── changelog/ # Existing future collection for dated updates
|
||||
│ │ ├── pages/ # Route-level content definitions
|
||||
│ │ └── resources/ # Existing optional future collection
|
||||
│ ├── content.config.ts
|
||||
│ ├── layouts/
|
||||
│ │ └── BaseLayout.astro
|
||||
│ ├── lib/
|
||||
│ │ ├── seo.ts
|
||||
│ │ └── site.ts # Current route, shell, and navigation truth
|
||||
│ ├── pages/
|
||||
│ │ ├── index.astro
|
||||
│ │ ├── product.astro
|
||||
│ │ ├── contact.astro
|
||||
│ │ ├── privacy.astro
|
||||
│ │ ├── solutions.astro
|
||||
│ │ ├── integrations.astro
|
||||
│ │ ├── security-trust.astro
|
||||
│ │ ├── legal.astro
|
||||
│ │ ├── terms.astro
|
||||
│ │ └── sitemap.xml.ts
|
||||
│ ├── styles/
|
||||
│ └── types/
|
||||
└── tests/
|
||||
└── smoke/
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the existing website structure and implement the IA in the current Astro route stack. The core truth should live in `src/lib/site.ts`, `src/types/site.ts`, `src/content/pages`, and published page files. Add canonical route files for `/trust`, `/changelog`, and `/imprint`, and use compatibility-only routes only when a rename is necessary to avoid avoidable breakage.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
None.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: The current website still reflects the broader v0 route inventory, with `Solutions` and `Integrations` acting as primary-nav peers, `Security & Trust` using a non-canonical path, and no `/changelog` or `/imprint` routes even though Spec 215 treats those as part of the small credible core.
|
||||
- **Existing structure is insufficient because**: `src/lib/site.ts` and `src/types/site.ts` currently encode one flat route inventory but do not distinguish between required core surfaces, optional content surfaces, deferred surfaces, and compatibility paths. That makes it too easy for template-era routes to keep occupying top-level attention.
|
||||
- **Narrowest correct implementation**: Reuse the existing route-definition layer and content collections to encode the new IA contract, publish only the required core routes, and tighten smoke coverage around route topology and navigation behavior. Do not introduce a CMS, API contract, or platform/shared routing abstraction.
|
||||
- **Ownership cost created**: Ongoing maintenance of the explicit public IA contract, a small set of compatibility decisions for renamed routes, and lightweight smoke assertions that protect navigation and route truth.
|
||||
- **Alternative intentionally rejected**: Keeping the current v0 topology and trying to solve the IA only through page copy was rejected because it leaves the route and navigation contract incoherent; adding a richer content/router system was rejected because the website does not need that extra ownership cost.
|
||||
- **Release truth**: Current-release truth for `apps/website`
|
||||
|
||||
## Phase 0 — Outline & Research (complete)
|
||||
|
||||
- Output: `specs/215-website-core-pages/research.md`
|
||||
- Key decisions captured:
|
||||
- Keep the IA fully local to `apps/website` and preserve the website working contract.
|
||||
- Use the existing `src/lib/site.ts` + `src/types/site.ts` route-definition layer as the canonical public IA source of truth.
|
||||
- Canonicalize the required public core around `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, and `/imprint`, with compatibility handling only where renames are necessary.
|
||||
- Gate optional `Resources` discoverability through the existing Astro content collections, and keep the editorial `articles` collection unpublished until a later spec activates it.
|
||||
- Reclassify current `Solutions`, `Integrations`, `Legal`, and `Terms` surfaces as secondary supporting routes so primary navigation stays buyer-first and intentionally small.
|
||||
- Validate with build proof plus route- and navigation-focused Playwright smoke coverage.
|
||||
|
||||
## Phase 1 — Design & Contracts (complete)
|
||||
|
||||
### Data model
|
||||
|
||||
- Output: `specs/215-website-core-pages/data-model.md`
|
||||
- No database schema changes are required; the model is a website-local route, navigation, publication, and compatibility contract.
|
||||
|
||||
### Public IA contract
|
||||
|
||||
- Output: `specs/215-website-core-pages/contracts/public-site-ia.openapi.yaml`
|
||||
- The contract captures required public HTML routes, navigation invariants, optional-surface gating, and compatibility-path expectations for the initial website IA.
|
||||
|
||||
### Quickstart
|
||||
|
||||
- Output: `specs/215-website-core-pages/quickstart.md`
|
||||
- Quickstart covers the local development flow, implementation order, compatibility expectations, and required validation commands.
|
||||
|
||||
### Agent context update
|
||||
|
||||
- Completed via `.specify/scripts/bash/update-agent-context.sh copilot` so the Copilot context reflects the current planning artifacts.
|
||||
|
||||
### Constitution re-check (post-design)
|
||||
|
||||
- ✅ The design remains fully local to `apps/website` and preserves the website working contract.
|
||||
- ✅ No database truth, backend form handling, queueing, auth, or platform-side runtime concern is introduced.
|
||||
- ✅ The IA layer remains narrow: route roles, navigation groups, publication rules, and compatibility handling only.
|
||||
- ✅ Validation remains cheap, representative, and website-specific.
|
||||
|
||||
## Phase 2 — Implementation Plan (next)
|
||||
|
||||
### Story 1 (P1): Canonical core-route contract
|
||||
|
||||
- Update `src/types/site.ts`, `src/lib/site.ts`, `src/content/pages`, and the published Astro route files so the canonical public core is `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, and `/imprint`.
|
||||
- Add the missing `/changelog` and `/imprint` surfaces and replace `/security-trust` as the canonical trust path with `/trust`.
|
||||
- Keep compatibility routes narrow and explicit; any legacy route retained during migration must stay out of primary navigation and must not become a second canonical truth.
|
||||
- Tests / validation:
|
||||
- Update smoke helpers and assertions to reflect the new canonical navigation labels and route set.
|
||||
- Verify sitemap/canonical behavior and route reachability through build proof plus browser smoke tests.
|
||||
|
||||
### Story 2 (P1): Navigation and footer reduction to the Spec 215 core
|
||||
|
||||
- Shrink primary navigation to Product, Trust, Changelog, Contact, and optional `Resources` only when content is substantive.
|
||||
- Move buyer-outcome explanation responsibility into Home and Product so `Solutions` is no longer required as a top-level peer; treat `Solutions` and `Integrations` as retained secondary supporting pages instead of core IA pillars.
|
||||
- Rebuild footer groups around Product, Trust/Legal, Contact, and optional Content, with direct links to `/privacy`, `/imprint`, and the retained secondary `/terms` legal disclosure.
|
||||
- Tests / validation:
|
||||
- Assert brand-to-home behavior, Trust top-level visibility, `/trust -> /contact` reachability, one clear primary CTA, and the new footer grouping.
|
||||
- Add negative assertions that placeholder or deferred routes do not appear in primary navigation.
|
||||
|
||||
### Story 3 (P2): Content-backed updates and optional-surface gating
|
||||
|
||||
- Use the existing `src/content/changelog` collection or equivalent dated entries to make `/changelog` a real progress surface rather than a placeholder.
|
||||
- Keep `/resources` unpublished or unlinked until content exists, keep the editorial `articles` collection unpublished until a later blog/editorial spec, and keep Docs and Pricing out of primary navigation until later specs explicitly activate them.
|
||||
- Rationalize current `/legal` and `/terms` behavior as retained secondary legal surfaces, and current `/solutions` and `/integrations` behavior as retained secondary supporting pages, without letting them inflate the initial IA.
|
||||
- Tests / validation:
|
||||
- Extend smoke coverage for `/changelog`, `/imprint`, and `/changelog -> /contact` reachability.
|
||||
- Verify unpublished optional surfaces stay hidden, the editorial `articles` collection remains undiscoverable, and any compatibility or retained secondary paths stay out of primary navigation.
|
||||
|
||||
## Validation Evidence
|
||||
|
||||
- **Lane**: `fast-feedback`
|
||||
- **Build proof**: `corepack pnpm build:website` completed successfully on 2026-04-19 after the final IA/content changes.
|
||||
- **Browser smoke proof**: `cd apps/website && corepack pnpm exec playwright test` completed successfully on 2026-04-19 with 13 passing smoke tests.
|
||||
- **Reviewer note**: If an Astro dev server is already running on `WEBSITE_PORT`, stop it before rerunning Playwright so the suite does not reuse stale content state.
|
||||
89
specs/215-website-core-pages/quickstart.md
Normal file
89
specs/215-website-core-pages/quickstart.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Quickstart: Website Information Architecture / Core Pages
|
||||
|
||||
## Purpose
|
||||
|
||||
This quickstart describes the local workflow for implementing the Spec 215 public-route and navigation contract in `apps/website`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node 20+ available through the repo workspace tooling
|
||||
- Corepack-enabled pnpm
|
||||
- Repo root at `wt-website`
|
||||
|
||||
## Local Development
|
||||
|
||||
Start the website from the repo root:
|
||||
|
||||
```bash
|
||||
corepack pnpm dev:website
|
||||
```
|
||||
|
||||
The website must continue to honor the existing `WEBSITE_PORT` contract.
|
||||
|
||||
## Expected Implementation Order
|
||||
|
||||
Implement the feature in this order:
|
||||
|
||||
1. Audit the current public-route truth in `apps/website/src/lib/site.ts`, `apps/website/src/types/site.ts`, `apps/website/src/content/pages`, and `apps/website/src/pages`.
|
||||
2. Update the route and navigation contract so the canonical core is `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, and `/imprint`.
|
||||
3. Add the missing core pages (`/changelog`, `/imprint`) and replace `/security-trust` as the canonical trust route with `/trust`.
|
||||
4. Shrink header and footer navigation to the Spec 215 core, keeping only one primary conversion route, gating optional `Resources` links on actual content readiness, and leaving the `articles` collection unpublished.
|
||||
5. Rationalize current non-core routes such as `/solutions`, `/integrations`, `/legal`, and `/terms` as retained secondary surfaces without giving them core-route prominence.
|
||||
6. Update sitemap, canonical route generation, and smoke tests to reflect the new IA contract.
|
||||
7. Re-run build and smoke validation.
|
||||
|
||||
## Working Constraints
|
||||
|
||||
The implementation must preserve all existing website working-contract guarantees:
|
||||
|
||||
- keep `@tenantatlas/website` unchanged
|
||||
- keep `WEBSITE_PORT` unchanged
|
||||
- keep root `dev:website` and `build:website` workflows working
|
||||
- keep all IA changes local to `apps/website`
|
||||
- do not introduce platform runtime coupling, shared auth, or shared DTO/API assumptions
|
||||
- do not publish placeholder top-level routes
|
||||
- keep Trust visible in primary navigation
|
||||
- keep `/contact` as the one clear primary conversion path in the header
|
||||
|
||||
## Required Validation
|
||||
|
||||
Run the website build proof from the repo root:
|
||||
|
||||
```bash
|
||||
corepack pnpm build:website
|
||||
```
|
||||
|
||||
Run the browser smoke suite from the website app:
|
||||
|
||||
```bash
|
||||
cd apps/website
|
||||
corepack pnpm exec playwright test
|
||||
```
|
||||
|
||||
If an Astro dev server is already running on `WEBSITE_PORT`, stop it before rerunning Playwright so the suite exercises a fresh server state.
|
||||
|
||||
## What Reviewers Should Verify
|
||||
|
||||
Reviewers should confirm that:
|
||||
|
||||
- the canonical core routes load correctly: `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, `/imprint`
|
||||
- the brand still routes to `/`
|
||||
- Trust is visible in top-level navigation
|
||||
- Product, Trust, Changelog, and Contact are easier to discover than any optional or deferred surfaces
|
||||
- `/resources` stays hidden unless substantive content exists, and the `articles` collection remains unpublished
|
||||
- any retained compatibility paths are explicit and do not create duplicate canonical truth
|
||||
- sitemap and canonical links reflect the canonical route set
|
||||
- no `apps/platform` coupling is introduced
|
||||
|
||||
## Validation Status
|
||||
|
||||
- 2026-04-19: `corepack pnpm build:website` passed.
|
||||
- 2026-04-19: `cd apps/website && corepack pnpm exec playwright test` passed with 13 smoke tests.
|
||||
|
||||
## Out of Scope for This Feature
|
||||
|
||||
- Hero or section composition
|
||||
- Final production copy for the individual pages
|
||||
- Resources activation or later editorial/blog implementation unless actual content is added as part of the same delivery
|
||||
- Pricing, Docs, Customers, Compare, Careers, or Status surfaces
|
||||
- Any `apps/platform` routing, theming, or auth behavior
|
||||
60
specs/215-website-core-pages/research.md
Normal file
60
specs/215-website-core-pages/research.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Research: Website Information Architecture / Core Pages
|
||||
|
||||
## Decision 1: Preserve the website working contract and keep all IA truth local to `apps/website`
|
||||
|
||||
- **Decision**: Treat Spec 215 as a website-only IA change inside `apps/website` and preserve `@tenantatlas/website`, `WEBSITE_PORT`, Astro static output, and the root `dev:website` / `build:website` workflows unchanged.
|
||||
- **Rationale**: The spec is explicitly scoped to the public website and excludes platform routing, auth, and Filament concerns. The narrowest correct implementation is therefore to evolve the existing Astro app in place without introducing backend, package, or runtime coupling to `apps/platform`.
|
||||
- **Alternatives considered**:
|
||||
- Introduce shared website-platform routing or metadata contracts: rejected because the spec explicitly excludes `apps/platform` obligations.
|
||||
- Solve IA through platform-side redirects or a shared CMS: rejected because the website already has a self-contained Astro route layer that can carry the IA truth directly.
|
||||
|
||||
## Decision 2: Use the existing `site.ts` and route-definition layer as the canonical IA source of truth
|
||||
|
||||
- **Decision**: Reuse `apps/website/src/lib/site.ts`, `apps/website/src/types/site.ts`, and `apps/website/src/content/pages` as the canonical source of truth for core routes, page roles, navigation, footer groupings, CTA hierarchy, and page-family mapping.
|
||||
- **Rationale**: The current website already centralizes navigation, page definitions, shell tone, CTA variants, and sitemap route generation in the `site.ts` layer. Extending that layer is the narrowest way to encode the new IA without introducing a second route manifest or a CMS-style navigation system.
|
||||
- **Alternatives considered**:
|
||||
- Create a second IA-only config file: rejected because it would duplicate route truth and increase drift risk.
|
||||
- Hardcode navigation and page-role decisions separately inside each page component: rejected because it would repeat the current v0 problem in a less controllable form.
|
||||
|
||||
## Decision 3: Canonicalize the required core route set and use compatibility paths only when necessary
|
||||
|
||||
- **Decision**: Treat `/`, `/product`, `/trust`, `/changelog`, `/contact`, `/privacy`, and `/imprint` as the canonical core route set for the initial website. Replace `/security-trust` with `/trust` as the canonical trust path, add `/changelog`, add `/imprint`, and keep compatibility paths narrow and temporary if route renames would otherwise cause avoidable breakage.
|
||||
- **Rationale**: The current route inventory contains `/security-trust` instead of `/trust` and lacks both `/changelog` and `/imprint`, even though the spec requires a small, enterprise-credible core that includes visible trust, visible product progress, and legal basics. Canonicalizing these paths aligns the implementation with the spec while keeping change scope focused on the website route layer.
|
||||
- **Alternatives considered**:
|
||||
- Keep `/security-trust` as the long-term canonical path: rejected because the spec names `/trust` and the shorter path is clearer.
|
||||
- Add `/changelog` and `/imprint` only as footer labels without real routes: rejected because the spec requires actual public surfaces, not navigation placeholders.
|
||||
- Preserve every old route as a permanent parallel surface: rejected because multiple canonical truths would dilute the IA and make future page work harder.
|
||||
|
||||
## Decision 4: Keep primary navigation intentionally small and demote non-core v0 routes
|
||||
|
||||
- **Decision**: Shrink primary navigation to the Spec 215 core: Product, Trust, Changelog, Contact, plus optional `Resources` only when substantive content exists. Treat `Solutions` and `Integrations` as retained secondary supporting pages rather than core IA peers.
|
||||
- **Rationale**: The current primary navigation already consumes all 5 available informational slots with Product, Solutions, Security & Trust, Integrations, and Contact, leaving no room for Changelog even though product progress is a first-class public signal in Spec 215. If the IA stays small, solutions-style audience framing must live inside Home/Product first, while already-published supporting pages such as Solutions and Integrations can remain secondary without occupying core nav space.
|
||||
- **Alternatives considered**:
|
||||
- Keep Solutions and Integrations in top-level nav and simply add Changelog as a 6th item: rejected because the spec explicitly prioritizes a small navigation and core-route discipline.
|
||||
- Remove outcome explanation entirely when demoting Solutions: rejected because the spec requires buyer-oriented outcome explanation even without a dedicated `/solutions` hub.
|
||||
|
||||
## Decision 5: Use Astro content collections as readiness gates for optional surfaces
|
||||
|
||||
- **Decision**: Treat the existing `resources` and `changelog` Astro content collections as the current readiness mechanism for optional and recommended public surfaces. The existing `articles` collection remains unpublished until a separate editorial/blog spec activates it. `/changelog` is special: it is part of the recommended core and therefore needs substantive dated entries rather than an empty shell.
|
||||
- **Rationale**: The repo already has empty content collections for articles, changelog, and resources. Using `resources` and `changelog` as explicit discoverability gates avoids empty prestige routes, while leaving `articles` unpublished prevents the current IA from implying an activated blog/editorial surface that does not yet exist.
|
||||
- **Alternatives considered**:
|
||||
- Publish `/blog` or `/resources` immediately as placeholders because collections already exist: rejected because the spec forbids empty prestige pages.
|
||||
- Ignore the existing content collections and hand-build separate page data sources: rejected because it would bypass the narrowest existing structure.
|
||||
|
||||
## Decision 6: Validate the IA through route-aware smoke coverage and build proof
|
||||
|
||||
- **Decision**: Keep validation in `fast-feedback` using `corepack pnpm build:website` and the existing Playwright smoke suite in `apps/website/tests/smoke`, expanded to cover the canonical core routes, updated nav/footer labels, optional-surface suppression, and any compatibility-route expectations.
|
||||
- **Rationale**: The feature changes route topology, navigation, footer grouping, and discoverability rules on a static Astro site. The current smoke suite already verifies shell structure, navigation labels, CTA hierarchy, and footer links; expanding those helpers is the smallest realistic proof of the new IA.
|
||||
- **Alternatives considered**:
|
||||
- Build-only validation: rejected because it would miss browser-visible regressions in navigation labels, hidden/visible route links, and compatibility paths.
|
||||
- Heavier browser or cross-browser infrastructure: rejected because the public website remains static-first and the current smoke suite is sufficient for this route-contract feature.
|
||||
|
||||
## Baseline Findings
|
||||
|
||||
- `apps/website` is a standalone Astro 6 app with static output, strict TypeScript, Tailwind CSS v4, and local Playwright smoke coverage.
|
||||
- The current public route inventory is `/`, `/product`, `/solutions`, `/security-trust`, `/integrations`, `/contact`, `/legal`, `/privacy`, and `/terms`.
|
||||
- The current primary navigation is driven from `src/lib/site.ts` and uses Product, Solutions, Security & Trust, Integrations, and Contact as the five informational links.
|
||||
- The current footer also comes from `src/lib/site.ts` and groups Explore, Next step, and Legal links around the v0 route set.
|
||||
- The current page-definition layer already models page roles, families (`landing`, `trust`, `content`), shell tones, header CTA variants, and footer lead variants.
|
||||
- Astro content collections for `articles`, `changelog`, and `resources` already exist; the current plan activates `changelog`, reserves `resources` as the only optional content surface in this feature, and leaves `articles` unpublished.
|
||||
- The current smoke suite already covers Home, Product, Solutions, Security & Trust, Integrations, Contact, and legal surfaces, so route-topology regression proof can stay inside the existing website test harness.
|
||||
203
specs/215-website-core-pages/spec.md
Normal file
203
specs/215-website-core-pages/spec.md
Normal file
@ -0,0 +1,203 @@
|
||||
# Feature Specification: Website Information Architecture / Core Pages
|
||||
|
||||
**Feature Branch**: `215-website-core-pages`
|
||||
**Created**: 2026-04-19
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Define the initial information architecture and core public pages for `apps/website`, including required pages, page roles, navigation, route model, optional surfaces, trust/legal/update rules, and explicit exclusion of `apps/platform` and page-level design."
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: `apps/website` currently risks inheriting its page inventory from theme defaults or ad hoc page requests instead of from a deliberate buyer journey and product-truth model.
|
||||
- **Today's failure**: Early website work can produce too many thin pages, unclear navigation, buried trust signals, and premature prominence for pricing, docs, or content hubs before product understanding is solid.
|
||||
- **User-visible improvement**: Visitors can understand what the product is, why it matters, why it is credible, and what to do next without navigating a bloated or immature sitemap.
|
||||
- **Smallest enterprise-capable version**: Define a website-local information architecture with required public pages, clear page jobs, a small navigation model, route priorities, and explicit rules for optional and later surfaces.
|
||||
- **Explicit non-goals**: Visual page design, hero or section composition, final copy, SEO strategy, CMS decisions, detailed docs IA, Filament or platform theming, platform IA, auth or app routing, pricing-model decisions, and full content drafting.
|
||||
- **Permanent complexity imported**: A website-local IA vocabulary for core surfaces, optional surfaces, deferred surfaces, primary navigation, footer navigation, outcome explanation, trust claims, and discoverability rules.
|
||||
- **Why now**: The website is early enough that route and navigation decisions can still be shaped before empty prestige pages and template-driven structure become expensive defaults.
|
||||
- **Why not local**: Solving IA one page at a time cannot prevent cross-site navigation drift, cannot prioritize trust and buyer understanding consistently, and cannot stop placeholder routes from becoming normalized.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: New website-local taxonomy for public surfaces; risk of drifting into design-detail work; risk of accidental bleed into `apps/platform` expectations.
|
||||
- **Score**: Nutzen: 3 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace
|
||||
- **Primary Routes**: Required core routes are `/`, `/product`, `/trust`, `/changelog`, the primary conversion route `/contact`, `/privacy`, and `/imprint`; the optional initial content surface for this feature is `/resources`; retained secondary supporting routes may include `/legal`, `/terms`, `/solutions`, and `/integrations` without core prominence; deferred route families include later `/pricing`, `/docs`, and any expanded dedicated solutions-hub structure.
|
||||
- **Data Ownership**: Website-owned public IA contract only: page taxonomy, route priorities, navigation model, and public-surface rules. No tenant-owned records, platform data, or shared persistence are introduced.
|
||||
- **RBAC**: None. This feature applies to public website surfaces and introduces no authorization model.
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: yes
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: yes
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: yes, but only within `apps/website`
|
||||
- **Current operator problem**: Website contributors and reviewers do not yet have a shared rule set for which public pages deserve prominence, which routes are mandatory, and how product, trust, and next-step surfaces should relate to each other.
|
||||
- **Existing structure is insufficient because**: Theme-provided pages and page-local decisions optimize local convenience but do not guarantee a coherent buyer journey, do not prevent empty prestige pages, and do not protect website work from drifting into platform concerns.
|
||||
- **Narrowest correct implementation**: A website-only IA specification that defines required pages, optional pages, deferred pages, route priorities, and public-surface rules without prescribing page design or implementation details.
|
||||
- **Ownership cost**: Future website work must align with this IA before adding routes, promoting new top-level links, or surfacing public claims that require supporting trust context.
|
||||
- **Alternative intentionally rejected**: Allowing page inventory to emerge from theme defaults or from page-by-page requests was rejected because it would prioritize breadth over clarity and create avoidable credibility problems.
|
||||
- **Release truth**: Current-release truth for `apps/website`; this spec must not be interpreted as a shared website-platform contract.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Browser
|
||||
- **Validation lane(s)**: fast-feedback
|
||||
- **Why this classification and these lanes are sufficient**: This spec governs public route composition, navigation behavior, and discoverability on a static website surface. Build proof plus browser smoke coverage of representative public routes is the narrowest honest proof; no database, auth, tenant, or platform runtime setup is required.
|
||||
- **New or expanded test families**: Focused website smoke coverage for required core routes, global navigation, footer navigation, primary CTA reachability, and optional-surface suppression when content is absent.
|
||||
- **Fixture / helper cost impact**: low; browser smoke helpers may expand slightly, but no backend fixtures, seeds, memberships, providers, or session setup are needed.
|
||||
- **Heavy-family visibility / justification**: none; validation stays in fast-feedback only.
|
||||
- **Special surface test profile**: N/A
|
||||
- **Standard-native relief or required special coverage**: Route-level smoke coverage is sufficient; no platform or operator-surface coverage is required.
|
||||
- **Reviewer handoff**: Reviewers should confirm that required routes exist, trust remains top-level visible, one primary conversion path is obvious, optional content hubs are not promoted when empty, and no route or navigation obligation leaks into `apps/platform`.
|
||||
- **Budget / baseline / trend impact**: none beyond small website smoke-suite growth.
|
||||
- **Escalation needed**: document-in-feature
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **Planned validation commands**: `corepack pnpm build:website` and `cd apps/website && corepack pnpm exec playwright test`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Understand the product and next step quickly (Priority: P1)
|
||||
|
||||
A first-time evaluator can land on the website, understand what TenantPilot / TenantAtlas is, find the most relevant deeper pages, and identify a clear next step without being forced through pricing, docs, or thin placeholder pages.
|
||||
|
||||
**Why this priority**: Product understanding and next-step clarity are the primary purpose of the public website and the strongest reason to keep the IA small.
|
||||
|
||||
**Independent Test**: Review the homepage and global navigation and confirm that a first-time visitor can identify the product surface, trust surface, changelog surface, and primary conversion surface without needing any deferred routes.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a first-time visitor lands on `/`, **When** they review the homepage and primary navigation, **Then** they can identify what the product is, where to validate trust, and how to take the next step.
|
||||
2. **Given** a visitor enters directly on `/product`, **When** they want more confidence or a sales path, **Then** they can reach `/trust` and the primary contact surface without navigating through unrelated pages.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Validate trust and technical seriousness (Priority: P1)
|
||||
|
||||
A technical decision maker or security stakeholder can find a dedicated trust surface that supports public claims about security, isolation, hosting, and operating discipline without relying on vague marketing copy.
|
||||
|
||||
**Why this priority**: Trust is a first-class purchase filter for this product category and must be structurally visible, not buried as footer-only material.
|
||||
|
||||
**Independent Test**: Review `/trust` and confirm that public trust, hosting, or residency claims have one explicit supporting surface and are not scattered or implied without context.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a visitor sees a public claim about hosting region, isolation, or operational discipline, **When** they open `/trust`, **Then** they find that claim explained, bounded, or explicitly limited on that page.
|
||||
2. **Given** the homepage keeps trust content intentionally concise, **When** a technical evaluator needs deeper reassurance, **Then** the IA routes them to `/trust` rather than forcing them to infer trust from unrelated marketing sections.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - See visible product progress (Priority: P2)
|
||||
|
||||
A returning visitor or existing follower can verify that the product is evolving by using a dedicated changelog surface instead of hunting across product pages or editorial content.
|
||||
|
||||
**Why this priority**: Visible product motion helps credibility, but it comes after basic product and trust understanding.
|
||||
|
||||
**Independent Test**: Review the core IA and confirm that a returning visitor has a direct route to dated product updates without depending on a blog or resource hub.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a returning visitor wants to know what changed recently, **When** they open `/changelog`, **Then** they see dated, concrete progress signals in one dedicated surface.
|
||||
2. **Given** optional resources or later editorial content is not yet substantive, **When** the visitor uses primary navigation, **Then** the IA does not elevate an empty content hub in place of the changelog.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Optional `/resources` content is not ready yet, and the later editorial `articles` collection is still unpublished, so the website must remain coherent without promoting either surface in primary navigation.
|
||||
- A public hosting or data-residency claim exists, but no supporting trust explanation is available yet; the claim must be removed or the trust surface must support it before publication.
|
||||
- A separate `/solutions` hub is not launched initially, so homepage and product surfaces must still carry buyer-oriented outcome explanation.
|
||||
- A future `/demo` route may exist eventually, but `/contact` remains the clear primary next-step path in the initial IA.
|
||||
- Docs or pricing material may exist in partial form, but they must not be promoted as primary navigation until they are mature enough to support honest public expectations.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no queueing, no long-running operations, no authorization changes, and no Filament operator surfaces. Its contract is explicitly local to `apps/website` and must not create obligations for `apps/platform`.
|
||||
|
||||
**Constitution alignment (UI-SEM-001 / LAYER-001 / BLOAT-001):** This feature intentionally introduces a website-local IA and surface-priority layer because ad hoc route growth and theme-driven page inventories are insufficient to produce a coherent enterprise website. The layer is scoped narrowly to public website surfaces and must not expand into platform IA or a shared website-platform taxonomy without a separate spec.
|
||||
|
||||
**Implementation boundary:** Any implementation under this specification MUST preserve the existing website working contract by keeping `@tenantatlas/website`, `WEBSITE_PORT`, and the root `dev:website` / `build:website` workflows intact, and it MUST NOT introduce runtime or package coupling to `apps/platform`.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The specification MUST define an initial public information architecture for `apps/website` that is explicitly local to the website and explicitly excludes `apps/platform`, platform IA, Filament theming, and app-auth routing decisions.
|
||||
- **FR-002**: The initial required core surfaces MUST be Home, Product, Trust, the primary conversion surface `/contact`, Privacy, and Imprint.
|
||||
- **FR-003**: The initial IA MUST classify Changelog as a strongly recommended public surface with a named role in the website journey.
|
||||
- **FR-004**: The IA MAY classify `Resources` as an optional initial surface, but only when substantive content exists and the route is not a placeholder; any later blog/editorial surface MUST remain unpublished until a separate spec activates it.
|
||||
- **FR-005**: Pricing, Customers or Case Studies, Careers, Compare or Alternatives, Status, a full Docs portal, and a dedicated Solutions hub MUST be treated as deferred surfaces rather than initial core requirements.
|
||||
- **FR-006**: The homepage at `/` MUST act as a routing, positioning, and trust hub that explains the product at a high level and opens clear paths to Product, Trust, Changelog, and the primary conversion surface.
|
||||
- **FR-007**: The homepage MUST include buyer-oriented outcome or use-case explanation and MUST NOT rely on feature taxonomy alone to explain why the product matters.
|
||||
- **FR-008**: The product surface at `/product` MUST explain what TenantPilot / TenantAtlas is, what it is not, how its capability areas are grouped, and how those capabilities relate to buyer outcomes.
|
||||
- **FR-009**: The product surface MUST cover, at an appropriate public level, Backup, Restore, Versioning, Audit, Inventory or Drift Detection, and Governance topics such as Baselines, Findings, Exceptions, Evidence, and Reviews.
|
||||
- **FR-010**: The trust surface at `/trust` MUST exist as a first-class public page and MUST cover security and operating principles, a public-safe architecture overview, tenant isolation, credential or access handling, protection measures, update and operating discipline, and a path for deeper trust or security questions.
|
||||
- **FR-011**: Any public claim about hosting region, data residency, tenant isolation, or security posture MUST be supported, bounded, or contextualized on `/trust`, and the website MUST NOT make absolute legal-compliance claims that it cannot responsibly qualify.
|
||||
- **FR-012**: The changelog surface at `/changelog` MUST show dated, concrete product progress and MUST NOT be treated as a replacement for the product page or for long-form editorial content.
|
||||
- **FR-013**: The website MUST expose `/contact` as the clear, low-friction primary conversion surface, and if a demo option exists later, it MUST remain secondary unless a future spec changes the primary conversion route.
|
||||
- **FR-014**: The initial top-level navigation MUST remain intentionally small and SHOULD default to Product, Trust, Changelog, optional `Resources` only when substantive, and Contact, plus one primary CTA.
|
||||
- **FR-015**: The brand or logo MUST route visitors to `/`.
|
||||
- **FR-016**: No top-level navigation item or other prominent public link MAY point to a placeholder, thin-content, or template-only page.
|
||||
- **FR-017**: Trust MUST remain visible in top-level navigation and MUST NOT be relegated to footer-only discoverability.
|
||||
- **FR-018**: The footer MUST group product, trust or legal, contact, and optional content or docs links in a consistent public navigation model.
|
||||
- **FR-019**: The initial URL model MUST use short, clear public paths and MUST avoid unnecessary early nesting, artificial segmentation, or mixing marketing routes with app routes.
|
||||
- **FR-020**: The website MUST provide a buyer-oriented outcome or use-case explanation layer on the homepage, on the product page, or on both, even if a dedicated `/solutions` hub is not launched initially.
|
||||
- **FR-021**: Docs MAY become discoverable once a minimal, credible documentation surface exists, but the IA MUST NOT force Docs into primary navigation before that threshold is met.
|
||||
- **FR-022**: Pricing MAY be introduced later, but the IA MUST NOT force Pricing into primary navigation until packaging, expectations, and public framing are mature enough to be honest and coherent.
|
||||
- **FR-023**: The page relationship model MUST support an entry route, a first-clarification phase, a deeper-exploration phase, and a clear action phase ending in Contact.
|
||||
- **FR-024**: Public website language and page prioritization MUST favor product truth, trust, outcome understanding, and clear next steps over hype, fake maturity signals, or inflated enterprise theater.
|
||||
- **FR-025**: The specification MUST define its deliverables explicitly: required core page list, role of each page, top-level navigation, footer navigation, route model, optional-surface rules, deferred-surface list, outcome-explanation rule, trust-claim rule, and docs-discoverability rule.
|
||||
- **FR-026**: No requirement in this specification MAY be interpreted as a commitment to platform routing, platform design, or shared website-platform IA work.
|
||||
- **FR-027**: Implementation work under this specification MUST preserve the website working contract by retaining `@tenantatlas/website`, `WEBSITE_PORT`, and the root `dev:website` / `build:website` workflows, and it MUST NOT introduce runtime coupling or shared-package obligations for `apps/platform`.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
- Visual design of individual pages
|
||||
- Hero or section composition
|
||||
- Final production copy
|
||||
- SEO keyword planning
|
||||
- CMS selection
|
||||
- Detailed documentation IA
|
||||
- Filament or `apps/platform` theming
|
||||
- Platform IA
|
||||
- Auth or app-routing behavior
|
||||
- Pricing-model decisions
|
||||
- Full content drafting for each page
|
||||
|
||||
#### Assumptions
|
||||
|
||||
- `apps/website` must work for enterprise buyers, MSPs, technical decision makers, security or governance stakeholders, and returning followers without creating separate website tracks for each audience in the initial IA.
|
||||
- Outcome and use-case explanation can live inside Home and Product initially without requiring a dedicated Solutions hub.
|
||||
- Legal basics must be present from the start, while Docs and Pricing can remain deferred until their public substance is strong enough.
|
||||
- The initial website should favor a small number of durable public routes over a broad marketing sitemap.
|
||||
- For Spec 215 execution, the optional content surface standardizes on `/resources`; the existing editorial `articles` collection remains unpublished until a later blog/editorial spec activates it.
|
||||
- Existing `/legal`, `/terms`, `/solutions`, and `/integrations` pages may remain published as secondary supporting surfaces, but they MUST NOT displace the required core IA in top-level navigation or buyer flow.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Core Surface**: A required initial public page that carries one named job in the buyer journey and is part of the minimal credible website.
|
||||
- **Optional Initial Surface**: A page family such as `Resources` that is allowed in the initial IA only if substantive content exists at launch.
|
||||
- **Deferred Surface**: A public page family intentionally excluded from the initial core website until its content, claims, or business model are mature enough.
|
||||
- **Trust Surface**: The dedicated public page that supports technical seriousness, trust claims, and bounded public statements about hosting, residency, isolation, and operating discipline.
|
||||
- **Outcome Explanation Layer**: The buyer-oriented explanation of operational or business problems solved by the product, expressed without depending on a dedicated Solutions hub.
|
||||
- **Primary Conversion Surface**: The main next-step path, Contact in the initial IA, that converts public understanding into a clear action.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: The IA defines 100% of required initial core routes: `/`, `/product`, `/trust`, `/changelog`, the primary conversion route `/contact`, `/privacy`, and `/imprint`.
|
||||
- **SC-002**: 100% of top-level navigation items in the initial IA map to named core or approved optional surfaces with explicit roles, and 0 prominent links point to placeholder or thin-content pages.
|
||||
- **SC-003**: From each core entry route (`/`, `/product`, `/trust`, and `/changelog`), a visitor can reach the primary conversion surface in no more than 2 clicks.
|
||||
- **SC-004**: 100% of public hosting, residency, isolation, or security claims in the initial IA have a designated supporting or bounding surface on `/trust`, or the claim is excluded from the public website.
|
||||
- **SC-005**: Reviewers can map the four core buyer questions - what is it, who is it for, why trust it, and what next - to explicit public surfaces without requiring an initial Pricing page, Docs portal, or dedicated Solutions hub.
|
||||
- **SC-006**: The initial informational top-level navigation exposes no more than 5 public route entries plus one primary CTA, preserving a deliberately small public IA.
|
||||
|
||||
## Planned Follow-on Specs
|
||||
|
||||
- Spec 216 - Homepage Structure and Section Model
|
||||
- Spec 217 - Product Page Structure
|
||||
- Spec 218 - Trust Surface
|
||||
- Spec 219 - Contact / Demo Flow
|
||||
- Spec 220 - Changelog Surface
|
||||
- Spec 221 - Blog / Resources Surface, if activated
|
||||
- Spec 222 - Solutions / Use-Case Surfaces, if activated later
|
||||
- Spec 223 - Pricing Surface, if activated later
|
||||
205
specs/215-website-core-pages/tasks.md
Normal file
205
specs/215-website-core-pages/tasks.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Tasks: Website Information Architecture / Core Pages
|
||||
|
||||
**Input**: Design documents from `/specs/215-website-core-pages/`
|
||||
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/public-site-ia.openapi.yaml`
|
||||
|
||||
**Tests**: Browser smoke coverage and the root website build proof are required for this runtime-changing website feature.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [X] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [X] New or changed tests stay in the smallest honest family, and any browser coverage addition remains explicit.
|
||||
- [X] Shared helpers and context defaults stay cheap by default; no backend, auth, or fixture-heavy setup is introduced.
|
||||
- [X] Planned validation commands cover the website IA change without pulling in unrelated platform lane cost.
|
||||
- [X] Any runtime-cost or escalation note stays documented in this feature rather than being deferred implicitly.
|
||||
- [X] Explicit governance outcome is recorded as `document-in-feature` for this feature-local fast-feedback validation scope.
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Prepare the canonical website IA types, metadata hooks, and smoke-test scaffolding that all user stories depend on.
|
||||
|
||||
- [X] T001 Update the expanded route and surface taxonomy in `apps/website/src/types/site.ts`
|
||||
- [X] T002 [P] Rebuild the core route registry, navigation metadata, footer group seeds, and conversion-route metadata in `apps/website/src/lib/site.ts`
|
||||
- [X] T003 [P] Extend route-topology, hidden-surface, and core-navigation smoke helpers in `apps/website/tests/smoke/smoke-helpers.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Create the shared SEO, shell, and publication scaffolding that must exist before any individual story can be completed.
|
||||
|
||||
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||
|
||||
- [X] T004 Prepare canonical URL and sitemap scaffolding for core and retained secondary routes in `apps/website/src/lib/seo.ts` and `apps/website/src/pages/sitemap.xml.ts`
|
||||
- [X] T005 [P] Prepare shared shell support for new core and retained secondary page roles in `apps/website/src/layouts/BaseLayout.astro` and `apps/website/src/components/layout/PageShell.astro`
|
||||
- [X] T006 [P] Wire optional `Resources` gating and keep the unpublished `articles` collection out of the public IA in `apps/website/src/content.config.ts` and `apps/website/src/lib/site.ts`
|
||||
|
||||
**Checkpoint**: The canonical core-route skeleton exists, and user-story work can proceed independently on top of it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Understand the Product and Next Step Quickly (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Make the first visitor journey obvious by centering Product, Trust, Changelog, and Contact in the header and core pages.
|
||||
|
||||
**Independent Test**: A first-time visitor can use `/` and `/product` to discover Product, Trust, Changelog, and Contact within two clicks, without seeing placeholder or deferred top-level routes.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [X] T007 [P] [US1] Update first-journey smoke coverage for home/product navigation, CTA hierarchy, and core-route reachability in `apps/website/tests/smoke/home-product.spec.ts`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T008 [P] [US1] Rebuild primary navigation and header CTA behavior around Product, Trust, Changelog, and Contact in `apps/website/src/lib/site.ts`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/PageShell.astro`
|
||||
- [X] T009 [P] [US1] Refocus the home and product content modules on product understanding, buyer outcomes, and the next-step path in `apps/website/src/content/pages/home.ts` and `apps/website/src/content/pages/product.ts`
|
||||
- [X] T010 [US1] Apply the core first-visit journey to `apps/website/src/content/pages/contact.ts`, `apps/website/src/pages/index.astro`, `apps/website/src/pages/product.astro`, and `apps/website/src/pages/contact.astro`
|
||||
|
||||
**Checkpoint**: Home and Product now deliver the MVP first-visitor journey with one clear next step.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Validate Trust and Technical Seriousness (Priority: P1)
|
||||
|
||||
**Goal**: Make `/trust` the canonical credibility surface and keep legal/trust discoverability coherent and explicit.
|
||||
|
||||
**Independent Test**: A technical evaluator can open `/trust`, confirm the presence of trust posture and bounded claims, reach Privacy and Imprint from the footer, and see the legacy `/security-trust` path resolve to the canonical Trust surface.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [X] T011 [P] [US2] Update trust/legal smoke coverage for the canonical Trust route, `/trust -> /contact` reachability, footer legal visibility, and the legacy Trust redirect in `apps/website/tests/smoke/solutions-trust-integrations.spec.ts` and `apps/website/tests/smoke/contact-legal.spec.ts`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T012 [P] [US2] Implement the canonical Trust surface with bounded trust-claim content in `apps/website/src/content/pages/trust.ts` and `apps/website/src/pages/trust.astro`
|
||||
- [X] T013 [P] [US2] Rebuild trust/legal footer groups around Trust, Privacy, Imprint, Terms, and Contact in `apps/website/src/lib/site.ts` and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T014 [US2] Align the retained secondary legal surfaces and Trust compatibility path to the new Trust contract in `apps/website/src/content/pages/legal.ts`, `apps/website/src/content/pages/privacy.ts`, `apps/website/src/content/pages/imprint.ts`, `apps/website/src/pages/legal.astro`, `apps/website/src/pages/privacy.astro`, `apps/website/src/pages/imprint.astro`, `apps/website/src/pages/terms.astro`, and `apps/website/src/pages/security-trust.astro`
|
||||
|
||||
**Checkpoint**: Trust and legal discoverability are now canonical, explicit, and compatibility-safe.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - See Visible Product Progress (Priority: P2)
|
||||
|
||||
**Goal**: Publish a real Changelog surface and keep optional or deferred surfaces from inflating the initial IA.
|
||||
|
||||
**Independent Test**: A returning visitor can open `/changelog`, see dated updates, and confirm that unpublished `Resources` and editorial `articles` content are not promoted while non-core legacy routes no longer dominate the IA.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [X] T015 [P] [US3] Add changelog and optional-surface smoke coverage for dated updates, `/changelog -> /contact` reachability, hidden `Resources`, hidden editorial `articles`, and footer content gating in `apps/website/tests/smoke/changelog-core-ia.spec.ts`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T016 [P] [US3] Implement the Changelog surface and its initial dated entry in `apps/website/src/content/pages/changelog.ts`, `apps/website/src/pages/changelog.astro`, and `apps/website/src/content/changelog/2026-04-19-initial-core-pages.md`
|
||||
- [X] T017 [P] [US3] Gate optional `Resources` discoverability, keep the editorial `articles` collection unpublished, and keep deferred surfaces out of primary navigation in `apps/website/src/lib/site.ts`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T018 [US3] Reclassify `/legal`, `/terms`, `/solutions`, and `/integrations` as retained secondary surfaces in `apps/website/src/content/pages/legal.ts`, `apps/website/src/pages/legal.astro`, `apps/website/src/content/pages/terms.ts`, `apps/website/src/pages/terms.astro`, `apps/website/src/content/pages/solutions.ts`, `apps/website/src/pages/solutions.astro`, `apps/website/src/content/pages/integrations.ts`, and `apps/website/src/pages/integrations.astro`
|
||||
|
||||
**Checkpoint**: Returning visitors can see real product progress, and optional/deferred surfaces no longer crowd the initial IA.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Finalize canonical route outputs, record the lane proof, and verify website-working-contract compatibility.
|
||||
|
||||
- [X] T019 [P] Refresh canonical URL generation and sitemap output for the final core IA in `apps/website/src/lib/seo.ts` and `apps/website/src/pages/sitemap.xml.ts`
|
||||
- [X] T020 [P] Record fast-feedback lane validation notes and reviewer proof commands in `specs/215-website-core-pages/plan.md` and `specs/215-website-core-pages/quickstart.md`
|
||||
- [X] T021 Run `corepack pnpm build:website` and `corepack pnpm exec playwright test` for the updated website IA contract using `package.json`, `apps/website/package.json`, and `apps/website/playwright.config.ts`
|
||||
- [X] T022 Verify website working-contract and static-output invariants in `apps/website/astro.config.mjs`, `package.json`, and `apps/website/package.json`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- Phase 1 starts immediately.
|
||||
- Phase 2 depends on Phase 1 and blocks all user stories.
|
||||
- Phase 3 depends on Phase 2 only.
|
||||
- Phase 4 depends on Phase 2 only.
|
||||
- Phase 5 depends on Phase 2 only.
|
||||
- Phase 6 depends on the targeted user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- US1 is the MVP slice and has no dependency on US2 or US3.
|
||||
- US2 has no dependency on US1 or US3, but reuses the shared route and navigation foundation.
|
||||
- US3 has no dependency on US1 or US2, but reuses the shared core-route scaffolding and smoke helpers.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Write or update the browser smoke coverage first.
|
||||
- Update central route or content metadata before finalizing page-route composition.
|
||||
- Finish route-level integration before moving to polish or the next story’s cleanup.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
- T002 and T003 can run in parallel after T001.
|
||||
- T005 can run in parallel with T006 after T004 starts.
|
||||
- In US1, T008 and T009 can run in parallel before T010.
|
||||
- In US2, T012 and T013 can run in parallel before T014.
|
||||
- In US3, T016 and T017 can run in parallel before T018.
|
||||
- In Phase 6, T019 and T020 can run in parallel before T021 and T022.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 1
|
||||
|
||||
```bash
|
||||
# Launch first-journey metadata and content work together:
|
||||
Task: "T008 [US1] Rebuild primary navigation and header CTA behavior"
|
||||
Task: "T009 [US1] Refocus the home and product content modules"
|
||||
|
||||
# Then finish route-level integration:
|
||||
Task: "T010 [US1] Apply the core first-visit journey to the published pages"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 2
|
||||
|
||||
```bash
|
||||
# Launch Trust and footer/legal work together:
|
||||
Task: "T012 [US2] Implement the canonical Trust surface"
|
||||
Task: "T013 [US2] Rebuild trust/legal footer groups"
|
||||
|
||||
# Then align the legal baseline and compatibility path:
|
||||
Task: "T014 [US2] Align the legal baseline and compatibility surfaces"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 3
|
||||
|
||||
```bash
|
||||
# Launch changelog publishing and optional-surface gating together:
|
||||
Task: "T016 [US3] Implement the Changelog surface and initial dated entry"
|
||||
Task: "T017 [US3] Gate optional Resources discoverability"
|
||||
|
||||
# Then reclassify the non-core legacy surfaces:
|
||||
Task: "T018 [US3] Reclassify the retained secondary surfaces"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First
|
||||
|
||||
1. Complete Phase 1: Setup.
|
||||
2. Complete Phase 2: Foundational.
|
||||
3. Complete Phase 3: User Story 1.
|
||||
4. Run `corepack pnpm build:website` and the updated home/product smoke proof.
|
||||
5. Demo the MVP on `/`, `/product`, and `/contact` with the new header navigation.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Setup and Foundational phases establish the canonical core-route contract.
|
||||
2. US1 makes the first visitor journey obvious.
|
||||
3. US2 makes Trust and legal discoverability canonical.
|
||||
4. US3 adds visible progress and optional-surface gating.
|
||||
5. Polish refreshes sitemap/canonical output and closes the lane-validation loop.
|
||||
|
||||
### Suggested MVP Scope
|
||||
|
||||
- Deliver through **User Story 1** if the smallest initial release is needed.
|
||||
- Add **User Story 2** next to make Trust and legal discoverability fully canonical.
|
||||
- Finish with **User Story 3** to publish Changelog and tighten optional-surface discipline.
|
||||
Loading…
Reference in New Issue
Block a user