Merge remote-tracking branch 'origin/dev' into 214-governance-outcome-compression
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 46s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 46s
# Conflicts: # .github/agents/copilot-instructions.md
This commit is contained in:
commit
85d93135e3
8
.github/agents/copilot-instructions.md
vendored
8
.github/agents/copilot-instructions.md
vendored
@ -208,10 +208,14 @@ ## Active Technologies
|
||||
- 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` (200-filament-surface-rules)
|
||||
- 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 (213-website-foundation-v0)
|
||||
- Static filesystem content, styles, and assets under `apps/website/src` and `apps/website/public`; no database (213-website-foundation-v0)
|
||||
- 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 (214-website-visual-foundation)
|
||||
- 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)
|
||||
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + Filament v5, Livewire v4, Pest v4, Laravel Sail, `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `OperatorExplanationBuilder`, `BaselineSnapshotPresenter`, `BadgeCatalog`, `BadgeRenderer`, existing governance Filament resources/pages, and current Enterprise Detail builders (214-governance-outcome-compression)
|
||||
- PostgreSQL via existing `baseline_snapshots`, `evidence_snapshots`, `evidence_snapshot_items`, `tenant_reviews`, `review_packs`, and `operation_runs` tables; no schema change planned (214-governance-outcome-compression)
|
||||
- Astro 6.0.0 templates + TypeScript 5.9 strict + 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,8 +250,10 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 215-website-core-pages: Added Astro 6.0.0 templates + TypeScript 5.9 strict + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro layout/primitive/content helpers, Playwright smoke tests
|
||||
- 214-governance-outcome-compression: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + Filament v5, Livewire v4, Pest v4, Laravel Sail, `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `OperatorExplanationBuilder`, `BaselineSnapshotPresenter`, `BadgeCatalog`, `BadgeRenderer`, existing governance Filament resources/pages, and current Enterprise Detail builders
|
||||
- 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
|
||||
- 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
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
---
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import type { AudienceRowContent } from '@/types/site';
|
||||
@ -11,17 +14,17 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{item.audience}
|
||||
</p>
|
||||
<h3 class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
<Eyebrow>{item.audience}</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
<ul class="mt-5 space-y-3 p-0">
|
||||
{
|
||||
item.bullets.map((bullet) => (
|
||||
<li class="list-none rounded-[1rem] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
|
||||
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
|
||||
{bullet}
|
||||
</li>
|
||||
))
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { CalloutContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,13 +14,11 @@ const variant = content.tone === 'accent' ? 'accent' : content.tone === 'subtle'
|
||||
---
|
||||
|
||||
<Card variant={variant}>
|
||||
{content.eyebrow && (
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{content.eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
{content.eyebrow && <Eyebrow>{content.eyebrow}</Eyebrow>}
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{content.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{content.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{content.description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -13,7 +15,10 @@ const { cta, points, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Card variant="accent">
|
||||
<h3 class="m-0 text-3xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{title}</h3>
|
||||
<Eyebrow>Qualified outreach</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4 text-3xl">
|
||||
{title}
|
||||
</Headline>
|
||||
<ul class="mt-5 space-y-3 p-0">
|
||||
{
|
||||
points.map((point) => (
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
|
||||
interface Props {
|
||||
description: string;
|
||||
@ -10,9 +13,11 @@ const { description, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Card>
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
Conversation focus
|
||||
</p>
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{title}</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{description}</p>
|
||||
<Eyebrow>Conversation focus</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{title}
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
tone?: 'accent' | 'neutral' | 'signal';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { class: className = '', tone = 'accent' } = Astro.props;
|
||||
const toneClasses = {
|
||||
accent: 'text-[var(--color-brand)]',
|
||||
neutral: 'text-[var(--color-muted-foreground)]',
|
||||
signal: 'text-[var(--color-signal)]',
|
||||
};
|
||||
---
|
||||
|
||||
<p class:list={['m-0 text-sm font-semibold uppercase tracking-[0.18em] text-[var(--color-brand)]', className]}>
|
||||
<p
|
||||
class:list={[
|
||||
'm-0 text-[var(--type-eyebrow-size)] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { FeatureItemContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -10,20 +13,18 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
{item.eyebrow && (
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{item.eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
{item.eyebrow && <Eyebrow>{item.eyebrow}</Eyebrow>}
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
{(item.meta || item.href) && (
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3 text-sm">
|
||||
{item.meta && <span class="text-[var(--color-brand)]">{item.meta}</span>}
|
||||
{item.href && (
|
||||
<a class="font-semibold text-[var(--color-ink-900)] underline decoration-[rgba(17,36,58,0.18)] underline-offset-4" href={item.href}>
|
||||
<a class="text-link font-semibold" href={item.href}>
|
||||
Learn more
|
||||
</a>
|
||||
)}
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
size?: 'card' | 'display' | 'page' | 'section';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { as = 'h2', class: className = '', size = 'section' } = Astro.props;
|
||||
const Tag = as;
|
||||
const sizeClasses = {
|
||||
display:
|
||||
'font-[var(--font-display)] text-[length:var(--type-display-size)] leading-[var(--line-display)] tracking-[var(--tracking-display)]',
|
||||
page: 'font-[var(--font-display)] text-[length:var(--type-page-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
|
||||
section:
|
||||
'font-[var(--font-display)] text-[length:var(--type-section-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
|
||||
card: 'font-semibold text-[length:var(--type-card-size)] leading-[1.12] tracking-[var(--tracking-tight)]',
|
||||
};
|
||||
---
|
||||
|
||||
<h2 class:list={['m-0 font-[var(--font-display)] text-4xl leading-[0.98] tracking-[-0.03em] text-[var(--color-ink-900)] sm:text-5xl', className]}>
|
||||
<Tag class:list={['m-0 text-[var(--color-ink-900)]', sizeClasses[size], className]}>
|
||||
<slot />
|
||||
</h2>
|
||||
</Tag>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Badge from '@/components/primitives/Badge.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { IntegrationEntry } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -9,11 +12,15 @@ interface Props {
|
||||
const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="rounded-[1.1rem] border border-[rgba(17,36,58,0.08)] bg-white/78 px-4 py-3 shadow-[var(--shadow-soft)]">
|
||||
<Card class="h-full px-4 py-4" variant="subtle">
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge tone="neutral">{item.category}</Badge>
|
||||
<p class="m-0 text-base font-semibold text-[var(--color-ink-900)]">{item.name}</p>
|
||||
<Headline as="h3" size="card" class="text-base">
|
||||
{item.name}
|
||||
</Headline>
|
||||
</div>
|
||||
<p class="mt-3 max-w-72 text-sm leading-6 text-[var(--color-copy)]">{item.summary}</p>
|
||||
<Lead class="mt-3 max-w-72" size="small">
|
||||
{item.summary}
|
||||
</Lead>
|
||||
{item.note && <p class="mt-2 text-xs font-medium uppercase tracking-[0.14em] text-[var(--color-brand)]">{item.note}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
size?: 'body' | 'lead' | 'small';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { as = 'p', class: className = '', size = 'lead' } = Astro.props;
|
||||
const Tag = as;
|
||||
const sizeClasses = {
|
||||
lead: 'text-[length:var(--type-body-size)] leading-[var(--line-body)] sm:text-lg',
|
||||
body: 'text-[length:var(--type-body-size)] leading-[var(--line-body)]',
|
||||
small: 'text-[length:var(--type-small-size)] leading-[1.65]',
|
||||
};
|
||||
---
|
||||
|
||||
<p class:list={['m-0 text-base leading-8 text-[var(--color-copy)] sm:text-lg', className]}>
|
||||
<Tag class:list={['m-0 text-[var(--color-copy)]', sizeClasses[size], className]}>
|
||||
<slot />
|
||||
</p>
|
||||
</Tag>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { MetricItem } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,8 +13,10 @@ const { item } = Astro.props;
|
||||
|
||||
<Card variant="subtle">
|
||||
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">{item.value}</p>
|
||||
<p class="mt-2 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-brand)]">
|
||||
<Eyebrow class="mt-2">
|
||||
{item.label}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Eyebrow>
|
||||
<Lead class="mt-2" size="small">
|
||||
{item.description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -4,9 +4,17 @@ import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
cta: CtaLink;
|
||||
class?: string;
|
||||
showHelper?: boolean;
|
||||
size?: 'lg' | 'md' | 'sm';
|
||||
}
|
||||
|
||||
const { cta } = Astro.props;
|
||||
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
|
||||
---
|
||||
|
||||
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>
|
||||
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="primary">
|
||||
<Button href={cta.href} variant={cta.variant ?? 'primary'} size={size}>
|
||||
{cta.label}
|
||||
</Button>
|
||||
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { LegalSection } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,19 +14,19 @@ const { sections } = Astro.props;
|
||||
<div class="space-y-6">
|
||||
{
|
||||
sections.map((section) => (
|
||||
<section class="rounded-[1.5rem] border border-[rgba(17,36,58,0.08)] bg-white/72 p-6 shadow-[var(--shadow-soft)]">
|
||||
<h2 class="m-0 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
<Card as="section" class="rounded-[var(--radius-lg)]" variant="subtle">
|
||||
<Headline as="h2" size="card">
|
||||
{section.title}
|
||||
</h2>
|
||||
</Headline>
|
||||
<div class="legal-prose mt-4">
|
||||
{section.body.map((paragraph) => <p>{paragraph}</p>)}
|
||||
{section.body.map((paragraph) => <Lead size="body">{paragraph}</Lead>)}
|
||||
{section.bullets && section.bullets.length > 0 && (
|
||||
<ul>
|
||||
{section.bullets.map((bullet) => <li>{bullet}</li>)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -4,9 +4,17 @@ import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
cta: CtaLink;
|
||||
class?: string;
|
||||
showHelper?: boolean;
|
||||
size?: 'lg' | 'md' | 'sm';
|
||||
}
|
||||
|
||||
const { cta } = Astro.props;
|
||||
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
|
||||
---
|
||||
|
||||
<Button href={cta.href} variant={cta.variant ?? 'secondary'}>{cta.label}</Button>
|
||||
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="secondary">
|
||||
<Button href={cta.href} variant={cta.variant ?? 'secondary'} size={size}>
|
||||
{cta.label}
|
||||
</Button>
|
||||
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { TrustPrincipleContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -10,7 +12,11 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
<h3 class="m-0 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{item.title}</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
{item.note && <p class="mt-4 text-sm font-medium text-[var(--color-brand)]">{item.note}</p>}
|
||||
<Headline as="h3" size="card">
|
||||
{item.title}
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
{item.note && <Lead class="mt-4 text-[var(--color-brand)]" size="small">{item.note}</Lead>}
|
||||
</Card>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { contactCta, footerNavigationGroups, siteMetadata } from '@/lib/site';
|
||||
import { getFooterLead, getFooterNavigationGroups, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
@ -9,25 +9,27 @@ interface Props {
|
||||
|
||||
const { currentPath: _currentPath } = Astro.props;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const footerLead = getFooterLead(_currentPath);
|
||||
const footerNavigationGroups = await getFooterNavigationGroups();
|
||||
---
|
||||
|
||||
<footer class="section-divider pt-10 sm:pt-12">
|
||||
<Container wide>
|
||||
<div class="grid gap-8 rounded-[2rem] bg-[rgba(255,255,255,0.58)] p-6 shadow-[var(--shadow-soft)] lg:grid-cols-[1.3fr,1fr] lg:p-8">
|
||||
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={footerLead.intent}>
|
||||
<Container width="wide">
|
||||
<div class="surface-card-muted grid gap-8 rounded-[var(--radius-panel)] p-6 lg:grid-cols-[1.3fr,1fr] lg:p-8">
|
||||
<div class="space-y-5">
|
||||
<p class="m-0 text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{siteMetadata.siteName}
|
||||
{footerLead.eyebrow}
|
||||
</p>
|
||||
<h2 class="m-0 max-w-xl font-[var(--font-display)] text-3xl leading-[0.98] text-[var(--color-ink-900)] sm:text-4xl">
|
||||
A calmer public surface for teams that need governance clarity before they need another dashboard.
|
||||
{footerLead.title}
|
||||
</h2>
|
||||
<p class="m-0 max-w-xl text-base leading-7 text-[var(--color-copy)]">
|
||||
TenantAtlas keeps product explanation, trust framing, and next-step guidance readable without hiding the product model behind hype or placeholders.
|
||||
{footerLead.description}
|
||||
</p>
|
||||
<Button href={contactCta.href} variant="primary" size="sm">{contactCta.label}</Button>
|
||||
<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>
|
||||
@ -50,7 +52,7 @@ const currentYear = new Date().getFullYear();
|
||||
</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,23 +1,26 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { contactCta, isActiveNavigationPath, primaryNavigation, siteMetadata } from '@/lib/site';
|
||||
import { getHeaderCta, getPrimaryNavigation, isActiveNavigationPath, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
const { currentPath } = Astro.props;
|
||||
const headerCta = getHeaderCta(currentPath);
|
||||
const primaryNavigation = await getPrimaryNavigation();
|
||||
---
|
||||
|
||||
<header class="sticky top-0 z-30 pt-4 sm:pt-6">
|
||||
<Container wide>
|
||||
<header class="sticky top-0 z-30 px-[var(--space-page-x)] pt-4 sm:pt-6">
|
||||
<Container width="wide">
|
||||
<div
|
||||
class="glass-panel flex items-center justify-between gap-4 rounded-[1.75rem] border border-white/70 px-4 py-3 sm:px-5"
|
||||
class="shell-panel flex items-center justify-between gap-4 rounded-[var(--radius-panel)] px-4 py-3 sm:px-5"
|
||||
data-shell-surface="header"
|
||||
>
|
||||
<a href="/" class="flex min-w-0 items-center gap-3 no-underline">
|
||||
<span
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-brand),#7fa6cf)] font-[var(--font-display)] text-lg text-white"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-primary),#8eaed1)] font-[var(--font-display)] text-lg text-white shadow-[var(--shadow-inline)]"
|
||||
>
|
||||
TA
|
||||
</span>
|
||||
@ -36,12 +39,14 @@ const { currentPath } = Astro.props;
|
||||
primaryNavigation.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
|
||||
class:list={[
|
||||
'rounded-full px-4 py-2 text-sm font-medium transition',
|
||||
isActiveNavigationPath(currentPath, item.href)
|
||||
? 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]'
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
|
||||
: 'text-[var(--color-ink-800)] hover:bg-white/70',
|
||||
]}
|
||||
data-nav-link="primary"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
@ -50,13 +55,14 @@ const { currentPath } = Astro.props;
|
||||
</nav>
|
||||
|
||||
<div class="hidden lg:block">
|
||||
<Button href={contactCta.href} variant="secondary" size="sm">{contactCta.label}</Button>
|
||||
<SecondaryCTA cta={headerCta} size="sm" />
|
||||
</div>
|
||||
|
||||
<details class="relative lg:hidden">
|
||||
<details class="relative lg:hidden" data-mobile-nav>
|
||||
<summary
|
||||
aria-label="Open navigation menu"
|
||||
class="flex h-11 w-11 cursor-pointer list-none items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)]"
|
||||
data-mobile-nav-trigger
|
||||
>
|
||||
<span class="sr-only">Open navigation menu</span>
|
||||
<span class="flex flex-col gap-1">
|
||||
@ -73,12 +79,14 @@ const { currentPath } = Astro.props;
|
||||
primaryNavigation.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
|
||||
class:list={[
|
||||
'rounded-[1rem] px-4 py-3 text-sm',
|
||||
isActiveNavigationPath(currentPath, item.href)
|
||||
? 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]'
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
|
||||
: 'text-[var(--color-ink-800)] hover:bg-white/75',
|
||||
]}
|
||||
data-nav-link="mobile-primary"
|
||||
>
|
||||
<span class="block font-semibold">{item.label}</span>
|
||||
{item.description && (
|
||||
@ -90,12 +98,7 @@ const { currentPath } = Astro.props;
|
||||
))
|
||||
}
|
||||
<div class="mt-2 rounded-[1rem] bg-[rgba(47,111,183,0.08)] p-3">
|
||||
<p class="m-0 text-sm font-semibold text-[var(--color-ink-900)]">
|
||||
{contactCta.label}
|
||||
</p>
|
||||
{contactCta.helper && (
|
||||
<p class="mt-1 text-sm text-[var(--color-copy)]">{contactCta.helper}</p>
|
||||
)}
|
||||
<SecondaryCTA cta={headerCta} size="sm" showHelper />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
---
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
import Navbar from '@/components/layout/Navbar.astro';
|
||||
import { getPageDefinition } from '@/lib/site';
|
||||
import { resolveSeo } from '@/lib/seo';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
|
||||
@ -15,6 +16,7 @@ const seo =
|
||||
title && description
|
||||
? resolveSeo({ description, path: currentPath, title })
|
||||
: undefined;
|
||||
const pageDefinition = getPageDefinition(currentPath);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@ -25,13 +27,22 @@ const seo =
|
||||
openGraphDescription={seo?.ogDescription}
|
||||
robots={seo?.robots}
|
||||
>
|
||||
<div class="surface-shell min-h-screen">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[30rem] bg-[radial-gradient(circle_at_top,rgba(47,111,183,0.16),transparent_50%),radial-gradient(circle_at_top_right,rgba(59,139,120,0.14),transparent_28%)]"
|
||||
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%)]"
|
||||
>
|
||||
</div>
|
||||
<Navbar currentPath={currentPath} />
|
||||
<main id="content" class="pb-16 sm:pb-20">
|
||||
<main id="content" class="foundation-main pb-20 sm:pb-24">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer currentPath={currentPath} />
|
||||
|
||||
@ -7,19 +7,20 @@ interface Props {
|
||||
const { class: className = '', tone = 'accent' } = Astro.props;
|
||||
|
||||
const toneClasses = {
|
||||
accent: 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]',
|
||||
neutral: 'bg-white/75 text-[var(--color-ink-800)]',
|
||||
signal: 'bg-[rgba(59,139,120,0.14)] text-[var(--color-signal)]',
|
||||
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]',
|
||||
neutral: 'bg-white/78 text-[var(--color-ink-800)]',
|
||||
signal: 'bg-[var(--surface-trust)] text-[var(--color-signal)]',
|
||||
warm: 'bg-[rgba(175,109,67,0.14)] text-[var(--color-warm)]',
|
||||
};
|
||||
---
|
||||
|
||||
<span
|
||||
class:list={[
|
||||
'inline-flex w-fit items-center rounded-full px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[0.18em]',
|
||||
'inline-flex w-fit items-center rounded-[var(--radius-pill)] px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
data-badge-tone={tone}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
@ -24,19 +24,19 @@ const {
|
||||
} = Astro.props;
|
||||
|
||||
const baseClass =
|
||||
'inline-flex items-center justify-center rounded-full border font-semibold tracking-[-0.01em] transition duration-150 focus-visible:outline-none';
|
||||
'inline-flex items-center justify-center rounded-[var(--radius-pill)] border font-semibold tracking-[var(--tracking-tight)] transition duration-150';
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'min-h-10 px-4 text-sm',
|
||||
md: 'min-h-11 px-5 text-sm sm:text-[0.95rem]',
|
||||
lg: 'min-h-12 px-6 text-[0.95rem]',
|
||||
lg: 'min-h-12 px-6 text-[0.97rem]',
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
'border-transparent bg-[var(--color-ink-900)] text-white shadow-[0_18px_38px_rgba(17,36,58,0.16)] hover:bg-[var(--color-brand)]',
|
||||
'border-transparent bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-[var(--shadow-inline)] hover:bg-[var(--color-brand-700)]',
|
||||
secondary:
|
||||
'border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)] hover:border-[var(--color-ink-900)] hover:bg-white',
|
||||
'border-[color:var(--color-border)] bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] hover:border-[var(--color-border-strong)] hover:bg-white',
|
||||
ghost: 'border-transparent bg-transparent text-[var(--color-ink-800)] hover:bg-white/70',
|
||||
};
|
||||
|
||||
@ -45,11 +45,27 @@ const classes = [baseClass, sizeClasses[size], variantClasses[variant], classNam
|
||||
|
||||
{
|
||||
href ? (
|
||||
<a href={href} target={target} rel={rel} aria-label={ariaLabel} class:list={classes}>
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel={rel}
|
||||
aria-label={ariaLabel}
|
||||
class:list={classes}
|
||||
data-button-variant={variant}
|
||||
data-cta-weight={variant}
|
||||
data-interaction="button"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
) : (
|
||||
<button type={type} aria-label={ariaLabel} class:list={classes}>
|
||||
<button
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
class:list={classes}
|
||||
data-button-variant={variant}
|
||||
data-cta-weight={variant}
|
||||
data-interaction="button"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
)
|
||||
|
||||
@ -8,16 +8,17 @@ interface Props {
|
||||
const { as = 'article', class: className = '', variant = 'default' } = Astro.props;
|
||||
|
||||
const variantClasses = {
|
||||
default: 'glass-panel border border-[color:var(--color-line)] bg-[var(--color-panel)]',
|
||||
accent:
|
||||
'border border-[rgba(47,111,183,0.18)] bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(238,245,252,0.94))] shadow-[var(--shadow-soft)]',
|
||||
subtle:
|
||||
'border border-[rgba(17,36,58,0.08)] bg-[linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.56))]',
|
||||
default: 'surface-card',
|
||||
accent: 'surface-card-accent',
|
||||
subtle: 'surface-card-muted',
|
||||
};
|
||||
|
||||
const Tag = as;
|
||||
---
|
||||
|
||||
<Tag class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], className]}>
|
||||
<Tag
|
||||
class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], className]}
|
||||
data-surface={variant}
|
||||
>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,12 +2,29 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
gap?: 'lg' | 'md' | 'sm';
|
||||
justify?: 'between' | 'end' | 'start';
|
||||
}
|
||||
|
||||
const { as = 'div', class: className = '' } = Astro.props;
|
||||
const {
|
||||
as = 'div',
|
||||
class: className = '',
|
||||
gap = 'md',
|
||||
justify = 'start',
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const gapClasses = {
|
||||
sm: 'gap-[var(--space-cluster-sm)]',
|
||||
md: 'gap-[var(--space-cluster)]',
|
||||
lg: 'gap-[var(--space-cluster-lg)]',
|
||||
};
|
||||
const justifyClasses = {
|
||||
start: 'justify-start',
|
||||
between: 'justify-between',
|
||||
end: 'justify-end',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag class:list={['flex flex-wrap items-center gap-3 sm:gap-4', className]}>
|
||||
<Tag class:list={['flex flex-wrap items-center', gapClasses[gap], justifyClasses[justify], className]}>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,13 +2,31 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
width?: 'content' | 'measure' | 'wide';
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
const { as = 'div', class: className = '', wide = false } = Astro.props;
|
||||
const {
|
||||
as = 'div',
|
||||
class: className = '',
|
||||
width,
|
||||
wide = false,
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const resolvedWidth = width ?? (wide ? 'wide' : 'content');
|
||||
const widthClasses = {
|
||||
content: 'max-w-[var(--content-max-width)]',
|
||||
measure: 'max-w-[var(--reading-max-width)]',
|
||||
wide: 'max-w-[var(--wide-max-width)]',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag class:list={['mx-auto w-full px-5 sm:px-6 lg:px-8', wide ? 'max-w-[80rem]' : 'max-w-6xl', className]}>
|
||||
<Tag
|
||||
class:list={[
|
||||
'mx-auto w-full px-5 sm:px-6 lg:px-8',
|
||||
widthClasses[resolvedWidth],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,17 +2,22 @@
|
||||
interface Props {
|
||||
class?: string;
|
||||
cols?: '2' | '3' | '4';
|
||||
gap?: 'lg' | 'md';
|
||||
}
|
||||
|
||||
const { class: className = '', cols = '3' } = Astro.props;
|
||||
const { class: className = '', cols = '3', gap = 'md' } = Astro.props;
|
||||
|
||||
const colClasses = {
|
||||
'2': 'grid-cols-1 md:grid-cols-2',
|
||||
'3': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3',
|
||||
'4': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-4',
|
||||
};
|
||||
const gapClasses = {
|
||||
md: 'gap-[var(--space-grid)] lg:gap-[var(--space-grid-lg)]',
|
||||
lg: 'gap-6 lg:gap-8',
|
||||
};
|
||||
---
|
||||
|
||||
<div class:list={['grid gap-5 lg:gap-6', colClasses[cols], className]}>
|
||||
<div class:list={['grid', gapClasses[gap], colClasses[cols], className]}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -24,8 +24,9 @@ const {
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
data-interaction="input"
|
||||
class:list={[
|
||||
'min-h-12 w-full rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/90 px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
'min-h-12 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
readonly ? 'cursor-default' : '',
|
||||
className,
|
||||
]}
|
||||
|
||||
@ -2,19 +2,39 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
density?: 'base' | 'compact' | 'spacious';
|
||||
id?: string;
|
||||
muted?: boolean;
|
||||
layer?: '1' | '2' | '3';
|
||||
tone?: 'default' | 'emphasis' | 'muted';
|
||||
}
|
||||
|
||||
const { as = 'section', class: className = '', id, muted = false } = Astro.props;
|
||||
const {
|
||||
as = 'section',
|
||||
class: className = '',
|
||||
density = 'base',
|
||||
id,
|
||||
layer = '2',
|
||||
tone = 'default',
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const densityClasses = {
|
||||
compact: 'section-density-compact',
|
||||
base: 'section-density-base',
|
||||
spacious: 'section-density-spacious',
|
||||
};
|
||||
const toneClasses = {
|
||||
default: '',
|
||||
muted: 'section-shell-muted px-3 sm:px-4',
|
||||
emphasis: 'section-shell-emphasis px-3 sm:px-4',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag
|
||||
id={id}
|
||||
data-disclosure-layer={layer}
|
||||
class:list={[
|
||||
'py-12 sm:py-16 lg:py-20',
|
||||
muted ? 'bg-white/45' : '',
|
||||
densityClasses[density],
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
|
||||
@ -9,6 +9,7 @@ interface Props {
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
width?: 'default' | 'measure' | 'wide';
|
||||
}
|
||||
|
||||
const {
|
||||
@ -17,10 +18,16 @@ const {
|
||||
description,
|
||||
eyebrow,
|
||||
title,
|
||||
width = 'default',
|
||||
} = Astro.props;
|
||||
const widthClasses = {
|
||||
default: 'max-w-3xl',
|
||||
measure: 'max-w-[var(--reading-max-width)]',
|
||||
wide: 'max-w-4xl',
|
||||
};
|
||||
---
|
||||
|
||||
<div class:list={['max-w-3xl', align === 'center' ? 'mx-auto text-center' : '', className]}>
|
||||
<div class:list={[widthClasses[width], align === 'center' ? 'mx-auto text-center' : '', className]}>
|
||||
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
|
||||
<Headline>{title}</Headline>
|
||||
{description && <Lead class="mt-4">{description}</Lead>}
|
||||
|
||||
@ -7,9 +7,9 @@ interface Props {
|
||||
|
||||
const { as = 'div', class: className = '', gap = 'md' } = Astro.props;
|
||||
const gapClasses = {
|
||||
sm: 'flex flex-col gap-3',
|
||||
md: 'flex flex-col gap-5',
|
||||
lg: 'flex flex-col gap-7',
|
||||
sm: 'flex flex-col gap-[var(--space-stack-sm)]',
|
||||
md: 'flex flex-col gap-[var(--space-stack)]',
|
||||
lg: 'flex flex-col gap-[var(--space-stack-lg)]',
|
||||
xl: 'flex flex-col gap-10',
|
||||
};
|
||||
const Tag = as;
|
||||
|
||||
@ -23,8 +23,9 @@ const {
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
data-interaction="textarea"
|
||||
class:list={[
|
||||
'min-h-32 w-full rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/90 px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
'min-h-32 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
readonly ? 'cursor-default' : '',
|
||||
className,
|
||||
]}
|
||||
|
||||
@ -19,8 +19,8 @@ const { description, eyebrow, primary, secondary, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Card variant="accent" class="overflow-hidden">
|
||||
<Container width="wide">
|
||||
<Card variant="accent" class="overflow-hidden" data-cta-section>
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr] lg:items-end">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<div class="flex flex-col gap-3 sm:flex-row lg:justify-end">
|
||||
|
||||
@ -16,8 +16,8 @@ interface Props {
|
||||
const { description, eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<Grid cols="3">
|
||||
|
||||
@ -14,9 +14,9 @@ interface Props {
|
||||
const { eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section class="pt-8 sm:pt-10">
|
||||
<Container wide>
|
||||
<div class="rounded-[1.8rem] border border-[rgba(17,36,58,0.08)] bg-white/55 px-5 py-6 shadow-[var(--shadow-soft)]">
|
||||
<Section class="pt-8 sm:pt-10" density="compact" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="surface-card-muted rounded-[var(--radius-lg)] px-5 py-6">
|
||||
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="space-y-3">
|
||||
{eyebrow && <Badge tone="signal">{eyebrow}</Badge>}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
---
|
||||
import Badge from '@/components/primitives/Badge.astro';
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Cluster from '@/components/primitives/Cluster.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import Metric from '@/components/content/Metric.astro';
|
||||
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import type { HeroContent, MetricItem } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -17,28 +21,24 @@ const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="pt-8 sm:pt-10 lg:pt-14">
|
||||
<Container wide>
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr]">
|
||||
<Container width="wide">
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr]" data-disclosure-layer="1">
|
||||
<Card class="motion-rise overflow-hidden">
|
||||
<div class="space-y-6">
|
||||
<Badge>{hero.eyebrow}</Badge>
|
||||
<div class="space-y-4">
|
||||
<h1 class="max-w-4xl font-[var(--font-display)] text-5xl leading-[0.93] tracking-[-0.04em] text-[var(--color-ink-900)] sm:text-6xl lg:text-7xl">
|
||||
<Headline as="h1" size="display" class="max-w-4xl">
|
||||
{hero.title}
|
||||
</h1>
|
||||
<p class="max-w-3xl text-lg leading-8 text-[var(--color-copy)] sm:text-xl">
|
||||
</Headline>
|
||||
<Lead class="max-w-3xl" size="lead">
|
||||
{hero.description}
|
||||
</p>
|
||||
</Lead>
|
||||
</div>
|
||||
{(hero.primaryCta || hero.secondaryCta) && (
|
||||
<Cluster>
|
||||
<Button href={hero.primaryCta.href} variant={hero.primaryCta.variant ?? 'primary'}>
|
||||
{hero.primaryCta.label}
|
||||
</Button>
|
||||
<Cluster data-cta-cluster gap="md">
|
||||
<PrimaryCTA cta={hero.primaryCta} />
|
||||
{hero.secondaryCta && (
|
||||
<Button href={hero.secondaryCta.href} variant={hero.secondaryCta.variant ?? 'secondary'}>
|
||||
{hero.secondaryCta.label}
|
||||
</Button>
|
||||
<SecondaryCTA cta={hero.secondaryCta} />
|
||||
)}
|
||||
</Cluster>
|
||||
)}
|
||||
@ -75,19 +75,7 @@ const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
|
||||
|
||||
{metrics.length > 0 && (
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
{
|
||||
metrics.map((metric) => (
|
||||
<Card variant="subtle">
|
||||
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">
|
||||
{metric.value}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-brand)]">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">{metric.description}</p>
|
||||
</Card>
|
||||
))
|
||||
}
|
||||
{metrics.map((metric) => <Metric item={metric} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -16,8 +16,8 @@ interface Props {
|
||||
const { description, eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<Grid cols="3">
|
||||
|
||||
@ -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 is the trust-first public site for Microsoft tenant change history, drift visibility, and safer operations.',
|
||||
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.',
|
||||
'Designed to scale into docs, changelog, and deeper resources later.',
|
||||
'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.',
|
||||
|
||||
@ -57,7 +57,7 @@ export const securityPrinciples: TrustPrincipleContent[] = [
|
||||
export const securityTrustNotes: CalloutContent[] = [
|
||||
{
|
||||
eyebrow: 'Substantiated public posture',
|
||||
title: 'Substantiated public posture',
|
||||
title: 'Public posture should stay anchored to product safeguards.',
|
||||
description:
|
||||
'The launch story focuses on product shape and operator safeguards: inventory truth, immutable snapshots, safer restore flows, drift visibility, and review support.',
|
||||
tone: 'accent',
|
||||
|
||||
@ -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.',
|
||||
'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,27 +1,35 @@
|
||||
import { coreRoutes, siteMetadata } from '@/lib/site';
|
||||
import type { PageSeo } from '@/types/site';
|
||||
import { getCanonicalPath, getPageDefinition, publishedSitemapRoutes, siteMetadata } from '@/lib/site';
|
||||
import type { PageFamily, PageRole, PageSeo, ShellTone } from '@/types/site';
|
||||
|
||||
export interface ResolvedSeo extends PageSeo {
|
||||
canonicalUrl: string;
|
||||
family: PageFamily;
|
||||
ogDescription: string;
|
||||
ogTitle: string;
|
||||
pageRole: PageRole;
|
||||
robots: string;
|
||||
shellTone: ShellTone;
|
||||
}
|
||||
|
||||
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 {
|
||||
const definition = getPageDefinition(seo.path);
|
||||
|
||||
return {
|
||||
...seo,
|
||||
canonicalUrl: buildCanonicalUrl(seo.path),
|
||||
family: definition.family,
|
||||
ogDescription: seo.ogDescription ?? seo.description,
|
||||
ogTitle: seo.ogTitle ?? seo.title,
|
||||
pageRole: definition.pageRole,
|
||||
robots: 'index,follow',
|
||||
shellTone: definition.shellTone,
|
||||
};
|
||||
}
|
||||
|
||||
export function sitemapEntries(): string[] {
|
||||
return [...coreRoutes].map((path) => buildCanonicalUrl(path));
|
||||
export async function sitemapEntries(): Promise<string[]> {
|
||||
return [...publishedSitemapRoutes].map((path) => buildCanonicalUrl(path));
|
||||
}
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import type {
|
||||
CollectionName,
|
||||
CtaLink,
|
||||
FooterLead,
|
||||
FooterNavigationGroup,
|
||||
NavigationItem,
|
||||
PageDefinition,
|
||||
PageFamily,
|
||||
ShellTone,
|
||||
SitePath,
|
||||
SiteMetadata,
|
||||
SurfaceAvailability,
|
||||
VisualFoundationContract,
|
||||
} from '@/types/site';
|
||||
|
||||
export const siteMetadata: SiteMetadata = {
|
||||
@ -13,40 +23,48 @@ export const siteMetadata: SiteMetadata = {
|
||||
siteUrl: import.meta.env.PUBLIC_SITE_URL ?? 'https://tenantatlas.example',
|
||||
};
|
||||
|
||||
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.' },
|
||||
];
|
||||
export const visualFoundationContract: VisualFoundationContract = {
|
||||
pageFamilies: ['landing', 'trust', 'content'],
|
||||
ctaHierarchy: ['primary', 'secondary', 'ghost'],
|
||||
requiredColorRoles: [
|
||||
'background',
|
||||
'foreground',
|
||||
'muted',
|
||||
'muted-foreground',
|
||||
'card',
|
||||
'card-foreground',
|
||||
'border',
|
||||
'input',
|
||||
'primary',
|
||||
'primary-foreground',
|
||||
'secondary',
|
||||
'secondary-foreground',
|
||||
'accent',
|
||||
'accent-foreground',
|
||||
'success',
|
||||
'warning',
|
||||
'destructive',
|
||||
'info',
|
||||
],
|
||||
accessibilityBaseline: [
|
||||
'focus-visible',
|
||||
'readable-contrast',
|
||||
'non-color-only-semantics',
|
||||
'navigation-vs-cta-differentiation',
|
||||
'mobile-readable-density',
|
||||
],
|
||||
surfaceLayers: ['page-background', 'default-content', 'card', 'elevated', 'muted-inset', 'highlighted'],
|
||||
};
|
||||
|
||||
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' },
|
||||
],
|
||||
},
|
||||
];
|
||||
type CollectionGatedNavigationItem = NavigationItem & {
|
||||
collection?: CollectionName;
|
||||
};
|
||||
|
||||
interface FooterGroupSeed {
|
||||
collection?: CollectionName;
|
||||
items: CollectionGatedNavigationItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const contactCta: CtaLink = {
|
||||
href: '/contact',
|
||||
@ -55,17 +73,328 @@ export const contactCta: CtaLink = {
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
export const coreRoutes = [
|
||||
const footerLeadByFamily: Record<PageFamily, FooterLead> = {
|
||||
landing: {
|
||||
eyebrow: 'Keep the next move obvious',
|
||||
title: 'Product, trust, progress, and contact should stay connected.',
|
||||
description:
|
||||
'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: '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 diligence into friction.',
|
||||
intent: 'guidance',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Discuss trust requirements',
|
||||
helper: 'Bring current review, legal, or rollout questions into one working conversation.',
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
eyebrow: 'Reading should still progress',
|
||||
title: 'Supporting pages should still route visitors back into the core journey.',
|
||||
description:
|
||||
'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',
|
||||
label: 'Continue the evaluation path',
|
||||
helper: 'Move from the reading surface back into a product or trust conversation.',
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const headerCtaByFamily: Record<PageFamily, CtaLink> = {
|
||||
landing: {
|
||||
href: '/contact',
|
||||
label: 'Request a working session',
|
||||
helper: 'Bring your governance questions, rollout concerns, or evaluation goals.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
trust: {
|
||||
href: '/contact',
|
||||
label: 'Discuss trust requirements',
|
||||
helper: 'Route trust, legal, or rollout questions into one conversation.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
content: {
|
||||
href: '/contact',
|
||||
label: 'Start the contact path',
|
||||
helper: 'Turn the reading path into a concrete next step.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
export const canonicalCoreRoutes: SitePath[] = [
|
||||
'/',
|
||||
'/product',
|
||||
'/solutions',
|
||||
'/security-trust',
|
||||
'/integrations',
|
||||
'/trust',
|
||||
'/changelog',
|
||||
'/contact',
|
||||
'/legal',
|
||||
'/privacy',
|
||||
'/terms',
|
||||
] as const;
|
||||
'/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: '/',
|
||||
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 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;
|
||||
}
|
||||
|
||||
export function getShellTone(path: string): ShellTone {
|
||||
return getPageDefinition(path).shellTone;
|
||||
}
|
||||
|
||||
export function getHeaderCta(path: string): CtaLink {
|
||||
const definition = getPageDefinition(path);
|
||||
|
||||
return {
|
||||
...headerCtaByFamily[definition.family],
|
||||
...definition.headerCta,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFooterLead(path: string): FooterLead {
|
||||
const definition = getPageDefinition(path);
|
||||
|
||||
return {
|
||||
...footerLeadByFamily[definition.family],
|
||||
...definition.footerLead,
|
||||
};
|
||||
}
|
||||
|
||||
export function isActiveNavigationPath(currentPath: string, href: string): boolean {
|
||||
if (href === '/') {
|
||||
|
||||
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>
|
||||
@ -1,6 +1,7 @@
|
||||
---
|
||||
import ContactPanel from '@/components/content/ContactPanel.astro';
|
||||
import DemoPrompt from '@/components/content/DemoPrompt.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import RichText from '@/components/content/RichText.astro';
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
@ -30,8 +31,8 @@ import {
|
||||
calloutDescription="The page should make it obvious who should reach out, why, and what a useful first exchange looks like."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<Grid cols="3">
|
||||
<ContactPanel cta={contactHero.primaryCta} points={contactTopics} title="Who should get in touch" />
|
||||
{contactPrompts.map((prompt) => <DemoPrompt title={prompt.title} description={prompt.description} />)}
|
||||
@ -39,13 +40,13 @@ import {
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,0.8fr]">
|
||||
<Card>
|
||||
<SectionHeader
|
||||
eyebrow="Suggested note"
|
||||
title="Give the team enough context to make the first reply useful."
|
||||
title="Structure the first conversation before anyone shares sensitive context."
|
||||
description="The public path stays static in v0, but it can still help a serious buyer structure the first message."
|
||||
/>
|
||||
<div class="mt-6 space-y-4">
|
||||
@ -57,28 +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">
|
||||
<a
|
||||
href="/privacy"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
href="/terms"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Terms
|
||||
</a>
|
||||
<a
|
||||
href="/legal"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Legal
|
||||
</a>
|
||||
<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: '/imprint', label: 'Imprint', variant: 'secondary' }} size="sm" />
|
||||
</Cluster>
|
||||
<div class="mt-6">
|
||||
<RichText sections={contactLegalSections} />
|
||||
@ -90,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,19 +34,19 @@ import {
|
||||
/>
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Product pillars"
|
||||
title="Explain the product in connected pillars, not isolated promises."
|
||||
description="Each section of the site should help a first-time visitor understand why backup, restore, findings, evidence, and reviews belong together."
|
||||
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}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public proof"
|
||||
title="A credible first reading should answer the buyer’s next two questions before they ask them."
|
||||
description="Why is this product category needed now, and why should anyone trust the story enough to continue?"
|
||||
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 first-glance clarity into the deeper product story."
|
||||
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,15 +20,15 @@ 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>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Current direction"
|
||||
title="Show the systems that shape the product workflow today."
|
||||
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,19 +14,28 @@ 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>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Available now"
|
||||
title="Privacy and terms are published as standalone routes."
|
||||
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,17 +77,17 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Section id="public-legal-notice">
|
||||
<Container wide>
|
||||
<Section density="compact" layer="3">
|
||||
<Container width="measure">
|
||||
<RichText sections={legalNoticeSections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<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>
|
||||
|
||||
@ -15,17 +15,17 @@ import { privacyHero, privacySections, privacySeo } from '@/content/pages/privac
|
||||
calloutDescription="The page explains the website and inquiry path clearly without pretending to be the product’s full data-processing documentation."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="measure">
|
||||
<RichText sections={privacySections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<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,18 +27,18 @@ import {
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Connected governance model"
|
||||
title="Treat the product as one operating system for safer tenant change management."
|
||||
description="This page explains how the pieces fit together so visitors do not mistake the product for a loose collection of backup, reporting, and restore features."
|
||||
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}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Narrative"
|
||||
title="Explain the operator journey, not just the capabilities."
|
||||
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="Operational trust starts with the way the product handles risky decisions."
|
||||
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>
|
||||
<Container wide>
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public messaging"
|
||||
title="Substantiated public posture"
|
||||
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,17 +19,17 @@ 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>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Operating models"
|
||||
title="Separate the delivery context clearly."
|
||||
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>
|
||||
|
||||
@ -15,17 +15,17 @@ import { termsHero, termsSections, termsSeo } from '@/content/pages/terms';
|
||||
calloutDescription="The page keeps the public website honest about what it can explain and what still belongs in later commercial/legal paperwork."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="measure">
|
||||
<RichText sections={termsSections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<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>
|
||||
@ -1,28 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "./tokens.css";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--color-ink-900: #11243a;
|
||||
--color-ink-800: #233a53;
|
||||
--color-copy: #42556a;
|
||||
--color-line: rgba(17, 36, 58, 0.14);
|
||||
--color-panel: rgba(255, 255, 255, 0.82);
|
||||
--color-panel-strong: rgba(255, 255, 255, 0.95);
|
||||
--color-panel-soft: rgba(243, 247, 251, 0.86);
|
||||
--color-brand: #2f6fb7;
|
||||
--color-brand-soft: rgba(47, 111, 183, 0.12);
|
||||
--color-signal: #3b8b78;
|
||||
--color-warm: #af6d43;
|
||||
--shadow-panel: 0 24px 80px rgba(17, 36, 58, 0.12);
|
||||
--shadow-soft: 0 18px 48px rgba(17, 36, 58, 0.08);
|
||||
}
|
||||
|
||||
html {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.92), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(92, 149, 215, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #f6f3ee 0%, #edf2f7 56%, #f3f7fb 100%);
|
||||
background: var(--color-background);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@ -30,7 +10,10 @@ body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-ink-900);
|
||||
color: var(--color-foreground);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.92), transparent 30%),
|
||||
linear-gradient(180deg, var(--color-background) 0%, var(--color-background-elevated) 48%, var(--color-muted) 100%);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@ -55,10 +38,10 @@ code {
|
||||
|
||||
::selection {
|
||||
background: rgba(47, 111, 183, 0.18);
|
||||
color: var(--color-ink-900);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
:where(a, button, input, textarea, summary):focus-visible {
|
||||
outline: 3px solid rgba(47, 111, 183, 0.32);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@ -67,28 +50,46 @@ main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.surface-shell {
|
||||
.foundation-page {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.surface-shell::before {
|
||||
.foundation-page::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
content: "";
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.65), transparent 16%),
|
||||
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.7), transparent 28%);
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.68), transparent 16%),
|
||||
radial-gradient(circle at 20% 18%, rgba(255, 255, 255, 0.72), transparent 28%);
|
||||
}
|
||||
|
||||
.surface-shell::after {
|
||||
.foundation-page[data-shell-tone='brand']::before {
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(255, 255, 255, 0.82), transparent 30%),
|
||||
radial-gradient(circle at 86% 0%, rgba(47, 111, 183, 0.18), transparent 28%),
|
||||
radial-gradient(circle at 72% 22%, rgba(59, 139, 120, 0.12), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.62), transparent 18%);
|
||||
}
|
||||
|
||||
.foundation-page[data-shell-tone='trust']::before {
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, rgba(255, 255, 255, 0.8), transparent 28%),
|
||||
radial-gradient(circle at 82% 8%, rgba(59, 139, 120, 0.16), transparent 30%),
|
||||
radial-gradient(circle at 74% 30%, rgba(175, 109, 67, 0.08), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.62), transparent 18%);
|
||||
}
|
||||
|
||||
.foundation-page::after {
|
||||
position: absolute;
|
||||
inset: 1rem;
|
||||
inset: clamp(0.7rem, 1vw, 1.2rem);
|
||||
z-index: -1;
|
||||
content: "";
|
||||
border: 1px solid rgba(17, 36, 58, 0.04);
|
||||
border-radius: 2rem;
|
||||
border: 1px solid var(--color-frame);
|
||||
border-radius: calc(var(--radius-panel) + 0.45rem);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
@ -97,8 +98,8 @@ .skip-link {
|
||||
left: 1rem;
|
||||
z-index: 40;
|
||||
transform: translateY(-200%);
|
||||
border-radius: 999px;
|
||||
background: var(--color-ink-900);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-foreground);
|
||||
padding: 0.75rem 1rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
@ -109,14 +110,83 @@ .skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: linear-gradient(180deg, var(--color-panel-strong), var(--color-panel));
|
||||
box-shadow: var(--shadow-panel);
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.site-shell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shell-panel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
background: linear-gradient(180deg, var(--surface-shell-strong), var(--surface-shell));
|
||||
box-shadow: var(--shadow-panel-strong);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 1px solid rgba(17, 36, 58, 0.08);
|
||||
.glass-panel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
background: linear-gradient(180deg, var(--surface-shell-strong), var(--surface-shell));
|
||||
box-shadow: var(--shadow-panel-strong);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
border: 1px solid var(--color-border);
|
||||
background: linear-gradient(180deg, var(--color-card), var(--surface-card-soft));
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.surface-card-muted {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
background: linear-gradient(180deg, var(--surface-muted-strong), var(--surface-muted));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.surface-card-accent {
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: linear-gradient(180deg, var(--surface-accent-strong), var(--surface-accent));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.section-density-compact {
|
||||
padding-block: var(--space-section-compact);
|
||||
}
|
||||
|
||||
.section-density-base {
|
||||
padding-block: var(--space-section);
|
||||
}
|
||||
|
||||
.section-density-spacious {
|
||||
padding-block: var(--space-section-spacious);
|
||||
}
|
||||
|
||||
.section-shell-muted {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(243, 247, 251, 0.4));
|
||||
border-radius: calc(var(--radius-lg) + 0.15rem);
|
||||
}
|
||||
|
||||
.section-shell-emphasis {
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.56), rgba(47, 111, 183, 0.05));
|
||||
border-radius: calc(var(--radius-lg) + 0.15rem);
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: var(--color-foreground);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(17, 36, 58, 0.2);
|
||||
text-underline-offset: 0.3em;
|
||||
transition:
|
||||
color 140ms ease,
|
||||
text-decoration-color 140ms ease;
|
||||
}
|
||||
|
||||
.text-link:hover {
|
||||
color: var(--color-primary);
|
||||
text-decoration-color: rgba(47, 111, 183, 0.35);
|
||||
}
|
||||
|
||||
.legal-prose p {
|
||||
@ -140,6 +210,21 @@ .legal-prose li + li {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='1'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='2'] {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='3'] {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.motion-rise {
|
||||
animation: rise-in 520ms ease both;
|
||||
}
|
||||
|
||||
@ -3,17 +3,117 @@ @theme {
|
||||
--font-display: "Iowan Old Style", "Palatino Linotype", serif;
|
||||
--font-mono: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
|
||||
--color-shell-50: oklch(0.985 0.01 86);
|
||||
--color-shell-100: oklch(0.97 0.015 85);
|
||||
--color-shell-200: oklch(0.935 0.025 84);
|
||||
--color-shell-300: oklch(0.88 0.04 78);
|
||||
--color-shell-900: oklch(0.19 0.03 65);
|
||||
--color-shell-950: oklch(0.14 0.025 62);
|
||||
--color-brand-400: oklch(0.76 0.09 210);
|
||||
--color-brand-500: oklch(0.68 0.11 218);
|
||||
--color-brand-700: oklch(0.49 0.11 228);
|
||||
--color-signal-400: oklch(0.78 0.1 162);
|
||||
--color-signal-700: oklch(0.52 0.08 170);
|
||||
--color-warm-300: oklch(0.87 0.05 53);
|
||||
--color-warm-500: oklch(0.69 0.09 48);
|
||||
--color-stone-50: oklch(0.986 0.008 86);
|
||||
--color-stone-100: oklch(0.974 0.012 84);
|
||||
--color-stone-150: oklch(0.958 0.016 82);
|
||||
--color-stone-200: oklch(0.936 0.018 79);
|
||||
--color-stone-300: oklch(0.896 0.025 76);
|
||||
--color-ink-700: oklch(0.4 0.04 244);
|
||||
--color-ink-800: oklch(0.31 0.04 248);
|
||||
--color-ink-900: oklch(0.23 0.038 252);
|
||||
--color-brand-300: oklch(0.84 0.05 228);
|
||||
--color-brand-400: oklch(0.76 0.09 214);
|
||||
--color-brand-500: oklch(0.67 0.12 226);
|
||||
--color-brand-700: oklch(0.48 0.1 232);
|
||||
--color-mint-300: oklch(0.85 0.05 182);
|
||||
--color-mint-500: oklch(0.72 0.07 186);
|
||||
--color-mint-700: oklch(0.54 0.07 184);
|
||||
--color-amber-300: oklch(0.88 0.045 73);
|
||||
--color-amber-500: oklch(0.75 0.085 62);
|
||||
--color-amber-700: oklch(0.56 0.09 54);
|
||||
--color-red-500: oklch(0.62 0.16 28);
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--color-background: var(--color-stone-50);
|
||||
--color-background-elevated: var(--color-stone-100);
|
||||
--color-foreground: var(--color-ink-900);
|
||||
--color-muted: var(--color-stone-150);
|
||||
--color-muted-foreground: var(--color-ink-700);
|
||||
--color-card: rgba(255, 255, 255, 0.9);
|
||||
--color-card-foreground: var(--color-ink-900);
|
||||
--color-border: rgba(17, 36, 58, 0.12);
|
||||
--color-border-strong: rgba(47, 111, 183, 0.22);
|
||||
--color-border-subtle: rgba(17, 36, 58, 0.07);
|
||||
--color-frame: rgba(17, 36, 58, 0.06);
|
||||
--color-input: rgba(255, 255, 255, 0.94);
|
||||
--color-primary: var(--color-brand-500);
|
||||
--color-primary-foreground: #f9fbff;
|
||||
--color-secondary: rgba(255, 255, 255, 0.82);
|
||||
--color-secondary-foreground: var(--color-ink-900);
|
||||
--color-accent: rgba(47, 111, 183, 0.1);
|
||||
--color-accent-foreground: var(--color-brand-700);
|
||||
--color-success: var(--color-mint-700);
|
||||
--color-warning: var(--color-amber-700);
|
||||
--color-destructive: var(--color-red-500);
|
||||
--color-info: var(--color-brand-700);
|
||||
|
||||
--surface-page: rgba(255, 255, 255, 0.34);
|
||||
--surface-shell: rgba(255, 255, 255, 0.78);
|
||||
--surface-shell-strong: rgba(255, 255, 255, 0.94);
|
||||
--surface-card-soft: rgba(246, 248, 251, 0.82);
|
||||
--surface-muted: rgba(243, 247, 251, 0.88);
|
||||
--surface-muted-strong: rgba(247, 249, 252, 0.94);
|
||||
--surface-accent: rgba(47, 111, 183, 0.1);
|
||||
--surface-accent-strong: rgba(241, 246, 253, 0.98);
|
||||
--surface-trust: rgba(59, 139, 120, 0.09);
|
||||
|
||||
--radius-sm: 1rem;
|
||||
--radius-md: 1.35rem;
|
||||
--radius-lg: 1.75rem;
|
||||
--radius-panel: 2rem;
|
||||
--radius-pill: 999px;
|
||||
|
||||
--shadow-panel-strong: 0 28px 90px rgba(17, 36, 58, 0.14);
|
||||
--shadow-card: 0 20px 56px rgba(17, 36, 58, 0.1);
|
||||
--shadow-soft: 0 12px 36px rgba(17, 36, 58, 0.08);
|
||||
--shadow-inline: 0 10px 22px rgba(17, 36, 58, 0.08);
|
||||
|
||||
--space-page-x: clamp(1.25rem, 2vw, 2.5rem);
|
||||
--space-page-y: clamp(4rem, 6vw, 6rem);
|
||||
--space-section-compact: clamp(3rem, 4vw, 4.25rem);
|
||||
--space-section: clamp(4rem, 6vw, 5.75rem);
|
||||
--space-section-spacious: clamp(5rem, 7vw, 7rem);
|
||||
--space-cluster-sm: 0.75rem;
|
||||
--space-cluster: 1rem;
|
||||
--space-cluster-lg: 1.5rem;
|
||||
--space-stack-sm: 0.75rem;
|
||||
--space-stack: 1.25rem;
|
||||
--space-stack-lg: 1.75rem;
|
||||
--space-grid: 1.25rem;
|
||||
--space-grid-lg: 1.75rem;
|
||||
|
||||
--content-max-width: 76rem;
|
||||
--wide-max-width: 84rem;
|
||||
--reading-max-width: 68rem;
|
||||
|
||||
--type-display-size: clamp(3.3rem, 6vw, 5rem);
|
||||
--type-page-size: clamp(2.75rem, 4.2vw, 3.75rem);
|
||||
--type-section-size: clamp(2rem, 3.5vw, 3rem);
|
||||
--type-card-size: clamp(1.35rem, 2vw, 1.85rem);
|
||||
--type-body-size: 1.02rem;
|
||||
--type-small-size: 0.94rem;
|
||||
--type-eyebrow-size: 0.74rem;
|
||||
--type-helper-size: 0.82rem;
|
||||
--tracking-display: -0.055em;
|
||||
--tracking-tight: -0.03em;
|
||||
--tracking-eyebrow: 0.18em;
|
||||
--line-display: 0.95;
|
||||
--line-heading: 1.02;
|
||||
--line-body: 1.75;
|
||||
--line-tight: 1.45;
|
||||
|
||||
/* Legacy aliases used by existing primitives and content blocks. */
|
||||
--color-copy: var(--color-muted-foreground);
|
||||
--color-line: var(--color-border);
|
||||
--color-panel: var(--surface-shell);
|
||||
--color-panel-strong: var(--surface-shell-strong);
|
||||
--color-panel-soft: var(--surface-muted);
|
||||
--color-brand: var(--color-primary);
|
||||
--color-brand-soft: var(--surface-accent);
|
||||
--color-signal: var(--color-success);
|
||||
--color-warm: var(--color-warning);
|
||||
--shadow-panel: var(--shadow-panel-strong);
|
||||
}
|
||||
|
||||
@ -1,12 +1,36 @@
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
|
||||
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'
|
||||
| '/trust'
|
||||
| '/changelog'
|
||||
| '/integrations'
|
||||
| '/solutions'
|
||||
| '/contact'
|
||||
| '/legal'
|
||||
| '/privacy'
|
||||
| '/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;
|
||||
@ -22,11 +46,39 @@ export interface NavigationItem {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceAvailability {
|
||||
articles: boolean;
|
||||
changelog: boolean;
|
||||
resources: boolean;
|
||||
}
|
||||
|
||||
export interface FooterNavigationGroup {
|
||||
items: NavigationItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface FooterLead {
|
||||
description: string;
|
||||
eyebrow: string;
|
||||
intent: FooterIntent;
|
||||
primaryCta: CtaLink;
|
||||
title: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
siteDescription: string;
|
||||
siteName: string;
|
||||
@ -103,3 +155,11 @@ export interface LegalSection {
|
||||
bullets?: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface VisualFoundationContract {
|
||||
accessibilityBaseline: readonly string[];
|
||||
ctaHierarchy: readonly ButtonVariant[];
|
||||
pageFamilies: readonly PageFamily[];
|
||||
requiredColorRoles: readonly string[];
|
||||
surfaceLayers: readonly string[];
|
||||
}
|
||||
|
||||
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,40 +1,66 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
coreRoutePaths,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
openMobileNavigation,
|
||||
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');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: 'Structure the first conversation before anyone shares sensitive context.',
|
||||
}),
|
||||
).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 expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
|
||||
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: 'Keep the legal baseline explicit without promoting it into the main navigation.' }),
|
||||
).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);
|
||||
|
||||
@ -50,8 +76,12 @@ test.describe('mobile navigation', () => {
|
||||
test('mobile menu exposes the published contact and legal paths', async ({ page }) => {
|
||||
await visitPage(page, '/');
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,39 +1,56 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCtaHierarchy,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('home explains the product category and exposes the next step', async ({ page }) => {
|
||||
test('home uses the landing foundation to explain the product category with one clear action hierarchy', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/');
|
||||
await expectShell(page, /TenantAtlas/);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Governance of record for Microsoft tenant operations.' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'See the product model' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('main').getByRole('link', { name: 'Review the trust posture' }).first(),
|
||||
page.getByRole('heading', {
|
||||
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' });
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(skipLink).toBeFocused();
|
||||
});
|
||||
|
||||
test('product explains the connected operating model instead of a loose feature list', async ({
|
||||
test('product keeps the connected operating model readable without collapsing into a feature list', async ({
|
||||
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');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(page.getByRole('heading', { name: 'Connected governance model' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'See audience fit' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('main')
|
||||
.getByRole('link', { name: 'Talk through your current operating model' })
|
||||
.first(),
|
||||
page.getByRole('heading', { name: 'Explain what the product does before asking for buyer trust.' }),
|
||||
).toBeVisible();
|
||||
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();
|
||||
@ -22,17 +33,32 @@ export async function expectShell(page: Page, heading: string | RegExp): Promise
|
||||
await expect(page.getByRole('heading', { level: 1, name: heading })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectPageFamily(page: Page, family: 'content' | 'landing' | 'trust'): Promise<void> {
|
||||
await expect(page.locator(`[data-page-family="${family}"]`).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectPrimaryNavigation(page: Page): Promise<void> {
|
||||
const header = page.getByRole('banner');
|
||||
|
||||
for (const label of primaryNavigationLabels) {
|
||||
await expect(header.getByRole('link', { name: label })).toBeVisible();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,3 +69,27 @@ export async function openMobileNavigation(page: Page): Promise<void> {
|
||||
await menuTrigger.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectDisclosureLayer(page: Page, layer: '1' | '2' | '3'): Promise<void> {
|
||||
await expect(page.locator(`[data-disclosure-layer="${layer}"]`).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectCtaHierarchy(
|
||||
page: Page,
|
||||
primaryLabel: string | RegExp,
|
||||
secondaryLabel: string | RegExp,
|
||||
): Promise<void> {
|
||||
const main = page.getByRole('main');
|
||||
|
||||
await expect(main.locator('[data-cta-weight="primary"]').filter({ hasText: primaryLabel }).first()).toBeVisible();
|
||||
await expect(
|
||||
main.locator('[data-cta-weight="secondary"]').filter({ hasText: secondaryLabel }).first(),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectNavigationVsCtaDifferentiation(page: Page): Promise<void> {
|
||||
const header = page.getByRole('banner');
|
||||
|
||||
await expect(header.locator('[data-nav-link]').first()).toBeVisible();
|
||||
await expect(header.locator('[data-cta-weight="secondary"]').first()).toBeVisible();
|
||||
}
|
||||
|
||||
@ -1,36 +1,67 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCompatibilityRedirect,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('solutions separates MSP and enterprise fit clearly', async ({ page }) => {
|
||||
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 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: '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', async ({ page }) => {
|
||||
await visitPage(page, '/security-trust');
|
||||
await expectShell(page, /trust posture|trust-first/i);
|
||||
test('security and trust stays grounded in substantiated product posture and layered disclosure', async ({
|
||||
page,
|
||||
}) => {
|
||||
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');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(page.getByRole('heading', { name: 'Substantiated public posture' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the legal surface' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Ground public trust claims in operator safeguards and explicit boundaries.' }),
|
||||
).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('integrations shows real ecosystem direction without wishlist claims', async ({ page }) => {
|
||||
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, '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 fit visible without pretending it belongs in primary navigation.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('Microsoft Graph')).toBeVisible();
|
||||
await expect(page.getByText('Entra ID')).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Plan the working session' }).first()).toBeVisible();
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCtaHierarchy,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('representative pages route CTA, badge, surface, and input semantics through shared primitives', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/');
|
||||
await expectShell(page, /TenantAtlas/);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectCtaHierarchy(page, 'See the product model', 'Review the trust posture');
|
||||
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, '/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, /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(
|
||||
page.locator('[data-button-variant="secondary"]').filter({ hasText: 'Privacy' }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
# Specification Quality Checklist: Website Visual Foundation
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-18
|
||||
**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 passed after refining the success criteria to keep the measurable outcomes technology-agnostic.
|
||||
- The specification remains strictly local to `apps/website` and introduces no obligations for `apps/platform`.
|
||||
@ -0,0 +1,178 @@
|
||||
version: 0.1.0
|
||||
type: design-foundation
|
||||
feature: 214-website-visual-foundation
|
||||
title: Website Visual Foundation Contract
|
||||
summary: >-
|
||||
Website-local visual contract for apps/website. This feature does not add or
|
||||
change HTTP/API surfaces; the contract defines the semantic design rules that
|
||||
implementation must satisfy.
|
||||
scope:
|
||||
app: apps/website
|
||||
includes:
|
||||
- visual direction
|
||||
- design tokens
|
||||
- typography roles
|
||||
- spacing rules
|
||||
- surface rules
|
||||
- interaction semantics
|
||||
- semantic website primitives
|
||||
- shadcn usage constraints
|
||||
- page consistency rules
|
||||
excludes:
|
||||
- apps/platform
|
||||
- Filament theming
|
||||
- shared cross-surface design system
|
||||
- API contracts
|
||||
- logo or brand redesign
|
||||
visual_direction:
|
||||
target_tone:
|
||||
- clear
|
||||
- calm
|
||||
- precise
|
||||
- trustworthy
|
||||
- modern
|
||||
- high-quality
|
||||
- operationally serious
|
||||
disallowed_traits:
|
||||
- playful
|
||||
- neon-heavy
|
||||
- glass-heavy-by-default
|
||||
- over-animated
|
||||
- loud
|
||||
- generic-startup-template
|
||||
token_roles:
|
||||
color:
|
||||
required:
|
||||
- background
|
||||
- foreground
|
||||
- muted
|
||||
- muted-foreground
|
||||
- card
|
||||
- card-foreground
|
||||
- border
|
||||
- input
|
||||
- primary
|
||||
- primary-foreground
|
||||
- secondary
|
||||
- secondary-foreground
|
||||
- accent
|
||||
- accent-foreground
|
||||
- success
|
||||
- warning
|
||||
- destructive
|
||||
- info
|
||||
rules:
|
||||
- neutrals-carry-most-of-the-surface
|
||||
- primary-used-sparingly
|
||||
- status-colors-semantic-only
|
||||
typography:
|
||||
required:
|
||||
- display
|
||||
- page-heading
|
||||
- section-heading
|
||||
- card-heading
|
||||
- body
|
||||
- small
|
||||
- eyebrow
|
||||
- helper
|
||||
spacing:
|
||||
required_levels:
|
||||
- page
|
||||
- section
|
||||
- component
|
||||
surface:
|
||||
required:
|
||||
- page-background
|
||||
- default-content
|
||||
- card
|
||||
- elevated
|
||||
- muted-inset
|
||||
- highlighted
|
||||
radius:
|
||||
preferred_scale:
|
||||
- small
|
||||
- medium
|
||||
- large
|
||||
interaction:
|
||||
buttons:
|
||||
required:
|
||||
- primary
|
||||
- secondary
|
||||
- ghost
|
||||
rules:
|
||||
- no-multiple-equally-loud-ctas-by-default
|
||||
- primary-not-inflationary
|
||||
links:
|
||||
rules:
|
||||
- navigation-vs-inline-intent-must-be-clear
|
||||
inputs:
|
||||
rules:
|
||||
- shared-height-logic
|
||||
- shared-border-logic
|
||||
- shared-focus-logic
|
||||
- clear-error-states
|
||||
primitives:
|
||||
navigation:
|
||||
- header
|
||||
- nav-links
|
||||
- cta-slot
|
||||
- mobile-nav
|
||||
hero:
|
||||
- eyebrow
|
||||
- main-headline
|
||||
- supporting-copy
|
||||
- primary-cta
|
||||
- secondary-cta
|
||||
- trust-supporting-area
|
||||
section:
|
||||
- section-wrapper
|
||||
- section-intro
|
||||
- section-title
|
||||
- section-body
|
||||
- optional-aside
|
||||
grouping:
|
||||
- card
|
||||
- feature-row
|
||||
- comparison-block
|
||||
- callout
|
||||
- quote-frame
|
||||
- logos-bar
|
||||
- stat-block
|
||||
trust:
|
||||
- compliance-callout
|
||||
- evidence-block
|
||||
- changelog-news-block
|
||||
- contact-request-demo-block
|
||||
shadcn_usage:
|
||||
mode: adapted-locally
|
||||
allowed:
|
||||
- use-as-pattern-reference
|
||||
- adapt-into-local-astro-primitives
|
||||
- reduce-or-normalize-default-behavior
|
||||
forbidden:
|
||||
- unreviewed-default-styling-adoption
|
||||
- component-library-driven-page-design
|
||||
- local-token-bypass-overrides
|
||||
- implicit-react-runtime-requirement
|
||||
page_consistency:
|
||||
applies_to:
|
||||
- landing-product
|
||||
- trust-legal
|
||||
- content-heavy
|
||||
rules:
|
||||
- shared-content-width-strategy
|
||||
- shared-heading-hierarchy
|
||||
- shared-section-rhythm
|
||||
- shared-cta-placement-logic
|
||||
- shared-card-and-callout-behavior
|
||||
- shared-footer-structure
|
||||
- shared-link-emphasis-rules
|
||||
validation:
|
||||
required_commands:
|
||||
- corepack pnpm build:website
|
||||
- cd apps/website && corepack pnpm exec playwright test
|
||||
review_checks:
|
||||
- representative-pages-use-shared-foundation
|
||||
- no-platform-coupling-introduced
|
||||
- no-react-or-radix-required-by-default
|
||||
- navigation-and-cta-hierarchy-remain-clear
|
||||
193
specs/214-website-visual-foundation/data-model.md
Normal file
193
specs/214-website-visual-foundation/data-model.md
Normal file
@ -0,0 +1,193 @@
|
||||
# Data Model: Website Visual Foundation
|
||||
|
||||
## Overview
|
||||
|
||||
This feature introduces no database schema. The model is a website-local design contract expressed through tokens, primitive semantics, and page-family consistency rules inside `apps/website`.
|
||||
|
||||
## Entities
|
||||
|
||||
### Website Visual Foundation
|
||||
|
||||
- **Purpose**: The canonical visual contract for `apps/website`.
|
||||
- **Key fields**:
|
||||
- `scope` (`apps/website` only)
|
||||
- `toneKeywords`
|
||||
- `disallowedTraits`
|
||||
- `pageFamilies`
|
||||
- `accessibilityBaseline`
|
||||
- `shadcnUsageMode`
|
||||
- **Relationships**:
|
||||
- Owns many `Token Role` entries
|
||||
- Owns many `Typography Role` entries
|
||||
- Owns many `Spacing Rule` entries
|
||||
- Owns many `Surface Rule` entries
|
||||
- Owns many `Interaction Contract` entries
|
||||
- Owns many `Primitive Contract` entries
|
||||
- Owns many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Must remain explicitly local to `apps/website`
|
||||
- Must explicitly exclude `apps/platform`, Filament theming, and shared cross-surface obligations
|
||||
- Must define both desired tone and disallowed visual patterns
|
||||
|
||||
### Token Role
|
||||
|
||||
- **Purpose**: A semantic token used to express color, surface, border, radius, or shadow meaning.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `category` (`color`, `surface`, `border`, `radius`, `shadow`)
|
||||
- `semanticPurpose`
|
||||
- `defaultValueSource`
|
||||
- `allowedContexts`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be referenced by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required color roles include `background`, `foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `border`, `input`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `success`, `warning`, `destructive`, and `info`
|
||||
- Primaries must be used intentionally and not as the dominant surface color
|
||||
- Status roles must remain semantic rather than decorative
|
||||
|
||||
### Typography Role
|
||||
|
||||
- **Purpose**: A named text role that governs hierarchy and readability.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `intent` (`display`, `page`, `section`, `card`, `body`, `small`, `eyebrow`, `helper`)
|
||||
- `fontFamily`
|
||||
- `weight`
|
||||
- `tracking`
|
||||
- `lineHeight`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be referenced by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required roles include display or hero, page, section, card, body, small, eyebrow or label, and UI label or helper text
|
||||
- Body and small text must stay readable on long-form pages
|
||||
- Headlines must read as calm and precise rather than aggressive or novelty-first
|
||||
|
||||
### Spacing Rule
|
||||
|
||||
- **Purpose**: A repeatable spacing decision at page, section, or component level.
|
||||
- **Key fields**:
|
||||
- `level` (`page`, `section`, `component`)
|
||||
- `density`
|
||||
- `gapStrategy`
|
||||
- `maxWidthBehavior`
|
||||
- `rhythmIntent`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May constrain many `Primitive Contract` entries
|
||||
- May constrain many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Must define page spacing, section spacing, and component spacing
|
||||
- Must support density shifts between hero-led and text-heavy layouts without abandoning the system
|
||||
- Must avoid page-by-page optical tweaking as the default mechanism
|
||||
|
||||
### Surface Rule
|
||||
|
||||
- **Purpose**: Defines one level of content containment or emphasis.
|
||||
- **Key fields**:
|
||||
- `name` (`page-background`, `default-content`, `card`, `elevated`, `muted-inset`, `highlighted`)
|
||||
- `emphasisLevel`
|
||||
- `borderBehavior`
|
||||
- `shadowBehavior`
|
||||
- `intendedUse`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be used by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Surface differences must communicate structure rather than decoration alone
|
||||
- Cards must group meaningful content, not appear arbitrarily as style wrappers
|
||||
- Shadows must remain restrained and border-led clarity must stay primary
|
||||
|
||||
### Interaction Contract
|
||||
|
||||
- **Purpose**: Governs semantic behavior for interactive elements.
|
||||
- **Key fields**:
|
||||
- `elementType` (`button`, `link`, `input`)
|
||||
- `variantSet`
|
||||
- `focusBehavior`
|
||||
- `errorBehavior`
|
||||
- `emphasisRules`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be used by many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Button hierarchy must at least cover `primary`, `secondary`, and low-emphasis or `ghost`
|
||||
- Links must distinguish navigation intent from inline/supportive CTA behavior
|
||||
- Inputs must share height, border, focus, and error-state logic across the website
|
||||
|
||||
### Primitive Contract
|
||||
|
||||
- **Purpose**: A reusable website building block that carries semantic and visual rules.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `category` (`navigation`, `hero`, `section`, `grouping`, `cta`, `trust`)
|
||||
- `backingComponent`
|
||||
- `allowedVariants`
|
||||
- `requiredTokenRoles`
|
||||
- `defaultSpacingRule`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May reference many `Token Role`, `Typography Role`, `Spacing Rule`, and `Surface Rule` entries
|
||||
- May be evaluated by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required primitive groups include navigation, hero, section, content grouping, CTA, and trust primitives
|
||||
- Primitive APIs must reinforce the foundation instead of allowing unrestricted local overrides
|
||||
- Existing Astro primitives remain the primary implementation targets
|
||||
|
||||
### shadcn Usage Policy
|
||||
|
||||
- **Purpose**: Defines how `shadcn/ui` concepts may enter the website codebase.
|
||||
- **Key fields**:
|
||||
- `mode` (`adapted-locally`)
|
||||
- `allowedUses`
|
||||
- `forbiddenUses`
|
||||
- `reviewExpectations`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- Constrains many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Library patterns must be adapted to website-owned tokens and primitives
|
||||
- Uncontrolled default styling must not become the website’s visual language
|
||||
- The policy must not implicitly require React or a second component runtime
|
||||
|
||||
### Page Consistency Rule
|
||||
|
||||
- **Purpose**: A cross-page rule that keeps the website coherent across route types.
|
||||
- **Key fields**:
|
||||
- `pageFamily`
|
||||
- `contentWidthStrategy`
|
||||
- `headingHierarchy`
|
||||
- `sectionRhythm`
|
||||
- `ctaPlacementLogic`
|
||||
- `footerExpectation`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- Applies to many routes in `apps/website/src/pages`
|
||||
- References many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Must cover at least landing/product, trust/legal, and content-heavy page families
|
||||
- Must enforce one recognizable website rhythm across those page families
|
||||
- Must prevent pages from inventing competing CTA or surface hierarchies
|
||||
|
||||
## Relationship Summary
|
||||
|
||||
- `Website Visual Foundation` owns many `Token Role`
|
||||
- `Website Visual Foundation` owns many `Typography Role`
|
||||
- `Website Visual Foundation` owns many `Spacing Rule`
|
||||
- `Website Visual Foundation` owns many `Surface Rule`
|
||||
- `Website Visual Foundation` owns many `Interaction Contract`
|
||||
- `Website Visual Foundation` owns many `Primitive Contract`
|
||||
- `Website Visual Foundation` owns many `Page Consistency Rule`
|
||||
- `Primitive Contract` references `Token Role`, `Typography Role`, `Spacing Rule`, and `Surface Rule`
|
||||
- `Page Consistency Rule` evaluates routes through the required `Primitive Contract` set
|
||||
- `shadcn Usage Policy` constrains how future primitives are authored or adapted
|
||||
|
||||
## State / Lifecycle Notes
|
||||
|
||||
- No persisted runtime states are introduced.
|
||||
- The foundation is repo-owned truth: it is expressed through shared styles, primitive APIs, and representative page composition rather than a database or application state machine.
|
||||
- A primitive or page is compliant when it maps to the defined foundation entities without inventing a competing local visual rule.
|
||||
183
specs/214-website-visual-foundation/plan.md
Normal file
183
specs/214-website-visual-foundation/plan.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Implementation Plan: Website Visual Foundation
|
||||
|
||||
**Branch**: `214-website-visual-foundation` | **Date**: 2026-04-18 | **Spec**: `specs/214-website-visual-foundation/spec.md`
|
||||
**Input**: Feature specification from `specs/214-website-visual-foundation/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 the existing workspace contracts defined by the website working contract.
|
||||
- Build the visual foundation on the current Astro 6 + Tailwind CSS v4 stack by formalizing semantic token roles, typography hierarchy, spacing rules, surface logic, interaction semantics, and page-level consistency rules.
|
||||
- Keep Astro-native primitives as the canonical implementation surface and treat `shadcn/ui` as an adaptation/reference contract rather than introducing React plus official `shadcn/ui` as a new required runtime layer.
|
||||
- Apply the foundation across representative page families already present in `apps/website` so landing, trust/legal, and content-heavy surfaces read as one controlled enterprise website instead of locally styled variants.
|
||||
|
||||
## 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 component primitives, Playwright browser smoke tests
|
||||
**Storage**: Static filesystem content, styles, assets, and content collections under `apps/website/src` and `apps/website/public`; no database
|
||||
**Testing**: Root build proof via `corepack pnpm build:website` plus Playwright browser 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 public routes, keep reading/navigation flows zero-hydration by default, and keep UI refinements within a low-JS, accessibility-first posture
|
||||
**Constraints**: Preserve `@tenantatlas/website`, `WEBSITE_PORT`, and root `dev:website` / `build:website` workflows; do not introduce platform runtime coupling; do not promote website semantics into a shared cross-surface design system; keep any `shadcn/ui` usage adapted to website-owned tokens and primitives
|
||||
**Scale/Scope**: 9 published public routes, existing layout/primitives/sections/content directories, and one website-local visual foundation spanning landing, trust/legal, and content-heavy page families
|
||||
|
||||
## 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 planned work stays inside `apps/website` and introduces no `/admin`, `/admin/t/{tenant}/...`, or `/system` runtime behavior.
|
||||
- Read/write separation: Pass. The feature changes public presentation and component composition only; it introduces no website write workflow, backend form handler, queue, or remote call path.
|
||||
- Workspace isolation: Pass. The website remains runtime-independent from `apps/platform`, and the plan preserves the explicit website working contract boundaries.
|
||||
- Data minimization: Pass. The feature only reorganizes or extends public styles, content composition, and component semantics; it introduces no tenant data, secrets, auth state, or operational records.
|
||||
- Test governance (TEST-GOV-001): Pass. Validation remains in `fast-feedback` using the existing website build and local Playwright smoke suite, with no database, auth, provider, or heavy-suite defaults.
|
||||
- Proportionality / no premature abstraction: Pass. The plan extends the current Astro/Tailwind foundation with a narrow website-local semantic layer instead of introducing React, a CMS, or a shared website-platform design system.
|
||||
- Persisted truth / new state: Pass. No database schema, persisted entity, queue lifecycle, or new application state family is introduced.
|
||||
- UI semantics / few layers: Pass. The added semantics stay limited to design tokens, primitive contracts, and page-composition rules for the website, not a reusable cross-app interpretation framework.
|
||||
- Website working contract: Pass. The plan keeps changes local to `apps/website` except for preserving existing root workflow compatibility, and it introduces no new API, auth, DTO, or shared-package coupling to `apps/platform`.
|
||||
|
||||
Status: ✅ No constitution violations for this feature. The implementation 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 representative public page families plus root static build proof
|
||||
- **Affected validation lanes**: fast-feedback
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The feature changes runtime website rendering, styling, and composition but stays within a static Astro site. Build proof catches asset and route breakage; a focused Playwright smoke suite catches browser-visible regressions in navigation, CTA visibility, representative content surfaces, and mobile usability without introducing 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 browser assertions remain local to `apps/website/tests/smoke`
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||
- **Closing validation and reviewer handoff**: Re-run the website build and smoke suite after token, primitive, or page-family changes. Reviewers should verify that Home, trust/legal, and another content-heavy route still load cleanly, navigation and footer links remain reachable, CTA hierarchy stays visible, and no unnecessary client framework hydration is introduced.
|
||||
- **Budget / baseline / trend follow-up**: none beyond the existing small website smoke-suite runtime
|
||||
- **Review-stop questions**: Does validation remain fast-feedback only? Did any change introduce hidden React/runtime coupling, broaden the smoke suite beyond representative website flows, or create new platform-facing contracts?
|
||||
- **Escalation path**: document-in-feature
|
||||
- **Why no dedicated follow-up spec is needed**: This feature’s validation scope is tightly bounded to the website’s local rendering and navigation behavior. A separate test-governance spec is only needed if the website later gains interactive workflows, API-backed forms, or broader browser coverage.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/214-website-visual-foundation/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── website-visual-foundation.contract.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, Badge, Card, Input, Textarea, Grid, Stack
|
||||
│ │ ├── sections/ # PageHero, FeatureGrid, TrustGrid, CTASection, LogoStrip
|
||||
│ │ └── content/ # Eyebrow, Headline, Lead, Callout, Metric, audience/trust helpers
|
||||
│ ├── content/ # Route content and future content collections
|
||||
│ ├── layouts/
|
||||
│ │ └── BaseLayout.astro
|
||||
│ ├── lib/ # Site metadata, SEO, and helper config
|
||||
│ ├── pages/ # Published public routes
|
||||
│ ├── styles/
|
||||
│ │ ├── global.css
|
||||
│ │ └── tokens.css
|
||||
│ └── types/
|
||||
└── tests/
|
||||
└── smoke/
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the existing website structure and formalize the design foundation inside the current `styles`, `primitives`, `sections`, `content`, and `layout` layers. The implementation should encode semantic design rules in the Astro-native foundation already present instead of introducing a second component stack.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
None.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Website contributors and reviewers lack a canonical, website-local visual contract, so styling decisions drift across pages and primitives and dilute enterprise trust.
|
||||
- **Existing structure is insufficient because**: The current site already has tokens, gradients, rounded cards, and reusable primitives, but those choices are only partially codified. They do not yet define the semantic token roles, page-family rules, or `shadcn/ui` usage boundaries needed to prevent future drift.
|
||||
- **Narrowest correct implementation**: Extend the current Astro/Tailwind foundation with explicit semantic token mapping, primitive contracts, and page-consistency rules, then apply those rules to representative routes. Do not add a new client framework, platform coupling, or shared website-platform design system.
|
||||
- **Ownership cost created**: Ongoing maintenance of token semantics, shared primitives, and a small browser smoke suite that protects representative public routes.
|
||||
- **Alternative intentionally rejected**: Continuing page-local styling was rejected because it preserves drift; full React plus official `shadcn/ui` was rejected because it adds unnecessary runtime and maintenance cost; a shared cross-surface design system was rejected because the feature is intentionally website-only.
|
||||
- **Release truth**: Current-release truth for `apps/website`
|
||||
|
||||
## Phase 0 — Outline & Research (complete)
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/research.md`
|
||||
- Key decisions captured:
|
||||
- Preserve the website working contract and keep all visual-language work local to `apps/website`.
|
||||
- Build the foundation on semantic token layering in Tailwind CSS v4 and CSS custom properties rather than page-local utility drift.
|
||||
- Keep Astro-native primitives as the canonical implementation surface and adapt any `shadcn/ui` usage to that layer instead of adding React as a new runtime dependency.
|
||||
- Use representative page families already in the repo to drive the foundation: landing, trust/legal, and content-heavy surfaces.
|
||||
- Validate with root build proof plus the existing Playwright smoke suite for representative public routes.
|
||||
|
||||
## Phase 1 — Design & Contracts (complete)
|
||||
|
||||
### Data model
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/data-model.md`
|
||||
- No database schema changes are required; the model is a website-local design contract expressed through token roles, primitive contracts, and page-family consistency rules.
|
||||
|
||||
### Design contract
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/contracts/website-visual-foundation.contract.yaml`
|
||||
- This feature has no HTTP or API contract changes. The contract artifact captures the website-local visual foundation rules that implementation must satisfy.
|
||||
|
||||
### Quickstart
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/quickstart.md`
|
||||
- Quickstart covers local development, representative implementation order, and the required validation commands.
|
||||
|
||||
### Agent context update
|
||||
|
||||
- Completed via `.specify/scripts/bash/update-agent-context.sh copilot` so the Copilot context reflects the current website stack and this feature’s 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, or platform-side runtime concern is introduced.
|
||||
- ✅ The semantic layer remains narrow: tokens, primitives, and page-family rules only.
|
||||
- ✅ Validation remains cheap, representative, and website-specific.
|
||||
|
||||
## Phase 2 — Implementation Plan (next)
|
||||
|
||||
### Story 1 (P1): Semantic token and rule foundation
|
||||
|
||||
- Normalize the existing palette and CSS variables into an explicit semantic role model for colors, surfaces, borders, shadows, radius, and typography.
|
||||
- Encode the spacing model and section rhythm into the current Astro foundation so `Container`, `Section`, `SectionHeader`, `Card`, `Button`, `Input`, and `Textarea` expose one canonical visual language.
|
||||
- Keep the site static and light-first, with focus states, contrast, and mobile readability treated as foundation rules rather than page-local cleanup.
|
||||
- Tests / validation:
|
||||
- Confirm the website still builds via `corepack pnpm build:website`.
|
||||
- Re-run smoke coverage for Home and Product, explicitly proving focus visibility, readable contrast, non-color-only semantics, and clear navigation-vs-CTA differentiation.
|
||||
|
||||
### Story 2 (P1): Representative page-family alignment
|
||||
|
||||
- Apply the foundation consistently across representative page families already present in the app: landing (`/` or `/product`), trust/legal (`/security-trust`, `/privacy`, `/terms`), and other content-heavy routes.
|
||||
- Reduce style drift in hero, section intro, callout, stat, and card usage so CTA emphasis, section spacing, surface elevation, and progressive-disclosure layering behave consistently across routes.
|
||||
- Ensure the footer, navigation shell, and CTA placement logic match the foundation’s page-level rules.
|
||||
- Tests / validation:
|
||||
- Extend or adjust browser smoke coverage to assert representative headings, CTA visibility, shell/navigation stability, and progressive-disclosure layering across at least three page families.
|
||||
- Verify mobile navigation remains usable.
|
||||
|
||||
### Story 3 (P2): `shadcn/ui` usage contract in code
|
||||
|
||||
- Encode `shadcn/ui` usage constraints through local primitives and variant APIs so future component work is forced back through website-owned tokens, surfaces, and interaction semantics.
|
||||
- Keep Astro-native wrappers as the primary authoring path; any borrowed `shadcn/ui` pattern must be translated into local primitives instead of exposing uncontrolled library defaults.
|
||||
- Avoid introducing React, Radix, or extra framework/runtime weight unless a later explicit spec proves a concrete need.
|
||||
- Tests / validation:
|
||||
- Re-run build and smoke coverage after primitive API changes.
|
||||
- Review representative components to ensure no page-local style override bypasses the shared token and primitive model.
|
||||
76
specs/214-website-visual-foundation/quickstart.md
Normal file
76
specs/214-website-visual-foundation/quickstart.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Quickstart: Website Visual Foundation
|
||||
|
||||
## Purpose
|
||||
|
||||
This quickstart describes the local workflow for implementing the website-only visual foundation 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 token and foundation layer in `apps/website/src/styles/tokens.css` and `apps/website/src/styles/global.css`.
|
||||
2. Formalize semantic token roles for color, typography, spacing, surfaces, radius, borders, and shadows.
|
||||
3. Update the current Astro primitives so shared rules are enforced through component APIs rather than page-local class decisions.
|
||||
4. Apply the foundation across representative page families: landing/product, trust/legal, and content-heavy routes.
|
||||
5. Encode `shadcn/ui` usage constraints through local primitives and implementation review rules, without introducing a new required React runtime.
|
||||
6. 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 changes local to `apps/website` unless a deliberate new contract is explicitly introduced
|
||||
- do not introduce platform runtime coupling, shared auth, or shared DTO/API assumptions
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## What Reviewers Should Verify
|
||||
|
||||
Reviewers should confirm that:
|
||||
|
||||
- representative landing, trust/legal, and content-heavy routes still load correctly
|
||||
- heading hierarchy, section rhythm, and CTA weight are visibly consistent across those page families
|
||||
- navigation and footer links remain reachable
|
||||
- focus states and mobile readability remain intact
|
||||
- no uncontrolled local override bypasses the shared token and primitive model
|
||||
- no React, Radix, or platform coupling is introduced without a separate explicit decision
|
||||
|
||||
## Out of Scope for This Feature
|
||||
|
||||
- Filament theming or `apps/platform` styling
|
||||
- shared website-platform design system work
|
||||
- API-backed contact handling or a website backend
|
||||
- CMS introduction
|
||||
- visual regression infrastructure beyond the current lightweight smoke path
|
||||
58
specs/214-website-visual-foundation/research.md
Normal file
58
specs/214-website-visual-foundation/research.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Research: Website Visual Foundation
|
||||
|
||||
## Decision 1: Preserve the website working contract and keep the feature local to `apps/website`
|
||||
|
||||
- **Decision**: Treat the visual foundation as a Class A/B website-local change under the website working contract and keep all runtime behavior inside `apps/website`.
|
||||
- **Rationale**: The current repo truth explicitly allows website-internal evolution while forbidding silent new coupling to `apps/platform`, shared APIs, shared auth, shared DTOs, or shared packages. This spec is about visual language, so the narrowest correct plan is to preserve that separation completely.
|
||||
- **Alternatives considered**:
|
||||
- Introduce a shared website-platform design system: rejected because the spec explicitly excludes cross-surface contracts and the repo does not currently verify such a coupling.
|
||||
- Solve visual consistency inside platform and website together: rejected because it would violate the website-only boundary and import unnecessary coordination cost.
|
||||
|
||||
## Decision 2: Build the foundation on semantic token layering, not raw palette reuse
|
||||
|
||||
- **Decision**: Keep the current Tailwind CSS v4 CSS-first setup and formalize a two-layer token model: primitive palette and font tokens in `src/styles/tokens.css`, semantic role mapping and surface behavior in the website’s shared foundation styles.
|
||||
- **Rationale**: The repo already uses Tailwind v4 and CSS variables, but the current token layer is only partially semantic. A formal role model is the smallest change that can stop visual drift while preserving the current stack and keeping styling readable in Astro templates.
|
||||
- **Alternatives considered**:
|
||||
- Continue with page-local utilities and one-off CSS variables: rejected because it would preserve the drift problem the spec is meant to eliminate.
|
||||
- Replace the current CSS-first setup with a JavaScript token system or external design-token pipeline: rejected because the website does not need that extra build or ownership complexity.
|
||||
|
||||
## Decision 3: Keep Astro-native primitives canonical and adapt `shadcn/ui` concepts locally
|
||||
|
||||
- **Decision**: Keep the existing Astro primitive layer as the canonical implementation surface. `shadcn/ui` will be treated as a design/build reference whose patterns are adapted into local primitives instead of installed as React plus official `shadcn/ui`.
|
||||
- **Rationale**: `apps/website` currently has no React, Radix, or `shadcn/ui` dependency. The public site is static-first and already composed from Astro primitives. Adapting `shadcn/ui` ideas into that layer satisfies the spec’s requirement for controlled usage without adding a second component runtime or framework abstraction.
|
||||
- **Alternatives considered**:
|
||||
- Add React and official `shadcn/ui` now: rejected because it adds unnecessary runtime and dependency cost for a content-driven static site.
|
||||
- Ignore `shadcn/ui` entirely: rejected because the spec explicitly requires a usage contract; the right answer is controlled adaptation, not omission.
|
||||
|
||||
## Decision 4: Use representative page families already in the repo to drive the foundation
|
||||
|
||||
- **Decision**: Drive the implementation and validation of the visual language through three website page families already present in `apps/website`: landing/product-marketing surfaces, trust/legal surfaces, and content-heavy long-form surfaces.
|
||||
- **Rationale**: The visual foundation only proves itself if it survives more than one hero screen. The existing route set already provides the right coverage: Home or Product for landing behavior, Security & Trust or Privacy/Terms for trust/legal behavior, and Legal/Privacy/Terms for long-form text density and callout rhythm.
|
||||
- **Alternatives considered**:
|
||||
- Optimize only the Home page: rejected because the spec explicitly requires consistency across more than one page type.
|
||||
- Wait for blog or changelog routes before defining the foundation: rejected because the existing legal and trust pages are sufficient to validate long-form behavior now.
|
||||
|
||||
## Decision 5: Encode interaction and surface semantics in shared primitives, not page-local classes
|
||||
|
||||
- **Decision**: Consolidate button hierarchy, link semantics, input behavior, card/surface treatments, and section rhythm through the current primitives and layout helpers instead of letting each page set those rules locally.
|
||||
- **Rationale**: The repo already contains `Button`, `Card`, `Input`, `Textarea`, `Section`, `SectionHeader`, and `PageHero`. Those are the narrowest existing seams where the design foundation can be enforced consistently without inventing a new framework.
|
||||
- **Alternatives considered**:
|
||||
- Document the rules but leave existing primitives unchanged: rejected because the drift would persist in code even if the rules existed in prose.
|
||||
- Introduce many new micro-primitives for every visual nuance: rejected because the foundation should reduce variety, not create a larger component taxonomy.
|
||||
|
||||
## Decision 6: Validate through build proof plus focused Playwright smoke coverage
|
||||
|
||||
- **Decision**: Keep validation in `fast-feedback` using `corepack pnpm build:website` and the existing Playwright smoke suite in `apps/website/tests/smoke`.
|
||||
- **Rationale**: This feature changes runtime rendering, navigation shell, and CTA hierarchy, so build proof alone is not enough. The current local Playwright setup is already the narrowest realistic browser-level proof for representative pages and mobile navigation behavior.
|
||||
- **Alternatives considered**:
|
||||
- Build-only validation: rejected because it would miss browser-visible regressions in hierarchy, navigation, and CTA reachability.
|
||||
- Visual regression or a heavier multi-browser lane: rejected because the current change does not justify that added test-governance cost yet.
|
||||
|
||||
## Baseline Findings
|
||||
|
||||
- `apps/website` already runs as an Astro 6 static app with strict TypeScript, Tailwind CSS v4, and local Playwright smoke tests.
|
||||
- The site already has reusable Astro primitives (`Button`, `Card`, `Section`, `SectionHeader`, `Input`, `Textarea`, `Container`, `Grid`, `Stack`) and section components (`PageHero`, `FeatureGrid`, `TrustGrid`, `CTASection`, `LogoStrip`).
|
||||
- `src/styles/tokens.css` currently defines palette and font tokens, while `src/styles/global.css` still carries important semantic choices such as focus behavior, panel backgrounds, shadows, and decorative shell treatments.
|
||||
- No `shadcn/ui`, Radix, or React dependency is present in `apps/website` today.
|
||||
- Existing published routes already cover the page-family range needed for foundation validation: landing/product, trust/legal, and content-heavy reading surfaces.
|
||||
- Root workspace contracts already expose `corepack pnpm dev:website` and `corepack pnpm build:website`, and those must remain intact.
|
||||
176
specs/214-website-visual-foundation/spec.md
Normal file
176
specs/214-website-visual-foundation/spec.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Feature Specification: Website Visual Foundation
|
||||
|
||||
**Feature Branch**: `214-website-visual-foundation`
|
||||
**Created**: 2026-04-18
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Create a website-only design foundation and visual language for apps/website, covering visual direction, design tokens, typography, spacing, surface rules, shadcn/ui usage, semantic website components, and explicit boundaries that exclude apps/platform."
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: `apps/website` risks drifting into page-by-page styling decisions that weaken enterprise trust, reduce consistency, and make later page work slower and harder to review.
|
||||
- **Today's failure**: New website sections currently invite ad hoc choices for typography, spacing, surfaces, CTA weight, and default shadcn/ui styling, which can produce a polished page in isolation but an inconsistent website overall.
|
||||
- **User-visible improvement**: Website pages present a calmer, more consistent, and more trustworthy enterprise SaaS visual language across landing, trust, legal, and content-heavy surfaces.
|
||||
- **Smallest enterprise-capable version**: Define a website-local visual foundation that covers tone, token roles, typography, spacing, surface behavior, interaction semantics, semantic page primitives, and shadcn/ui usage constraints.
|
||||
- **Explicit non-goals**: Filament theming, any visual contract for `apps/platform`, a shared cross-surface design system, detailed page IA, final website copy, full implementation of every component, and logo or brand redesign.
|
||||
- **Permanent complexity imported**: A website-local token vocabulary, typography hierarchy, spacing model, surface taxonomy, interaction semantics, component primitive set, and review rules for shadcn/ui usage.
|
||||
- **Why now**: The website is early enough that a foundation can still prevent drift before more pages and components normalize inconsistent patterns.
|
||||
- **Why not local**: Local page fixes or isolated component tweaks cannot solve cross-page consistency, cannot stop template-driven styling drift, and do not give reviewers a stable baseline for future work.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: New semantic UI layer; risk of overspecifying aesthetics; risk of accidental bleed into platform visual decisions.
|
||||
- **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**: All public-facing pages in `apps/website`, with immediate relevance to landing, trust, legal/privacy, content-heavy, and CTA/contact surfaces.
|
||||
- **Data Ownership**: Website-owned visual rules, semantic primitives, and review criteria only; no tenant-owned records, platform data, or shared persistence.
|
||||
- **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 builders and reviewers do not have a shared enterprise-grade baseline for visual decisions, so every new page risks re-litigating foundational styling choices.
|
||||
- **Existing structure is insufficient because**: Default component libraries and page-local styling do not encode the intended product tone, do not constrain cross-page consistency, and do not protect `apps/platform` from accidental visual coupling.
|
||||
- **Narrowest correct implementation**: A website-only foundation spec that defines rules and semantics without creating a shared design system, platform obligations, or a full component implementation backlog.
|
||||
- **Ownership cost**: Future website work must stay aligned with the foundation, and reviewers must enforce the token and primitive model instead of allowing page-local exceptions to accumulate.
|
||||
- **Alternative intentionally rejected**: Styling pages one by one or accepting default shadcn/ui aesthetics was rejected because both approaches optimize local speed at the cost of long-term coherence and trust.
|
||||
- **Release truth**: Current-release truth for `apps/website`; this is not future-platform preparation and must not be used to imply a cross-surface contract.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Runtime website rendering, styling, and composition change inside `apps/website` with browser smoke coverage and static build proof.
|
||||
- **Validation lane(s)**: fast-feedback
|
||||
- **Why this classification and these lanes are sufficient**: This feature changes executable website presentation, shared primitives, and route composition, but it remains a static Astro surface with no backend writes, no auth, no database setup, and no platform runtime coupling. Root build proof plus focused Playwright smoke coverage is the narrowest honest proof.
|
||||
- **New or expanded test families**: Focused Playwright browser smoke coverage in `apps/website/tests/smoke` for representative landing, trust/legal, content-heavy, and component-guardrail flows.
|
||||
- **Fixture / helper cost impact**: low; smoke helpers may expand slightly, but no database, auth, tenant, or provider setup is required.
|
||||
- **Heavy-family visibility / justification**: none; validation remains in fast-feedback only.
|
||||
- **Reviewer handoff**: Reviewers should confirm that representative public routes still load cleanly, shell/navigation/footer behavior remains stable, CTA hierarchy and progressive disclosure stay readable, accessibility-baseline rules remain visible, and no unnecessary client-framework or platform coupling is introduced.
|
||||
- **Budget / baseline / trend impact**: none beyond the small existing website smoke-suite cost.
|
||||
- **Escalation needed**: document-in-feature
|
||||
- **Planned validation commands**: `corepack pnpm build:website` and `cd apps/website && corepack pnpm exec playwright test`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Build new website pages from a stable foundation (Priority: P1)
|
||||
|
||||
A website contributor can create a new public page without inventing baseline rules for hierarchy, spacing, surfaces, and CTA emphasis.
|
||||
|
||||
**Why this priority**: Preventing visual drift at the point of creation is the core value of the feature and the fastest way to improve long-term quality.
|
||||
|
||||
**Independent Test**: Review a proposed new page and confirm that its headings, section rhythm, surfaces, CTAs, and content groupings can all be mapped to named foundation rules without adding bespoke foundational decisions.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a contributor starts a new page in `apps/website`, **When** they choose typography, spacing, surface treatment, and CTA hierarchy, **Then** each choice can be traced to a defined token role or semantic primitive.
|
||||
2. **Given** a contributor proposes a new section or card treatment, **When** an existing foundation primitive already covers the need, **Then** no new foundational visual rule is required.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Review multiple page types against one visual language (Priority: P1)
|
||||
|
||||
A product or design reviewer can judge consistency across multiple website page types using one shared website-only standard.
|
||||
|
||||
**Why this priority**: The foundation only creates leverage if it works across more than one hero page and remains stable on trust, legal, and content-heavy surfaces.
|
||||
|
||||
**Independent Test**: Compare representative landing, trust/legal, and content-heavy page concepts and verify that they share the same heading logic, spacing rhythm, surface model, and CTA weighting rules.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a landing page and a trust-oriented page, **When** they are reviewed together, **Then** both can be evaluated against the same typography, surface, and CTA rules without page-specific exceptions.
|
||||
2. **Given** a content-heavy page such as privacy, terms, or integrations, **When** it is reviewed against the foundation, **Then** the page still reads as part of the same website rather than as a separate visual mode.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Use shadcn/ui without inheriting a template look (Priority: P2)
|
||||
|
||||
A website component builder can use shadcn/ui as a building block while keeping the website's own visual language and semantic constraints in control.
|
||||
|
||||
**Why this priority**: shadcn/ui is useful for speed, but uncontrolled defaults would quickly reintroduce inconsistency and generic template aesthetics.
|
||||
|
||||
**Independent Test**: Evaluate a proposed shadcn/ui-based component and confirm that its tokens, emphasis, borders, radius, and interaction states are governed by the website foundation rather than by library defaults.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a builder selects a shadcn/ui primitive for website use, **When** they adapt it, **Then** the component must inherit website-defined tokens and semantics instead of preserving its default look unmodified.
|
||||
2. **Given** a local override bypasses shared token or primitive rules, **When** the component is reviewed, **Then** the override is rejected as non-compliant with the foundation.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A page is text-heavy rather than hero-led and still needs the same visual language to feel deliberate and trustworthy.
|
||||
- A page contains multiple potential CTAs and must still maintain one clear action hierarchy rather than several equally loud buttons.
|
||||
- Status or accent colors are available but must not become decorative shortcuts that dilute semantic clarity.
|
||||
- Cards and elevated surfaces must stay useful on long-form content pages instead of turning every block into a visually noisy container.
|
||||
- Future website work must not interpret this foundation as approval to theme Filament or create a shared design contract with `apps/platform`.
|
||||
- Early website forms, if introduced, must use the same input height, border, focus, and error-state logic instead of one-off marketing styling.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no queueing, no long-running operations, no authorization changes, and no Filament surface changes. 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 semantic visual layer because page-local styling and default library decisions are insufficient to create a coherent enterprise website. The layer is scoped narrowly to public website semantics, replaces ad hoc page-by-page foundation decisions, and must not expand into a platform-wide design system 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 a visual direction for `apps/website` that is explicitly local to the website and explicitly excludes `apps/platform`, Filament theming, and any shared cross-surface design contract.
|
||||
- **FR-002**: The visual direction MUST describe the website as clear, calm, precise, trustworthy, modern, and high-quality, and MUST explicitly reject playful, neon-heavy, glass-heavy, over-animated, loud, or generic startup-template aesthetics.
|
||||
- **FR-003**: The foundation MUST define a color-role model that includes, at minimum, `background`, `foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `border`, `input`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `success`, `warning`, `destructive`, and `info`.
|
||||
- **FR-004**: The color-role rules MUST state that neutrals carry most of the interface, that primary color is used sparingly, that status colors remain semantic rather than decorative, and that contrast supports readability on both short hero screens and longer content pages.
|
||||
- **FR-005**: The foundation MUST define a typography hierarchy covering display or hero heading, page heading, section heading, card heading, body text, small text, eyebrow or label text, and UI label or helper text.
|
||||
- **FR-006**: Typography rules MUST require calm, precise headings, readable body copy for long-form pages, and sufficient contrast for small UI text across landing, trust, and content-oriented page types.
|
||||
- **FR-007**: The foundation MUST define a repeatable spacing model for page spacing, section spacing, and component spacing, including rules for section rhythm, content density shifts, and avoidance of page-by-page optical tweaking without a system reference.
|
||||
- **FR-008**: The foundation MUST define a surface language covering page background, default content surface, card surface, elevated surface, muted or inset surface, and highlighted or emphasis surface, with rules that make surface levels structurally useful rather than decorative.
|
||||
- **FR-009**: The foundation MUST define a small radius scale and clear border and shadow rules, including border-first clarity for cards and inputs, restrained shadow usage, and a rejection of decorative float effects.
|
||||
- **FR-010**: The foundation MUST define semantic interaction rules for buttons, links, and inputs, including button hierarchy, CTA weighting, text-vs-button link behavior, shared focus logic, shared border logic, and clear error states for any website form surfaces.
|
||||
- **FR-011**: The foundation MUST define a website semantic primitive model covering navigation primitives, hero primitives, section primitives, content-grouping primitives, CTA primitives, and trust primitives.
|
||||
- **FR-012**: The foundation MUST define usage rules for shadcn/ui that allow it as an implementation building block, require adaptation to website tokens and semantics, and forbid uncontrolled use of default component styling or local overrides disconnected from shared tokens.
|
||||
- **FR-013**: The foundation MUST define page-level consistency rules for content width, heading hierarchy, section rhythm, CTA placement logic, card behavior, callout behavior, footer structure, and link-emphasis rules across `apps/website`.
|
||||
- **FR-014**: The foundation MUST include a tone-to-UI alignment statement that anchors the website in an enterprise and MSP-credible posture: precise instead of hype-driven, believable instead of flashy, structured instead of loud, and modern but controlled.
|
||||
- **FR-015**: The foundation MUST include an accessibility baseline covering readable text sizing, sufficient contrast, distinct focus states, non-color-only semantics, durable mobile readability, and clear differentiation between navigation and CTA elements.
|
||||
- **FR-016**: The foundation MUST define progressive-disclosure behavior so that information density, CTA emphasis, and surface emphasis reveal complexity in layers instead of presenting all signals at once.
|
||||
- **FR-017**: The specification MUST state its required deliverables: design token set, typography hierarchy, color-role mapping, spacing principles, surface/border/radius/shadow rules, button/link/input semantics, section/card/callout primitives, shadcn/ui usage constraints, and page consistency rules.
|
||||
- **FR-018**: The foundation MUST be reviewable against at least three page families: landing pages, trust or legal pages, and content-heavy pages.
|
||||
- **FR-019**: No requirement in this specification MAY be interpreted as mandatory visual alignment work for `apps/platform`, Filament, or a shared website-platform design system.
|
||||
- **FR-020**: 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
|
||||
|
||||
- Filament theming
|
||||
- `apps/platform`
|
||||
- Cross-surface visual contracts
|
||||
- A shared design system for website and platform
|
||||
- Final page IA or page-by-page content architecture beyond route-level composition changes strictly required to apply and prove the foundation
|
||||
- Final website copy or content inventory beyond semantic, hierarchy, disclosure, or CTA-weighting adjustments strictly required to apply and prove the foundation
|
||||
- Full implementation of every component described by the foundation
|
||||
- Logo or brand redesign
|
||||
|
||||
#### Assumptions
|
||||
|
||||
- `apps/website` continues to serve public marketing, trust, legal, and content-oriented page types rather than operator-facing product UI.
|
||||
- The first implementation phase should prefer a small number of repeatable primitives over a wide component catalog.
|
||||
- Enterprise trust and clarity matter more than maximal visual novelty for this website surface.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Visual Direction**: The high-level statement of intended tone, trust posture, and disallowed aesthetic patterns for the website.
|
||||
- **Design Token Set**: The named color, spacing, radius, border, and shadow roles that anchor repeatable visual decisions.
|
||||
- **Typography Hierarchy**: The ordered set of text roles that governs hero, page, section, card, body, and supporting text usage.
|
||||
- **Surface Model**: The set of page and component surface levels used to organize content and emphasis without decorative overload.
|
||||
- **Semantic Primitive Set**: The named website building blocks for navigation, hero, sections, groupings, CTAs, and trust-oriented content.
|
||||
- **shadcn/ui Usage Contract**: The rules that determine when and how shadcn/ui primitives can be adapted for website use.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In review of representative landing, trust/legal, and content-heavy page exemplars, 100% of headings, CTAs, cards, callouts, and input states can be mapped to named foundation rules without inventing new foundational styling rules.
|
||||
- **SC-002**: Reviewers can assess the same three representative page families without raising unresolved foundational questions about typography hierarchy, spacing rhythm, surface usage, or CTA weighting.
|
||||
- **SC-003**: 100% of library-derived website components proposed for the initial implementation phase can be justified as adaptations of the defined token and primitive model rather than uncontrolled default styles.
|
||||
- **SC-004**: The specification contains zero requirements that obligate visual changes to `apps/platform`, Filament, or a shared cross-surface design system.
|
||||
- **SC-005**: At least one primary CTA hierarchy and one low-emphasis CTA pattern are defined clearly enough that reviewers can classify CTA weight consistently across representative website pages.
|
||||
197
specs/214-website-visual-foundation/tasks.md
Normal file
197
specs/214-website-visual-foundation/tasks.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Tasks: Website Visual Foundation
|
||||
|
||||
**Input**: Design documents from `/specs/214-website-visual-foundation/`
|
||||
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/website-visual-foundation.contract.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 the browser coverage 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 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 shared types, metadata hooks, and smoke-test helpers that the visual-foundation refactor will rely on.
|
||||
|
||||
- [X] T001 Add page-family and visual-foundation contract types in `apps/website/src/types/site.ts`
|
||||
- [X] T002 [P] Add foundation-aware site metadata and CTA/footer configuration hooks in `apps/website/src/lib/site.ts` and `apps/website/src/lib/seo.ts`
|
||||
- [X] T003 [P] Extend shell, CTA-hierarchy, accessibility-baseline, navigation-vs-CTA differentiation, and mobile-navigation smoke helpers in `apps/website/tests/smoke/smoke-helpers.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Establish the semantic token layer, shared shell, and reusable layout scaffolds that every user story depends on.
|
||||
|
||||
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||
|
||||
- [X] T004 Implement semantic color, typography, spacing, surface, border, radius, shadow, and accessibility-baseline roles in `apps/website/src/styles/tokens.css` and `apps/website/src/styles/global.css`
|
||||
- [X] T005 [P] Refactor the shared document and navigation shell to use foundation surfaces in `apps/website/src/layouts/BaseLayout.astro`, `apps/website/src/components/layout/PageShell.astro`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T006 [P] Refactor shared width and spacing primitives in `apps/website/src/components/primitives/Container.astro`, `apps/website/src/components/primitives/Section.astro`, `apps/website/src/components/primitives/SectionHeader.astro`, `apps/website/src/components/primitives/Stack.astro`, `apps/website/src/components/primitives/Cluster.astro`, and `apps/website/src/components/primitives/Grid.astro`
|
||||
- [X] T007 [P] Align reusable section scaffolds to the foundation contract, including progressive-disclosure and CTA-emphasis layering, in `apps/website/src/components/sections/PageHero.astro`, `apps/website/src/components/sections/CTASection.astro`, `apps/website/src/components/sections/FeatureGrid.astro`, `apps/website/src/components/sections/TrustGrid.astro`, and `apps/website/src/components/sections/LogoStrip.astro`
|
||||
|
||||
**Checkpoint**: Semantic tokens, shared shell, and section scaffolds are in place. User-story work can now proceed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Build New Pages From a Stable Foundation (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Make Home and Product the proof that new website pages can be assembled from the shared foundation instead of page-local design decisions.
|
||||
|
||||
**Independent Test**: Visit Home and Product and verify the smoke suite proves a shared heading hierarchy, consistent section rhythm, clear CTA weighting, focus visibility, readable contrast, non-color-only semantics, and navigation-vs-CTA differentiation without bespoke foundational styling.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T008 [US1] Update landing/product smoke coverage for heading hierarchy, CTA emphasis, focus visibility, readable contrast, non-color-only semantics, and navigation-vs-CTA differentiation in `apps/website/tests/smoke/home-product.spec.ts`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T009 [P] [US1] Align core content primitives to the shared foundation in `apps/website/src/components/content/Eyebrow.astro`, `apps/website/src/components/content/Headline.astro`, `apps/website/src/components/content/Lead.astro`, `apps/website/src/components/content/FeatureItem.astro`, `apps/website/src/components/content/Callout.astro`, and `apps/website/src/components/content/Metric.astro`
|
||||
- [X] T010 [P] [US1] Update landing and product content modules for semantic sections and CTA rhythm in `apps/website/src/content/pages/home.ts` and `apps/website/src/content/pages/product.ts`
|
||||
- [X] T011 [US1] Apply the stable foundation to `apps/website/src/pages/index.astro` and `apps/website/src/pages/product.astro`
|
||||
|
||||
**Checkpoint**: Home and Product work as the MVP proof that new pages can be built from the shared foundation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Review Multiple Page Types Against One Visual Language (Priority: P1)
|
||||
|
||||
**Goal**: Make trust, legal, audience-fit, and long-form routes read as the same website rather than as separate visual modes.
|
||||
|
||||
**Independent Test**: Visit representative landing, trust/legal, and content-heavy routes and verify the smoke suite proves consistent shell behavior, section rhythm, progressive-disclosure layering, and CTA placement across those page families.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T012 [US2] Update landing, trust/legal, and content-heavy smoke coverage for heading hierarchy, section rhythm, surface consistency, shell/navigation stability, progressive-disclosure layering, CTA placement, and mobile-navigation usability in `apps/website/tests/smoke/home-product.spec.ts`, `apps/website/tests/smoke/solutions-trust-integrations.spec.ts`, and `apps/website/tests/smoke/contact-legal.spec.ts`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T013 [P] [US2] Align audience, trust, and long-form content primitives in `apps/website/src/components/content/AudienceRow.astro`, `apps/website/src/components/content/TrustPrincipleCard.astro`, `apps/website/src/components/content/IntegrationBadge.astro`, `apps/website/src/components/content/ContactPanel.astro`, `apps/website/src/components/content/DemoPrompt.astro`, and `apps/website/src/components/content/RichText.astro`
|
||||
- [X] T014 [P] [US2] Normalize trust/legal/content-heavy page content modules for semantic structure, disclosure layers, and CTA rhythm without changing final copy or IA in `apps/website/src/content/pages/solutions.ts`, `apps/website/src/content/pages/security-trust.ts`, `apps/website/src/content/pages/integrations.ts`, `apps/website/src/content/pages/contact.ts`, `apps/website/src/content/pages/legal.ts`, `apps/website/src/content/pages/privacy.ts`, and `apps/website/src/content/pages/terms.ts`
|
||||
- [X] T015 [US2] Apply page-family consistency rules and route-composition changes needed to prove the foundation, without redefining final IA or copy, in `apps/website/src/pages/solutions.astro`, `apps/website/src/pages/security-trust.astro`, `apps/website/src/pages/integrations.astro`, `apps/website/src/pages/contact.astro`, `apps/website/src/pages/legal.astro`, `apps/website/src/pages/privacy.astro`, and `apps/website/src/pages/terms.astro`
|
||||
|
||||
**Checkpoint**: Landing, trust/legal, and long-form routes can be reviewed against the same visual language.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Use shadcn/ui Without Inheriting a Template Look (Priority: P2)
|
||||
|
||||
**Goal**: Encode the website-local component contract so future `shadcn/ui`-inspired work routes through local primitives instead of uncontrolled library defaults.
|
||||
|
||||
**Independent Test**: Run a dedicated smoke check that proves representative pages still expose consistent CTA hierarchy, shared input/button behavior, and mobile navigation while relying on local foundation-backed primitives.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T016 [US3] Write component-guardrail smoke coverage for focus states, input semantics, non-color-only semantics, navigation-vs-CTA differentiation, CTA hierarchy, and consistent rendered component behavior in `apps/website/tests/smoke/visual-foundation-guardrails.spec.ts`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T017 [P] [US3] Encode local foundation-backed component variants in `apps/website/src/components/primitives/Button.astro`, `apps/website/src/components/primitives/Badge.astro`, `apps/website/src/components/primitives/Card.astro`, `apps/website/src/components/primitives/Input.astro`, and `apps/website/src/components/primitives/Textarea.astro`
|
||||
- [X] T018 [P] [US3] Route CTA and low-emphasis action patterns through shared wrappers in `apps/website/src/components/content/PrimaryCTA.astro`, `apps/website/src/components/content/SecondaryCTA.astro`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T019 [US3] Remove remaining page-local template-style escapes from `apps/website/src/components/sections/PageHero.astro`, `apps/website/src/components/sections/CTASection.astro`, `apps/website/src/pages/index.astro`, `apps/website/src/pages/product.astro`, `apps/website/src/pages/security-trust.astro`, and `apps/website/src/pages/contact.astro`
|
||||
|
||||
**Checkpoint**: `shadcn/ui`-style patterns are now constrained by the local website foundation instead of shaping the website by default.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Run the narrowest validation proof and confirm website-working-contract compatibility after all stories land.
|
||||
|
||||
- [X] T020 [P] Record fast-feedback lane validation, including contrast, focus visibility, non-color-only semantics, navigation-vs-CTA differentiation, and progressive-disclosure proof, and run the required proof commands from `specs/214-website-visual-foundation/plan.md` and `specs/214-website-visual-foundation/quickstart.md`
|
||||
- [X] T021 Verify website working-contract compatibility and static-first runtime constraints, including no new React/runtime coupling or unnecessary client hydration, in `package.json`, `apps/website/package.json`, `apps/website/astro.config.mjs`, and `apps/website/playwright.config.ts`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: Starts immediately.
|
||||
- **Foundational (Phase 2)**: Depends on Setup and blocks all user stories.
|
||||
- **User Story 1 (Phase 3)**: Depends on Foundational only.
|
||||
- **User Story 2 (Phase 4)**: Depends on Foundational only.
|
||||
- **User Story 3 (Phase 5)**: Depends on Foundational only and can proceed in parallel with US1 or US2 once the shared shell and primitives are stable.
|
||||
- **Polish (Phase 6)**: Depends on all targeted user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1**: No dependency on other user stories; this is the MVP slice.
|
||||
- **US2**: No dependency on US1, but it reuses the same shared shell, token, and primitive foundation.
|
||||
- **US3**: No dependency on US1 or US2 for implementation, but its guardrail checks should run against representative routes after the shared foundation is stable.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Write the browser smoke test first and verify it fails.
|
||||
- Align shared content or primitive components before changing page content modules.
|
||||
- Update page content modules before completing route composition.
|
||||
- Finish route-level integration before moving to the next story’s polish or validation work.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
- `T002` and `T003` can run in parallel after `T001` is scoped.
|
||||
- `T005`, `T006`, and `T007` can run in parallel after `T004` starts.
|
||||
- In US1, `T009` and `T010` can run in parallel before `T011`.
|
||||
- In US2, `T013` and `T014` can run in parallel before `T015`.
|
||||
- In US3, `T017` and `T018` can run in parallel before `T019`.
|
||||
- `T020` and `T021` can run in parallel during final polish.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 2
|
||||
|
||||
```bash
|
||||
# Launch trust/legal/content-heavy preparation together:
|
||||
Task: "T013 [US2] Align audience, trust, and long-form content primitives"
|
||||
Task: "T014 [US2] Update trust/legal/content-heavy page content modules"
|
||||
|
||||
# Then complete route integration:
|
||||
Task: "T015 [US2] Apply page-family consistency rules to the affected routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 Home and Product.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Setup + Foundational establish the reusable website-local foundation.
|
||||
2. US1 proves new page construction from the shared rules.
|
||||
3. US2 aligns representative page families under one visual language.
|
||||
4. US3 hardens the local primitive contract so future `shadcn/ui`-inspired work cannot drift back to template defaults.
|
||||
5. Polish runs the narrowest proof commands and verifies website-working-contract compatibility.
|
||||
|
||||
### Suggested MVP Scope
|
||||
|
||||
- Deliver through **User Story 1** if the smallest initial release is needed.
|
||||
- Add **User Story 2** next for page-family consistency across trust/legal/content-heavy surfaces.
|
||||
- Finish with **User Story 3** to lock future component work behind the local foundation contract.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
- `2026-04-19`: `corepack pnpm build:website` passed from repo root.
|
||||
- `2026-04-19`: `corepack pnpm --filter @tenantatlas/website exec playwright test` passed with 10 smoke tests.
|
||||
- Focus visibility proof is covered by the updated landing-page smoke flow (`Skip to content` keyboard focus plus CTA hierarchy checks).
|
||||
- Non-color-only semantics, navigation-vs-CTA differentiation, progressive-disclosure layers, badge/button/input hooks, and content-heavy route consistency are exercised through the updated smoke helpers and representative route specs.
|
||||
- Website working-contract review confirmed `@tenantatlas/website`, `WEBSITE_PORT`, Astro static output, and Playwright web-server flow remain intact with no React, Radix, or platform runtime coupling introduced.
|
||||
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