feat: initial website foundation and v0 product site #249

Merged
ahmido merged 2 commits from 213-website-foundation-v0 into dev 2026-04-18 20:56:48 +00:00
81 changed files with 4046 additions and 249 deletions
Showing only changes of commit 020d416d0d - Show all commits

View File

@ -3,6 +3,9 @@ apps/platform/node_modules/
apps/website/node_modules/ apps/website/node_modules/
apps/website/.astro/ apps/website/.astro/
apps/website/dist/ apps/website/dist/
apps/website/playwright-report/
apps/website/test-results/
apps/website/blob-report/
dist/ dist/
build/ build/
vendor/ vendor/

View File

@ -205,6 +205,8 @@ ## Active Technologies
- Repository-owned markdown and contract artifacts under `.specify/`, `specs/212-test-authoring-guardrails/`, and root documentation files; no product database persistence (212-test-authoring-guardrails) - Repository-owned markdown and contract artifacts under `.specify/`, `specs/212-test-authoring-guardrails/`, and root documentation files; no product database persistence (212-test-authoring-guardrails)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext` (199-global-context-shell-contract) - PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext` (199-global-context-shell-contract)
- PostgreSQL unchanged plus existing Laravel session keys `current_workspace_id`, `workspace_intended_url`, and `workspace_last_tenant_ids`; no schema change planned (199-global-context-shell-contract) - PostgreSQL unchanged plus existing Laravel session keys `current_workspace_id`, `workspace_intended_url`, and `workspace_last_tenant_ids`; no schema change planned (199-global-context-shell-contract)
- 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)
- PHP 8.4.15 (feat/005-bulk-operations) - PHP 8.4.15 (feat/005-bulk-operations)
@ -239,8 +241,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 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
- 199-global-context-shell-contract: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext` - 199-global-context-shell-contract: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext`
- 212-test-authoring-guardrails: Added Markdown for repository governance artifacts, JSON Schema plus logical OpenAPI for planning contracts, and Bash-backed SpecKit scripts already present in the repo + `.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`, `README.md`, and the existing Specs 206 through 211 governance vocabulary - 212-test-authoring-guardrails: Added Markdown for repository governance artifacts, JSON Schema plus logical OpenAPI for planning contracts, and Bash-backed SpecKit scripts already present in the repo + `.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`, `README.md`, and the existing Specs 206 through 211 governance vocabulary
- 211-runtime-trend-recalibration: Added PHP 8.4.15 for repo-truth governance logic, Bash for repo-root wrappers, GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/`, plus JSON Schema and logical OpenAPI for repository contracts + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, uploaded artifact bundles, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

3
.gitignore vendored
View File

@ -19,6 +19,9 @@
/apps/website/node_modules /apps/website/node_modules
/.pnpm-store /.pnpm-store
/apps/website/.astro /apps/website/.astro
/apps/website/playwright-report
/apps/website/test-results
/apps/website/blob-report
dist/ dist/
build/ build/
coverage/ coverage/

View File

@ -7,6 +7,9 @@ apps/platform/node_modules/
apps/website/node_modules/ apps/website/node_modules/
apps/website/.astro/ apps/website/.astro/
apps/website/dist/ apps/website/dist/
apps/website/playwright-report/
apps/website/test-results/
apps/website/blob-report/
vendor/ vendor/
apps/platform/vendor/ apps/platform/vendor/
*.log *.log

View File

@ -1,9 +1,23 @@
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
const publicSiteUrl = process.env.PUBLIC_SITE_URL ?? 'https://tenantatlas.example';
export default defineConfig({ export default defineConfig({
output: 'static', output: 'static',
site: publicSiteUrl,
server: { server: {
host: true, host: true,
port: 4321, port: 4321,
}, },
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
},
}); });

View File

@ -9,9 +9,18 @@
"scripts": { "scripts": {
"dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}", "dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"build": "astro build", "build": "astro build",
"preview": "astro preview --host 0.0.0.0" "preview": "astro preview --host 0.0.0.0",
"test": "playwright test",
"test:smoke": "playwright test"
}, },
"dependencies": { "dependencies": {
"astro": "^6.0.0" "astro": "^6.0.0"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.7.2",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
} }
} }

View File

@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test';
const port = Number(process.env.WEBSITE_PORT ?? '4321');
const baseURL = `http://127.0.0.1:${port}`;
export default defineConfig({
testDir: './tests/smoke',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['list']],
use: {
baseURL,
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
webServer: {
command: `WEBSITE_PORT=${port} corepack pnpm dev`,
port,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

View File

@ -1,2 +1,3 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: /sitemap.xml

View File

@ -0,0 +1,35 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import Card from '@/components/primitives/Card.astro';
import type { AudienceRowContent } from '@/types/site';
interface Props {
item: AudienceRowContent;
}
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)]">
{item.title}
</h3>
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
<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)]">
{bullet}
</li>
))
}
</ul>
{item.cta && (
<div class="mt-6">
<SecondaryCTA cta={item.cta} />
</div>
)}
</Card>

View File

@ -0,0 +1,23 @@
---
import Card from '@/components/primitives/Card.astro';
import type { CalloutContent } from '@/types/site';
interface Props {
content: CalloutContent;
}
const { content } = Astro.props;
const variant = content.tone === 'accent' ? 'accent' : content.tone === 'subtle' ? 'subtle' : 'default';
---
<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.title}
</h3>
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{content.description}</p>
</Card>

View File

@ -0,0 +1,29 @@
---
import Button from '@/components/primitives/Button.astro';
import Card from '@/components/primitives/Card.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
points: string[];
title: string;
}
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>
<ul class="mt-5 space-y-3 p-0">
{
points.map((point) => (
<li class="list-none rounded-[1rem] bg-white/72 px-4 py-3 text-sm text-[var(--color-ink-800)]">
{point}
</li>
))
}
</ul>
<div class="mt-6">
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>
</div>
</Card>

View File

@ -0,0 +1,18 @@
---
import Card from '@/components/primitives/Card.astro';
interface Props {
description: string;
title: string;
}
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>
</Card>

View File

@ -0,0 +1,11 @@
---
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
---
<p class:list={['m-0 text-sm font-semibold uppercase tracking-[0.18em] text-[var(--color-brand)]', className]}>
<slot />
</p>

View File

@ -0,0 +1,32 @@
---
import Card from '@/components/primitives/Card.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
item: FeatureItemContent;
}
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.title}
</h3>
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
{(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}>
Learn more
</a>
)}
</div>
)}
</Card>

View File

@ -0,0 +1,11 @@
---
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
---
<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]}>
<slot />
</h2>

View File

@ -0,0 +1,19 @@
---
import Badge from '@/components/primitives/Badge.astro';
import type { IntegrationEntry } from '@/types/site';
interface Props {
item: IntegrationEntry;
}
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)]">
<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>
</div>
<p class="mt-3 max-w-72 text-sm leading-6 text-[var(--color-copy)]">{item.summary}</p>
{item.note && <p class="mt-2 text-xs font-medium uppercase tracking-[0.14em] text-[var(--color-brand)]">{item.note}</p>}
</div>

View File

@ -0,0 +1,11 @@
---
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
---
<p class:list={['m-0 text-base leading-8 text-[var(--color-copy)] sm:text-lg', className]}>
<slot />
</p>

View File

@ -0,0 +1,18 @@
---
import Card from '@/components/primitives/Card.astro';
import type { MetricItem } from '@/types/site';
interface Props {
item: MetricItem;
}
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)]">
{item.label}
</p>
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">{item.description}</p>
</Card>

View File

@ -0,0 +1,12 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
}
const { cta } = Astro.props;
---
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>

View File

@ -0,0 +1,29 @@
---
import type { LegalSection } from '@/types/site';
interface Props {
sections: LegalSection[];
}
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)]">
{section.title}
</h2>
<div class="legal-prose mt-4">
{section.body.map((paragraph) => <p>{paragraph}</p>)}
{section.bullets && section.bullets.length > 0 && (
<ul>
{section.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
</div>
</section>
))
}
</div>

View File

@ -0,0 +1,12 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
}
const { cta } = Astro.props;
---
<Button href={cta.href} variant={cta.variant ?? 'secondary'}>{cta.label}</Button>

View File

@ -0,0 +1,16 @@
---
import Card from '@/components/primitives/Card.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
item: TrustPrincipleContent;
}
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>}
</Card>

View File

@ -0,0 +1,59 @@
---
import Button from '@/components/primitives/Button.astro';
import Container from '@/components/primitives/Container.astro';
import { contactCta, footerNavigationGroups, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath: _currentPath } = Astro.props;
const currentYear = new Date().getFullYear();
---
<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">
<div class="space-y-5">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
{siteMetadata.siteName}
</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.
</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.
</p>
<Button href={contactCta.href} variant="primary" size="sm">{contactCta.label}</Button>
</div>
<div class="grid gap-6 sm:grid-cols-3">
{
footerNavigationGroups.map((group) => (
<div>
<p class="m-0 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-ink-900)]">
{group.title}
</p>
<ul class="mt-4 space-y-3 p-0 text-sm text-[var(--color-copy)]">
{group.items.map((item) => (
<li class="list-none">
<a class="transition hover:text-[var(--color-brand)]" href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
</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">
Built as a static Astro track with no platform auth, session, or API coupling.
</p>
</div>
</Container>
</footer>

View File

@ -0,0 +1,105 @@
---
import Button from '@/components/primitives/Button.astro';
import Container from '@/components/primitives/Container.astro';
import { contactCta, isActiveNavigationPath, primaryNavigation, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
---
<header class="sticky top-0 z-30 pt-4 sm:pt-6">
<Container 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"
>
<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"
>
TA
</span>
<span class="min-w-0">
<span class="block truncate text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-ink-900)]">
{siteMetadata.siteName}
</span>
<span class="block truncate text-sm text-[var(--color-copy)]">
{siteMetadata.siteTagline}
</span>
</span>
</a>
<nav class="hidden items-center gap-1 lg:flex" aria-label="Primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
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)]'
: 'text-[var(--color-ink-800)] hover:bg-white/70',
]}
>
{item.label}
</a>
))
}
</nav>
<div class="hidden lg:block">
<Button href={contactCta.href} variant="secondary" size="sm">{contactCta.label}</Button>
</div>
<details class="relative lg:hidden">
<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)]"
>
<span class="sr-only">Open navigation menu</span>
<span class="flex flex-col gap-1">
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
</span>
</summary>
<div
class="glass-panel absolute right-0 top-[calc(100%+0.75rem)] w-[min(18rem,88vw)] rounded-[1.5rem] border border-white/80 p-3"
>
<nav class="flex flex-col gap-1" aria-label="Mobile primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
class:list={[
'rounded-[1rem] px-4 py-3 text-sm',
isActiveNavigationPath(currentPath, item.href)
? 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]'
: 'text-[var(--color-ink-800)] hover:bg-white/75',
]}
>
<span class="block font-semibold">{item.label}</span>
{item.description && (
<span class="mt-1 block text-xs text-[var(--color-copy)]">
{item.description}
</span>
)}
</a>
))
}
<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>
)}
</div>
</nav>
</div>
</details>
</div>
</Container>
</header>

View File

@ -0,0 +1,39 @@
---
import Footer from '@/components/layout/Footer.astro';
import Navbar from '@/components/layout/Navbar.astro';
import { resolveSeo } from '@/lib/seo';
import BaseLayout from '@/layouts/BaseLayout.astro';
interface Props {
currentPath: string;
description?: string;
title?: string;
}
const { currentPath, description, title } = Astro.props;
const seo =
title && description
? resolveSeo({ description, path: currentPath, title })
: undefined;
---
<BaseLayout
title={title}
description={description}
canonicalUrl={seo?.canonicalUrl}
openGraphTitle={seo?.ogTitle}
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%)]"
>
</div>
<Navbar currentPath={currentPath} />
<main id="content" class="pb-16 sm:pb-20">
<slot />
</main>
<Footer currentPath={currentPath} />
</div>
</BaseLayout>

View File

@ -0,0 +1,25 @@
---
interface Props {
class?: string;
tone?: 'accent' | 'neutral' | 'signal' | 'warm';
}
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)]',
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]',
toneClasses[tone],
className,
]}
>
<slot />
</span>

View File

@ -0,0 +1,56 @@
---
import type { ButtonVariant } from '@/types/site';
interface Props {
ariaLabel?: string;
class?: string;
href?: string;
rel?: string;
size?: 'lg' | 'md' | 'sm';
target?: '_blank' | '_self';
type?: 'button' | 'reset' | 'submit';
variant?: ButtonVariant;
}
const {
ariaLabel,
class: className = '',
href,
rel,
size = 'md',
target,
type = 'button',
variant = 'primary',
} = 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';
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]',
};
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)]',
secondary:
'border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)] hover:border-[var(--color-ink-900)] hover:bg-white',
ghost: 'border-transparent bg-transparent text-[var(--color-ink-800)] hover:bg-white/70',
};
const classes = [baseClass, sizeClasses[size], variantClasses[variant], className];
---
{
href ? (
<a href={href} target={target} rel={rel} aria-label={ariaLabel} class:list={classes}>
<slot />
</a>
) : (
<button type={type} aria-label={ariaLabel} class:list={classes}>
<slot />
</button>
)
}

View File

@ -0,0 +1,23 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
variant?: 'accent' | 'default' | 'subtle';
}
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))]',
};
const Tag = as;
---
<Tag class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], className]}>
<slot />
</Tag>

View File

@ -0,0 +1,13 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
}
const { as = 'div', class: className = '' } = Astro.props;
const Tag = as;
---
<Tag class:list={['flex flex-wrap items-center gap-3 sm:gap-4', className]}>
<slot />
</Tag>

View File

@ -0,0 +1,14 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
wide?: boolean;
}
const { as = 'div', class: className = '', wide = false } = Astro.props;
const Tag = as;
---
<Tag class:list={['mx-auto w-full px-5 sm:px-6 lg:px-8', wide ? 'max-w-[80rem]' : 'max-w-6xl', className]}>
<slot />
</Tag>

View File

@ -0,0 +1,18 @@
---
interface Props {
class?: string;
cols?: '2' | '3' | '4';
}
const { class: className = '', cols = '3' } = 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',
};
---
<div class:list={['grid gap-5 lg:gap-6', colClasses[cols], className]}>
<slot />
</div>

View File

@ -0,0 +1,32 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
type?: string;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
type = 'text',
value,
} = Astro.props;
---
<input
type={type}
name={name}
value={value}
placeholder={placeholder}
readonly={readonly}
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',
readonly ? 'cursor-default' : '',
className,
]}
/>

View File

@ -0,0 +1,22 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
id?: string;
muted?: boolean;
}
const { as = 'section', class: className = '', id, muted = false } = Astro.props;
const Tag = as;
---
<Tag
id={id}
class:list={[
'py-12 sm:py-16 lg:py-20',
muted ? 'bg-white/45' : '',
className,
]}
>
<slot />
</Tag>

View File

@ -0,0 +1,27 @@
---
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
interface Props {
align?: 'center' | 'left';
class?: string;
description?: string;
eyebrow?: string;
title: string;
}
const {
align = 'left',
class: className = '',
description,
eyebrow,
title,
} = Astro.props;
---
<div class:list={['max-w-3xl', align === 'center' ? 'mx-auto text-center' : '', className]}>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
<Headline>{title}</Headline>
{description && <Lead class="mt-4">{description}</Lead>}
</div>

View File

@ -0,0 +1,20 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
gap?: 'lg' | 'md' | 'sm' | 'xl';
}
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',
xl: 'flex flex-col gap-10',
};
const Tag = as;
---
<Tag class:list={[gapClasses[gap], className]}>
<slot />
</Tag>

View File

@ -0,0 +1,31 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
rows?: number;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
rows = 5,
value,
} = Astro.props;
---
<textarea
name={name}
rows={rows}
placeholder={placeholder}
readonly={readonly}
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',
readonly ? 'cursor-default' : '',
className,
]}
>{value}</textarea>

View File

@ -0,0 +1,33 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.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 type { CtaLink } from '@/types/site';
interface Props {
description: string;
eyebrow?: string;
primary: CtaLink;
secondary?: CtaLink;
title: string;
}
const { description, eyebrow, primary, secondary, title } = Astro.props;
---
<Section>
<Container wide>
<Card variant="accent" class="overflow-hidden">
<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">
<PrimaryCTA cta={primary} />
{secondary && <SecondaryCTA cta={secondary} />}
</div>
</div>
</Card>
</Container>
</Section>

View File

@ -0,0 +1,28 @@
---
import FeatureItem from '@/components/content/FeatureItem.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 type { FeatureItemContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: FeatureItemContent[];
title: string;
}
const { description, eyebrow, items, title } = Astro.props;
---
<Section>
<Container wide>
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
<Grid cols="3">
{items.map((item) => <FeatureItem item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -0,0 +1,44 @@
---
import IntegrationBadge from '@/components/content/IntegrationBadge.astro';
import Badge from '@/components/primitives/Badge.astro';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import type { IntegrationEntry, LogoStripItem } from '@/types/site';
interface Props {
eyebrow?: string;
items: (IntegrationEntry | LogoStripItem)[];
title: string;
}
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)]">
<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>}
<h2 class="m-0 max-w-2xl font-[var(--font-display)] text-3xl leading-tight text-[var(--color-ink-900)]">
{title}
</h2>
</div>
<div class="flex flex-wrap gap-3">
{
items.map((item) => (
<IntegrationBadge
item={{
category: 'category' in item ? item.category : 'Ecosystem',
name: item.label ?? item.name,
note: item.note,
summary: 'summary' in item ? item.summary : `${item.label} aligns with the launch story.`,
}}
/>
))
}
</div>
</div>
</div>
</Container>
</Section>

View File

@ -0,0 +1,96 @@
---
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 type { HeroContent, MetricItem } from '@/types/site';
interface Props {
calloutDescription?: string;
calloutTitle?: string;
hero: HeroContent;
metrics?: MetricItem[];
}
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]">
<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">
{hero.title}
</h1>
<p class="max-w-3xl text-lg leading-8 text-[var(--color-copy)] sm:text-xl">
{hero.description}
</p>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<Cluster>
<Button href={hero.primaryCta.href} variant={hero.primaryCta.variant ?? 'primary'}>
{hero.primaryCta.label}
</Button>
{hero.secondaryCta && (
<Button href={hero.secondaryCta.href} variant={hero.secondaryCta.variant ?? 'secondary'}>
{hero.secondaryCta.label}
</Button>
)}
</Cluster>
)}
{hero.highlights && hero.highlights.length > 0 && (
<ul class="grid gap-3 p-0 sm:grid-cols-3">
{
hero.highlights.map((highlight) => (
<li class="list-none rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/70 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{highlight}
</li>
))
}
</ul>
)}
</div>
</Card>
<div class="grid gap-5">
{(calloutTitle || calloutDescription) && (
<Card variant="accent" class="motion-rise">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.15em] text-[var(--color-brand)]">
Trust-first launch surface
</p>
{calloutTitle && (
<h2 class="mt-4 font-[var(--font-display)] text-3xl leading-tight text-[var(--color-ink-900)]">
{calloutTitle}
</h2>
)}
{calloutDescription && (
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{calloutDescription}</p>
)}
</Card>
)}
{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>
))
}
</div>
)}
</div>
</div>
</Container>
</section>

View File

@ -0,0 +1,28 @@
---
import TrustPrincipleCard from '@/components/content/TrustPrincipleCard.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 type { TrustPrincipleContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: TrustPrincipleContent[];
title: string;
}
const { description, eyebrow, items, title } = Astro.props;
---
<Section>
<Container wide>
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
<Grid cols="3">
{items.map((item) => <TrustPrincipleCard item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -0,0 +1,24 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const futureContentSchema = z.object({
title: z.string(),
description: z.string(),
publishedAt: z.coerce.date().optional(),
draft: z.boolean().default(false),
});
export const collections = {
articles: defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/articles' }),
schema: futureContentSchema,
}),
changelog: defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/changelog' }),
schema: futureContentSchema,
}),
resources: defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/resources' }),
schema: futureContentSchema,
}),
};

View File

@ -0,0 +1,65 @@
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.',
path: '/contact',
};
export const contactHero: HeroContent = {
eyebrow: 'Contact / Demo',
title: 'Start a qualified working session 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: {
href: 'mailto:hello@tenantatlas.example?subject=TenantAtlas%20working%20session',
label: 'Email the TenantAtlas team',
variant: 'primary',
},
secondaryCta: {
href: '/legal',
label: 'Read the legal surface',
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.',
],
};
export const contactTopics = [
'Evaluation of backup, restore, or version-governance workflows for Intune and Microsoft tenant operations.',
'Questions about MSP fit, customer-facing evidence, or multi-tenant operational discipline.',
'Internal enterprise review of change history, drift visibility, evidence collection, or restore posture.',
];
export const contactPrompts = [
{
title: 'Good first conversation',
description:
'Explain the current operating model, where version history breaks down today, and which changes feel hardest to review or restore safely.',
},
{
title: 'Useful context to share',
description:
'Team shape, tenant count, policy complexity, change frequency, and whether the first concern is restore safety, auditability, or review evidence.',
},
];
export const contactPreview = {
message:
'We operate Microsoft tenant governance across multiple environments and want to understand how TenantAtlas approaches version history, safer restore flows, drift visibility, and review evidence.',
topic: 'Environment and operating model summary',
};
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.',
],
},
];

View File

@ -0,0 +1,145 @@
import type {
CalloutContent,
FeatureItemContent,
HeroContent,
IntegrationEntry,
MetricItem,
PageSeo,
} from '@/types/site';
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.',
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.',
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.',
primaryCta: {
href: '/product',
label: 'See the product model',
},
secondaryCta: {
href: '/security-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.',
],
};
export const homeMetrics: MetricItem[] = [
{
value: '1',
label: 'Connected model',
description: 'Inventory, snapshots, review evidence, and restore posture stay in one narrative.',
},
{
value: '7+',
label: 'Core public surfaces',
description: 'Visitors can move from explanation to trust and contact without dead ends.',
},
{
value: '0',
label: 'Runtime coupling',
description: 'The website stays independent from platform auth, session, and API behavior.',
},
];
export const homePillars: FeatureItemContent[] = [
{
eyebrow: 'Inventory',
title: 'Normalize what the tenant really looks like right now.',
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',
},
{
eyebrow: 'Snapshots',
title: 'Keep immutable history instead of vague memory.',
description:
'Version history stays queryable by tenant, operator, and moment in time so teams can explain what changed and why.',
meta: 'Reproducible versions',
},
{
eyebrow: 'Drift & findings',
title: 'Surface drift, exceptions, and review needs in the same language.',
description:
'Operational questions move from “what broke?” to “what changed, what matters, and what review is due?”',
meta: 'Review-oriented visibility',
},
{
eyebrow: 'Restore',
title: 'Treat rollback and restore as governed actions, not panic buttons.',
description:
'Preview, validation, and operator confirmation stay central so risky changes are reversible without becoming casual.',
meta: 'Safer execution',
},
{
eyebrow: 'Evidence',
title: 'Connect reviews, findings, and evidence without a second reporting layer.',
description:
'Teams can show why a configuration is acceptable, where exceptions exist, and how review decisions stay attributable.',
meta: 'Audit-ready context',
},
{
eyebrow: 'Operations',
title: 'Keep velocity without hiding risk.',
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',
},
];
export const homeProofBlocks: CalloutContent[] = [
{
eyebrow: 'Positioning',
title: 'Governance of record for Microsoft tenant operations.',
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.',
tone: 'accent',
},
{
eyebrow: 'Why it matters now',
title: 'Microsoft tenant change volume keeps climbing while operator certainty keeps shrinking.',
description:
'When policy history, restore posture, findings, and evidence live in separate conversations, teams lose time exactly when they need clarity.',
},
{
eyebrow: 'Public promise',
title: 'No inflated compliance or automation claims.',
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.',
tone: 'subtle',
},
];
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.',
},
{
category: 'Identity',
name: 'Entra ID',
summary: 'Identity and access context remain part of the governance narrative where they matter to change control.',
},
{
category: 'Endpoint',
name: 'Intune',
summary: 'Configuration state, backup, and restore posture 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.',
},
];

View File

@ -0,0 +1,86 @@
import type {
FeatureItemContent,
HeroContent,
IntegrationEntry,
PageSeo,
} from '@/types/site';
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.',
path: '/integrations',
};
export const integrationsHero: HeroContent = {
eyebrow: 'Ecosystem fit',
title: 'Stay clear about the ecosystem fit without turning the page into a wishlist.',
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.',
primaryCta: {
href: '/contact',
label: 'Plan the working session',
},
secondaryCta: {
href: '/product',
label: 'Revisit the product model',
variant: 'secondary',
},
highlights: [
'Microsoft-first and governance-led.',
'Real direction only, no catalog-padding.',
'Explains fit without implying runtime coupling to the public site.',
],
};
export const integrationEntries: IntegrationEntry[] = [
{
category: 'Microsoft core',
name: 'Microsoft Graph',
summary:
'Graph remains the primary contract path for inventory, history, and restore-oriented product behavior in the platform.',
note: 'Core product contract',
},
{
category: 'Identity',
name: 'Entra ID',
summary:
'Identity context matters where tenant change, privileged access, or governance reviews intersect with Microsoft tenant administration.',
note: 'Operational context',
},
{
category: 'Endpoint',
name: 'Intune',
summary:
'Intune configuration state is central to the current product story, especially for version history, backup, restore, and drift visibility.',
note: 'Current release truth',
},
{
category: 'Evidence',
name: 'Review & evidence workflows',
summary:
'Exports, review packs, and evidence-linked conversations should remain grounded in the actual tenant object and its change history.',
note: 'Governance workflow fit',
},
];
export const integrationRules: FeatureItemContent[] = [
{
eyebrow: 'Direction',
title: 'Integrations should reinforce the governance model.',
description:
'The page is not a marketplace list. It should show which systems matter because they change the product workflow, evidence story, or operator context.',
},
{
eyebrow: 'Boundaries',
title: 'Do not imply shared public-site runtime dependencies.',
description:
'The website stays static and independent even while the product story references Graph, Intune, and Microsoft tenant governance flows.',
},
{
eyebrow: 'Credibility',
title: 'Speculative wishlist entries reduce trust instead of creating momentum.',
description:
'The public integrations page should stay shorter and sharper than a catalog full of future ideas that are not relevant to launch truth.',
},
];

View File

@ -0,0 +1,44 @@
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.',
path: '/legal',
};
export const legalHero: HeroContent = {
eyebrow: 'Legal surface',
title: 'Legal access should stay one click away from the 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.',
primaryCta: {
href: '/privacy',
label: 'Privacy',
},
secondaryCta: {
href: '/terms',
label: 'Terms',
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.',
],
};
export const legalNoticeSections: LegalSection[] = [
{
title: 'Public legal notice',
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.',
],
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.',
],
},
];

View File

@ -0,0 +1,60 @@
import type { HeroContent, LegalSection, PageSeo } from '@/types/site';
export const privacySeo: PageSeo = {
title: 'TenantAtlas | Privacy',
description:
'Public-site privacy overview for TenantAtlas inquiries, including how contact details and evaluation context are handled on the public website.',
path: '/privacy',
};
export const privacyHero: HeroContent = {
eyebrow: 'Privacy',
title: 'Public-site privacy overview for TenantAtlas inquiries.',
description:
'This page explains the privacy expectations for the public website and the contact path, rather than promising a full product-tenant data processing agreement from a static marketing surface.',
primaryCta: {
href: '/contact',
label: 'Return to contact',
},
secondaryCta: {
href: '/terms',
label: 'Review website terms',
variant: 'secondary',
},
highlights: [
'The public site should only request information that supports a useful follow-up.',
'Contact details and evaluation context should be handled carefully and minimally.',
'Future product-processing details belong in product/legal agreements, not hidden marketing copy.',
],
};
export const privacySections: LegalSection[] = [
{
title: 'Scope',
body: [
'This privacy overview applies to the public TenantAtlas website and to information a visitor intentionally shares through the public contact path.',
'It does not describe tenant data processing inside the product itself, which belongs in product-specific legal and contractual materials.',
],
},
{
title: 'Information you choose to send',
body: [
'If you contact the team, the information you provide may include your name, company, role, email address, and the evaluation or governance questions you want to discuss.',
'The site should not ask for unnecessary secrets, production credentials, or tenant data through the public contact path.',
],
},
{
title: 'Use and retention',
body: [
'Information shared through the public contact path is used to understand the inquiry, respond to the request, and coordinate a relevant follow-up conversation.',
'Public-site inquiry information should be retained only for as long as needed to manage the evaluation discussion and related follow-up.',
],
},
{
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.',
'If the public-site data handling model changes materially, this page should be updated before or at the same time as the change.',
],
},
];

View File

@ -0,0 +1,110 @@
import type {
CalloutContent,
FeatureItemContent,
HeroContent,
MetricItem,
PageSeo,
} from '@/types/site';
export const productSeo: PageSeo = {
title: 'TenantAtlas | Product',
description:
'TenantAtlas connects inventory, snapshots, restore safety, drift visibility, findings, exceptions, and evidence into one governance model.',
path: '/product',
};
export const productHero: HeroContent = {
eyebrow: 'Product model',
title: 'One operating model for change history, drift visibility, and review readiness.',
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',
},
secondaryCta: {
href: '/contact',
label: 'Talk through your current operating model',
variant: 'secondary',
},
highlights: [
'Inventory first, snapshots second.',
'Restore flows stay previewable and attributable.',
'Evidence and review posture stay connected to real change history.',
],
};
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: '100%',
label: 'Queryable versions',
description: 'Version semantics stay tied to who changed what, when, and in which tenant context.',
},
];
export const productModelBlocks: FeatureItemContent[] = [
{
eyebrow: 'Connected governance model',
title: 'Inventory creates the starting point for every other decision.',
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.',
},
{
eyebrow: 'Connected governance model',
title: 'Snapshots add immutable history without replacing current 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.',
},
{
eyebrow: 'Connected governance model',
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',
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.',
},
{
eyebrow: 'Exceptions & 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.',
description:
'The product is built so teams can explain actions afterward, not just execute them quickly in the moment.',
},
];
export const productNarrative: CalloutContent[] = [
{
eyebrow: 'Why it is not a feature list',
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',
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',
title: 'No generic dashboard theater.',
description:
'The product story avoids pretending that another alerting page or compliance badge alone solves governance discipline.',
tone: 'subtle',
},
];

View File

@ -0,0 +1,78 @@
import type {
CalloutContent,
HeroContent,
PageSeo,
TrustPrincipleContent,
} from '@/types/site';
export const securityTrustSeo: PageSeo = {
title: 'TenantAtlas | Security & Trust',
description:
'TenantAtlas frames trust through substantiated product posture, safer restore discipline, and operational clarity rather than inflated guarantees.',
path: '/security-trust',
};
export const securityTrustHero: HeroContent = {
eyebrow: 'Security & Trust',
title: 'Explain the trust posture in the language of operational controls, not marketing claims.',
description:
'The public trust page should set realistic expectations: what the product helps teams observe, preserve, review, and restore, and where launch claims intentionally stay narrow until they can be substantiated further.',
primaryCta: {
href: '/legal',
label: 'Read the legal surface',
},
secondaryCta: {
href: '/contact',
label: 'Discuss trust requirements',
variant: 'secondary',
},
highlights: [
'Preview, confirmation, and auditability remain part of the restore story.',
'Public claims stay narrower than internal ambition.',
'No fake compliance theater for launch.',
],
};
export const securityPrinciples: TrustPrincipleContent[] = [
{
title: 'Safer changes are a product rule, not an afterthought.',
description:
'Destructive or high-risk flows are framed around preview, validation, and explicit confirmation instead of one-click confidence theater.',
note: 'Trust starts with operator discipline.',
},
{
title: 'Operational evidence stays tied to the real object and version.',
description:
'Findings, exceptions, and reviews remain anchored to observable tenant state so the trust story is defensible when someone asks for proof.',
note: 'Evidence should stay attributable.',
},
{
title: 'Launch messaging stays within substantiated boundaries.',
description:
'The public site avoids promising certifications, guarantees, or full automation outcomes that are not yet appropriate to claim.',
note: 'Restraint is part of credibility.',
},
];
export const securityTrustNotes: CalloutContent[] = [
{
eyebrow: 'Substantiated public posture',
title: 'Substantiated public posture',
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',
},
{
eyebrow: 'Sensitive connections',
title: 'Sensitive Microsoft connections should be explained carefully.',
description:
'The public site acknowledges Graph- and tenant-facing access without pretending the site itself is part of the runtime trust boundary.',
},
{
eyebrow: 'What we will not say',
title: 'No blanket assurances and no vague “fully automated governance” language.',
description:
'Trust pages lose credibility quickly when they substitute slogans for the actual controls and workflows a buyer will later inspect.',
tone: 'subtle',
},
];

View File

@ -0,0 +1,90 @@
import type {
AudienceRowContent,
FeatureItemContent,
HeroContent,
PageSeo,
} from '@/types/site';
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.',
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.',
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.',
primaryCta: {
href: '/integrations',
label: 'Review the ecosystem fit',
},
secondaryCta: {
href: '/contact',
label: 'Talk through your evaluation path',
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.',
],
};
export const solutionsAudiences: AudienceRowContent[] = [
{
audience: 'MSP',
title: 'MSP operating model',
description:
'Managed service providers need tenant-scoped operational truth, repeatable review workflows, and a way to explain change history to customers without drowning in manual evidence gathering.',
bullets: [
'Keep per-tenant history and restore posture reviewable during service delivery.',
'Support a higher tempo of customer change while preserving a clean audit story.',
'Give account and delivery teams a shared language for exceptions, findings, and follow-up.',
],
cta: {
href: '/contact',
label: 'Discuss MSP delivery fit',
variant: 'secondary',
},
},
{
audience: 'Enterprise IT',
title: 'Enterprise IT operating model',
description:
'Internal IT and security teams need durable version truth, change visibility, and review evidence that can stand up to operational leadership, audit, and cross-team scrutiny.',
bullets: [
'Reduce uncertainty around who changed what and when across the Microsoft tenant surface.',
'Support internal review packs, exception handling, and evidence collection without fragmented tooling.',
'Keep restore and remediation conversations grounded in the current tenant state and the relevant history.',
],
cta: {
href: '/security-trust',
label: 'Inspect the trust posture',
variant: 'secondary',
},
},
];
export const solutionsSignals: FeatureItemContent[] = [
{
eyebrow: 'Why buyers care',
title: 'The product is serious about both velocity and control.',
description:
'Teams can move quickly without giving up visibility, confirmation discipline, or explainability when a risky change needs review.',
},
{
eyebrow: 'Where it lands',
title: 'The product belongs in the operating layer, not just the reporting layer.',
description:
'Visitors should understand that TenantAtlas helps teams make safer decisions about configuration state rather than merely summarize activity afterward.',
},
{
eyebrow: 'How it reads',
title: 'The story changes by audience, but the product truth does not.',
description:
'MSP and enterprise readers see their own operating concerns reflected without the site inventing two different products.',
},
];

View File

@ -0,0 +1,60 @@
import type { HeroContent, LegalSection, PageSeo } from '@/types/site';
export const termsSeo: PageSeo = {
title: 'TenantAtlas | Terms',
description:
'Website terms for the public TenantAtlas surface, covering informational use of the site and the limits of public product statements.',
path: '/terms',
};
export const termsHero: HeroContent = {
eyebrow: 'Website terms',
title: 'Website terms for the public TenantAtlas surface.',
description:
'These terms describe the public website itself: informational use of the content, basic conduct expectations, and the fact that a public product site is not the same thing as a signed service agreement.',
primaryCta: {
href: '/contact',
label: 'Return to contact',
},
secondaryCta: {
href: '/privacy',
label: 'Review privacy',
variant: 'secondary',
},
highlights: [
'Public copy explains the product but does not replace commercial agreements.',
'The site is for evaluation and information, not operational control of a tenant.',
'Any future service commitment belongs in explicit signed terms.',
],
};
export const termsSections: LegalSection[] = [
{
title: 'Informational website use',
body: [
'The public TenantAtlas website is provided to explain the product category, trust posture, integrations direction, and contact path for evaluation conversations.',
'Nothing on the public site should be interpreted as a guarantee of product availability, certification, or commercial commitment unless it is later confirmed in signed agreements.',
],
},
{
title: 'Reasonable reliance',
body: [
'Visitors may use the public site to understand the product and decide whether to start a conversation, but they should not rely on public marketing pages as the sole source of contractual or implementation truth.',
'Detailed service commitments, security terms, and procurement obligations belong in later commercial and legal documentation.',
],
},
{
title: 'Acceptable conduct',
body: [
'Visitors should use the public website lawfully and should not attempt to interfere with the availability or integrity of the public site.',
'The public website is not a runtime administration surface and should not be treated as one.',
],
},
{
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.',
],
},
];

View File

@ -1,15 +1,26 @@
--- ---
import '../styles/global.css'; import '../styles/global.css';
import { siteMetadata } from '@/lib/site';
interface Props { interface Props {
canonicalUrl?: string;
description?: string; description?: string;
openGraphDescription?: string;
openGraphTitle?: string;
robots?: string;
title?: string; title?: string;
} }
const { const {
description = 'TenantPilot keeps Intune governance observable, reviewable, and safe to operate.', canonicalUrl,
title = 'TenantPilot', description = siteMetadata.siteDescription,
robots = 'index,follow',
title = `${siteMetadata.siteName} | ${siteMetadata.siteTagline}`,
} = Astro.props; } = Astro.props;
const openGraphTitle = Astro.props.openGraphTitle ?? title;
const openGraphDescription = Astro.props.openGraphDescription ?? description;
--- ---
<!doctype html> <!doctype html>
@ -17,11 +28,22 @@ const {
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="robots" content={robots} />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<meta property="og:site_name" content={siteMetadata.siteName} />
<meta property="og:title" content={openGraphTitle} />
<meta property="og:description" content={openGraphDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={openGraphTitle} />
<meta name="twitter:description" content={openGraphDescription} />
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title> <title>{title}</title>
</head> </head>
<body> <body>
<a class="skip-link" href="#content">Skip to content</a>
<slot /> <slot />
</body> </body>
</html> </html>

View File

@ -0,0 +1,27 @@
import { coreRoutes, siteMetadata } from '@/lib/site';
import type { PageSeo } from '@/types/site';
export interface ResolvedSeo extends PageSeo {
canonicalUrl: string;
ogDescription: string;
ogTitle: string;
robots: string;
}
export function buildCanonicalUrl(path: string): string {
return new URL(path, siteMetadata.siteUrl).toString();
}
export function resolveSeo(seo: PageSeo): ResolvedSeo {
return {
...seo,
canonicalUrl: buildCanonicalUrl(seo.path),
ogDescription: seo.ogDescription ?? seo.description,
ogTitle: seo.ogTitle ?? seo.title,
robots: 'index,follow',
};
}
export function sitemapEntries(): string[] {
return [...coreRoutes].map((path) => buildCanonicalUrl(path));
}

View File

@ -0,0 +1,76 @@
import type {
CtaLink,
FooterNavigationGroup,
NavigationItem,
SiteMetadata,
} from '@/types/site';
export const siteMetadata: SiteMetadata = {
siteName: 'TenantAtlas',
siteTagline: 'Governance of record for Microsoft tenant operations.',
siteDescription:
'TenantAtlas helps MSP and enterprise teams keep Microsoft tenant change history observable, reviewable, and safer to operate.',
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 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' },
],
},
];
export const contactCta: CtaLink = {
href: '/contact',
label: 'Request a working session',
helper: 'Bring your governance questions, rollout concerns, or evaluation goals.',
variant: 'primary',
};
export const coreRoutes = [
'/',
'/product',
'/solutions',
'/security-trust',
'/integrations',
'/contact',
'/legal',
'/privacy',
'/terms',
] as const;
export function isActiveNavigationPath(currentPath: string, href: string): boolean {
if (href === '/') {
return currentPath === '/';
}
return currentPath === href || currentPath.startsWith(`${href}/`);
}

View File

@ -0,0 +1,98 @@
---
import ContactPanel from '@/components/content/ContactPanel.astro';
import DemoPrompt from '@/components/content/DemoPrompt.astro';
import RichText from '@/components/content/RichText.astro';
import PageShell from '@/components/layout/PageShell.astro';
import Card from '@/components/primitives/Card.astro';
import Cluster from '@/components/primitives/Cluster.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Input from '@/components/primitives/Input.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import Textarea from '@/components/primitives/Textarea.astro';
import CTASection from '@/components/sections/CTASection.astro';
import PageHero from '@/components/sections/PageHero.astro';
import {
contactHero,
contactLegalSections,
contactPreview,
contactPrompts,
contactSeo,
contactTopics,
} from '@/content/pages/contact';
---
<PageShell currentPath="/contact" title={contactSeo.title} description={contactSeo.description}>
<PageHero
hero={contactHero}
calloutTitle="Qualified conversations beat anonymous form funnels."
calloutDescription="The page should make it obvious who should reach out, why, and what a useful first exchange looks like."
/>
<Section>
<Container 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} />)}
</Grid>
</Container>
</Section>
<Section>
<Container 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."
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">
<Input readonly value={contactPreview.topic} />
<Textarea readonly rows={7} value={contactPreview.message} />
</div>
</Card>
<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."
/>
<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>
</Cluster>
<div class="mt-6">
<RichText sections={contactLegalSections} />
</div>
</Card>
</div>
</Container>
</Section>
<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' }}
secondary={{ href: '/product', label: 'Revisit the product model', variant: 'secondary' }}
/>
</PageShell>

View File

@ -1,66 +1,65 @@
--- ---
import BaseLayout from '../layouts/BaseLayout.astro'; 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 FeatureGrid from '@/components/sections/FeatureGrid.astro';
import LogoStrip from '@/components/sections/LogoStrip.astro';
import PageHero from '@/components/sections/PageHero.astro';
import {
homeEcosystem,
homeHero,
homeMetrics,
homePillars,
homeProofBlocks,
homeSeo,
} from '@/content/pages/home';
--- ---
<BaseLayout <PageShell currentPath="/" title={homeSeo.title} description={homeSeo.description}>
title="TenantPilot | Workspace Foundation" <PageHero
description="The first public TenantPilot website surface for workspace-safe Intune operations." hero={homeHero}
> metrics={homeMetrics}
<main class="page-shell"> calloutTitle="Governance of record for Microsoft tenant operations."
<section class="hero"> calloutDescription="The public story positions TenantAtlas as a trust-first system for version truth, safer restore posture, drift visibility, evidence, and review support."
<p class="eyebrow">TenantPilot</p> />
<h1>One public website, one stable platform, one clear workspace model.</h1>
<p class="lede">
TenantPilot keeps Intune change management auditable for operators while the public
website stays fast, static, and operationally separate from the Laravel platform.
</p>
<div class="hero-actions"> <LogoStrip
<a class="primary-action" href="#workspace-model">View the workspace model</a> eyebrow="Ecosystem fit"
<a class="secondary-action" href="#boundaries">Review the isolation rules</a> title="Built around the Microsoft tenant reality buyers already need to govern."
items={homeEcosystem}
/>
<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."
items={homePillars}
/>
<Section>
<Container wide>
<div class="space-y-8">
<SectionHeader
eyebrow="Public proof"
title="A credible first reading should answer the buyers 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?"
/>
<Grid cols="3">
{homeProofBlocks.map((block) => <Callout content={block} />)}
</Grid>
</div> </div>
</section> </Container>
</Section>
<section class="signal-grid" id="workspace-model" aria-label="Workspace foundations"> <CTASection
<article class="signal-card"> eyebrow="Next step"
<p class="signal-label">Platform</p> title="Move from first-glance clarity into the deeper product story."
<h2>Laravel stays in <code>apps/platform</code>.</h2> 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."
<p> primary={{ href: '/product', label: 'See the product model' }}
Sail, Filament, Livewire, and deployment-sensitive runtime concerns remain secondary={{ href: '/contact', label: 'Start the working session', variant: 'secondary' }}
platform-owned and unchanged. />
</p> </PageShell>
</article>
<article class="signal-card">
<p class="signal-label">Website</p>
<h2>Astro lives independently in <code>apps/website</code>.</h2>
<p>
Public pages build statically, run without Laravel, and keep their own dev and
build outputs.
</p>
</article>
<article class="signal-card">
<p class="signal-label">Root</p>
<h2>The repository root orchestrates without becoming an app.</h2>
<p>
Root scripts expose the official entry commands while app-local execution logic
stays inside each app directory.
</p>
</article>
</section>
<section class="boundary-panel" id="boundaries">
<div>
<p class="eyebrow">Isolation</p>
<h2>Builds, ports, and ownership stay intentionally separate.</h2>
</div>
<ul class="boundary-list">
<li>Website dev defaults to port 4321 and supports explicit port overrides.</li>
<li>Platform Docker, queues, and Filament assets stay under the existing Sail flow.</li>
<li>No shared package layer, CMS, or extra app surface is introduced in this slice.</li>
</ul>
</section>
</main>
</BaseLayout>

View File

@ -0,0 +1,55 @@
---
import IntegrationBadge from '@/components/content/IntegrationBadge.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 FeatureGrid from '@/components/sections/FeatureGrid.astro';
import PageHero from '@/components/sections/PageHero.astro';
import {
integrationEntries,
integrationRules,
integrationsHero,
integrationsSeo,
} from '@/content/pages/integrations';
---
<PageShell currentPath="/integrations" title={integrationsSeo.title} description={integrationsSeo.description}>
<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."
/>
<Section>
<Container wide>
<div class="space-y-8">
<SectionHeader
eyebrow="Current direction"
title="Show the systems that shape the product workflow today."
description="This page should stay focused on the contracts and ecosystems that matter to Microsoft tenant governance work now."
/>
<Grid cols="2">
{integrationEntries.map((item) => <IntegrationBadge item={item} />)}
</Grid>
</div>
</Container>
</Section>
<FeatureGrid
eyebrow="Page rules"
title="Use the integrations page to clarify scope, not to perform ambition."
description="The public site becomes more credible when it names the real ecosystem fit and avoids presenting speculative adjacencies as if they were launch truth."
items={integrationRules}
/>
<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."
primary={{ href: '/contact', label: 'Plan the working session' }}
secondary={{ href: '/product', label: 'Revisit the product model', variant: 'secondary' }}
/>
</PageShell>

View File

@ -0,0 +1,75 @@
---
import RichText from '@/components/content/RichText.astro';
import PageShell from '@/components/layout/PageShell.astro';
import Card from '@/components/primitives/Card.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 { 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."
/>
<Section>
<Container 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."
/>
<Grid cols="3">
<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)]">
Review how the public contact path handles inquiry information and what this static website does not claim to process.
</p>
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="/privacy">
Privacy
</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>
<Card variant="accent">
<h3 class="m-0 text-2xl font-semibold text-[var(--color-ink-900)]">Public legal notice</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.
</p>
<a class="mt-5 inline-flex text-sm font-semibold text-[var(--color-brand)]" href="#public-legal-notice">
Legal notice
</a>
</Card>
</Grid>
</div>
</Container>
</Section>
<Section id="public-legal-notice">
<Container wide>
<RichText sections={legalNoticeSections} />
</Container>
</Section>
<CTASection
eyebrow="Continue"
title="Return to the contact path once the legal basics are 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' }}
/>
</PageShell>

View 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 { privacyHero, privacySections, privacySeo } from '@/content/pages/privacy';
---
<PageShell currentPath="/privacy" title={privacySeo.title} description={privacySeo.description}>
<PageHero
hero={privacyHero}
calloutTitle="Public-site privacy should stay narrow and readable."
calloutDescription="The page explains the website and inquiry path clearly without pretending to be the products full data-processing documentation."
/>
<Section>
<Container wide>
<RichText sections={privacySections} />
</Container>
</Section>
<CTASection
eyebrow="Next step"
title="Return to the product or contact flow after reviewing public-site privacy."
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' }}
/>
</PageShell>

View File

@ -0,0 +1,61 @@
---
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 FeatureGrid from '@/components/sections/FeatureGrid.astro';
import PageHero from '@/components/sections/PageHero.astro';
import {
productHero,
productMetrics,
productModelBlocks,
productNarrative,
productSeo,
} from '@/content/pages/product';
---
<PageShell currentPath="/product" title={productSeo.title} description={productSeo.description}>
<PageHero
hero={productHero}
metrics={productMetrics}
calloutTitle="Connected governance model"
calloutDescription="TenantAtlas connects present-state inventory, immutable snapshots, restore posture, drift, exceptions, and evidence so teams can explain what happened before they decide what to do next."
/>
<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."
items={productModelBlocks}
/>
<Section>
<Container 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."
/>
<Grid cols="3">
{productNarrative.map((block) => <Callout content={block} />)}
</Grid>
</div>
</Container>
</Section>
<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' }}
secondary={{
href: '/contact',
label: 'Talk through your current operating model',
variant: 'secondary',
}}
/>
</PageShell>

View File

@ -0,0 +1,59 @@
---
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';
---
<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>

View File

@ -0,0 +1,20 @@
import type { APIRoute } from 'astro';
import { sitemapEntries } from '@/lib/seo';
export const GET: APIRoute = () => {
const urls = sitemapEntries()
.map((url) => ` <url><loc>${url}</loc></url>`)
.join('\n');
const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
return new Response(body, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
},
});
};

View File

@ -0,0 +1,55 @@
---
import AudienceRow from '@/components/content/AudienceRow.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 FeatureGrid from '@/components/sections/FeatureGrid.astro';
import PageHero from '@/components/sections/PageHero.astro';
import {
solutionsAudiences,
solutionsHero,
solutionsSeo,
solutionsSignals,
} from '@/content/pages/solutions';
---
<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."
/>
<Section>
<Container 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."
/>
<Grid cols="2">
{solutionsAudiences.map((item) => <AudienceRow item={item} />)}
</Grid>
</div>
</Container>
</Section>
<FeatureGrid
eyebrow="Buying signal"
title="Give the buyer a concrete reason to keep evaluating."
description="The goal is not to decorate the page with vertical tags. The goal is to show why the product belongs in the operating model for that audience."
items={solutionsSignals}
/>
<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' }}
/>
</PageShell>

View 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 { termsHero, termsSections, termsSeo } from '@/content/pages/terms';
---
<PageShell currentPath="/terms" title={termsSeo.title} description={termsSeo.description}>
<PageHero
hero={termsHero}
calloutTitle="Public terms define the site, not a service contract."
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>
<RichText sections={termsSections} />
</Container>
</Section>
<CTASection
eyebrow="Next step"
title="Move back into privacy 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' }}
/>
</PageShell>

View File

@ -1,221 +1,157 @@
@import "tailwindcss";
@import "./tokens.css";
:root { :root {
color-scheme: light; color-scheme: light;
--bg: #f6efe5; --color-ink-900: #11243a;
--bg-accent: #fffdf9; --color-ink-800: #233a53;
--surface: rgba(255, 255, 255, 0.74); --color-copy: #42556a;
--surface-strong: rgba(255, 255, 255, 0.92); --color-line: rgba(17, 36, 58, 0.14);
--ink: #17120f; --color-panel: rgba(255, 255, 255, 0.82);
--muted: #66584d; --color-panel-strong: rgba(255, 255, 255, 0.95);
--line: rgba(23, 18, 15, 0.12); --color-panel-soft: rgba(243, 247, 251, 0.86);
--accent: #cc5f2c; --color-brand: #2f6fb7;
--accent-deep: #8b3820; --color-brand-soft: rgba(47, 111, 183, 0.12);
--shadow: 0 30px 80px rgba(103, 52, 33, 0.16); --color-signal: #3b8b78;
font-family: "Avenir Next", "Segoe UI", sans-serif; --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);
* {
box-sizing: border-box;
} }
html { html {
background: background:
radial-gradient(circle at top left, rgba(255, 201, 149, 0.55), transparent 34%), radial-gradient(circle at top left, rgba(255, 255, 255, 0.92), transparent 32%),
radial-gradient(circle at right 12% top 10%, rgba(255, 145, 96, 0.18), transparent 24%), radial-gradient(circle at top right, rgba(92, 149, 215, 0.18), transparent 28%),
linear-gradient(180deg, #fffaf3 0%, var(--bg) 58%, #efe3d5 100%); linear-gradient(180deg, #f6f3ee 0%, #edf2f7 56%, #f3f7fb 100%);
scroll-behavior: smooth;
} }
body { body {
margin: 0;
min-height: 100vh; min-height: 100vh;
color: var(--ink); margin: 0;
font-family: var(--font-sans);
color: var(--color-ink-900);
text-rendering: optimizeLegibility;
}
*,
*::before,
*::after {
box-sizing: border-box;
} }
a { a {
color: inherit; color: inherit;
} }
img {
display: block;
max-width: 100%;
}
code { code {
font-family: "SFMono-Regular", "SF Mono", "IBM Plex Mono", monospace; font-family: var(--font-mono);
font-size: 0.92em;
} }
.page-shell { ::selection {
width: min(1120px, calc(100% - 2rem)); background: rgba(47, 111, 183, 0.18);
margin: 0 auto; color: var(--color-ink-900);
padding: 4.5rem 0 5rem;
} }
.hero, :focus-visible {
.signal-card, outline: 3px solid rgba(47, 111, 183, 0.32);
.boundary-panel { outline-offset: 4px;
backdrop-filter: blur(18px);
box-shadow: var(--shadow);
} }
.hero { main {
display: block;
}
.surface-shell {
position: relative; position: relative;
overflow: hidden; isolation: isolate;
padding: clamp(2rem, 4vw, 4.5rem);
border: 1px solid var(--line);
border-radius: 2rem;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(255, 247, 239, 0.72)),
linear-gradient(120deg, rgba(204, 95, 44, 0.08), rgba(255, 255, 255, 0));
} }
.hero::after { .surface-shell::before {
content: "";
position: absolute; position: absolute;
inset: auto -8rem -8rem auto; inset: 0;
width: 18rem; z-index: -2;
aspect-ratio: 1; content: "";
border-radius: 999px; background:
background: radial-gradient(circle, rgba(204, 95, 44, 0.22), transparent 72%); 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%);
}
.eyebrow,
.signal-label { .surface-shell::after {
margin: 0 0 0.9rem; position: absolute;
text-transform: uppercase; inset: 1rem;
letter-spacing: 0.18em; z-index: -1;
font-size: 0.78rem; content: "";
font-weight: 700; border: 1px solid rgba(17, 36, 58, 0.04);
color: var(--accent-deep); border-radius: 2rem;
} }
.hero h1, .skip-link {
.boundary-panel h2, position: absolute;
.signal-card h2 { top: 1rem;
margin: 0; left: 1rem;
font-family: "Iowan Old Style", "Palatino Linotype", serif; z-index: 40;
line-height: 0.95; transform: translateY(-200%);
}
.hero h1 {
max-width: 13ch;
font-size: clamp(3rem, 8vw, 6rem);
}
.lede {
max-width: 46rem;
margin: 1.5rem 0 0;
font-size: clamp(1.05rem, 2vw, 1.35rem);
line-height: 1.7;
color: var(--muted);
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
}
.primary-action,
.secondary-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 3.25rem;
padding: 0.9rem 1.4rem;
border-radius: 999px; border-radius: 999px;
background: var(--color-ink-900);
padding: 0.75rem 1rem;
color: white;
text-decoration: none; text-decoration: none;
font-weight: 700; transition: transform 140ms ease;
transition:
transform 180ms ease,
box-shadow 180ms ease,
background-color 180ms ease;
} }
.primary-action { .skip-link:focus {
background: var(--ink); transform: translateY(0);
color: #fff7f1;
} }
.secondary-action { .glass-panel {
border: 1px solid rgba(23, 18, 15, 0.12); background: linear-gradient(180deg, var(--color-panel-strong), var(--color-panel));
background: rgba(255, 255, 255, 0.5); box-shadow: var(--shadow-panel);
backdrop-filter: blur(18px);
} }
.primary-action:hover, .section-divider {
.secondary-action:hover { border-top: 1px solid rgba(17, 36, 58, 0.08);
transform: translateY(-1px);
} }
.signal-grid { .legal-prose p {
display: grid; margin: 0;
gap: 1.25rem; color: var(--color-copy);
margin-top: 1.4rem; line-height: 1.8;
grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.signal-card, .legal-prose p + p {
.boundary-panel { margin-top: 1rem;
padding: 1.6rem;
border: 1px solid var(--line);
border-radius: 1.5rem;
background: var(--surface);
} }
.signal-card h2 { .legal-prose ul {
font-size: clamp(1.55rem, 3vw, 2.1rem);
}
.signal-card p:last-child,
.boundary-list {
margin: 1rem 0 0; margin: 1rem 0 0;
color: var(--muted); padding-left: 1.1rem;
line-height: 1.7; color: var(--color-copy);
line-height: 1.75;
} }
.boundary-panel { .legal-prose li + li {
display: grid; margin-top: 0.6rem;
gap: 1rem;
margin-top: 1.25rem;
background: var(--surface-strong);
} }
.boundary-panel h2 { .motion-rise {
font-size: clamp(2rem, 4vw, 3.1rem); animation: rise-in 520ms ease both;
} }
.boundary-list { @keyframes rise-in {
padding-left: 1.2rem; from {
opacity: 0;
transform: translateY(16px);
} }
.boundary-list li + li { to {
margin-top: 0.7rem; opacity: 1;
} transform: translateY(0);
@media (max-width: 920px) {
.signal-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.page-shell {
width: min(100% - 1.25rem, 1120px);
padding-top: 2rem;
padding-bottom: 3rem;
}
.hero,
.signal-card,
.boundary-panel {
border-radius: 1.3rem;
}
.hero {
padding: 1.4rem;
}
.hero-actions {
flex-direction: column;
}
.primary-action,
.secondary-action {
width: 100%;
} }
} }

View File

@ -0,0 +1,19 @@
@theme {
--font-sans: "Avenir Next", "Segoe UI", sans-serif;
--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);
}

View File

@ -0,0 +1,105 @@
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
export type PageRole =
| 'home'
| 'product'
| 'solutions'
| 'trust'
| 'integrations'
| 'contact'
| 'legal';
export interface CtaLink {
href: string;
label: string;
helper?: string;
target?: '_blank' | '_self';
variant?: ButtonVariant;
}
export interface NavigationItem {
href: string;
label: string;
description?: string;
}
export interface FooterNavigationGroup {
items: NavigationItem[];
title: string;
}
export interface SiteMetadata {
siteDescription: string;
siteName: string;
siteTagline: string;
siteUrl: string;
}
export interface PageSeo {
description: string;
ogDescription?: string;
ogTitle?: string;
path: string;
title: string;
}
export interface HeroContent {
description: string;
eyebrow: string;
highlights?: string[];
primaryCta: CtaLink;
secondaryCta?: CtaLink;
title: string;
}
export interface MetricItem {
description: string;
label: string;
value: string;
}
export interface FeatureItemContent {
description: string;
eyebrow?: string;
href?: string;
meta?: string;
title: string;
}
export interface CalloutContent {
description: string;
eyebrow?: string;
title: string;
tone?: 'accent' | 'neutral' | 'subtle';
}
export interface AudienceRowContent {
audience: string;
bullets: string[];
cta?: CtaLink;
description: string;
title: string;
}
export interface TrustPrincipleContent {
description: string;
note?: string;
title: string;
}
export interface IntegrationEntry {
category: string;
name: string;
note?: string;
summary: string;
}
export interface LogoStripItem {
label: string;
note?: string;
}
export interface LegalSection {
body: string[];
bullets?: string[];
title: string;
}

View File

@ -0,0 +1,57 @@
import { expect, test } from '@playwright/test';
import {
expectFooterLinks,
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 expectPrimaryNavigation(page);
await expectFooterLinks(page);
await expect(page.getByRole('main').getByRole('link', { name: 'Email the TenantAtlas team' }).first()).toBeVisible();
await expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
await expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
});
test('legal, privacy, 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 expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
await visitPage(page, '/privacy');
await expectShell(page, 'Public-site privacy overview for TenantAtlas inquiries.');
await visitPage(page, '/terms');
await expectShell(page, 'Website terms for the public TenantAtlas surface.');
});
test('core pages keep contact and legal paths within reach', async ({ page }) => {
for (const path of coreRoutes) {
await visitPage(page, path);
await expectFooterLinks(page);
if (path !== '/contact') {
await expect(page.locator('main a[href="/contact"]').first()).toBeVisible();
}
}
});
test.describe('mobile navigation', () => {
test.use({ viewport: { width: 390, height: 844 } });
test('mobile menu exposes the published contact and legal paths', async ({ page }) => {
await visitPage(page, '/');
await openMobileNavigation(page);
await expect(page.getByRole('banner').getByRole('link', { name: /Contact/ }).first()).toBeVisible();
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Privacy' })).toBeVisible();
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Terms' })).toBeVisible();
});
});

View File

@ -0,0 +1,39 @@
import { expect, test } from '@playwright/test';
import {
expectFooterLinks,
expectPrimaryNavigation,
expectShell,
visitPage,
} from './smoke-helpers';
test('home explains the product category and exposes the next step', async ({ page }) => {
await visitPage(page, '/');
await expectShell(page, /TenantAtlas/);
await expectPrimaryNavigation(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(),
).toBeVisible();
});
test('product explains the connected operating model instead of a loose feature list', async ({
page,
}) => {
await visitPage(page, '/product');
await expectShell(page, 'One operating model for change history, drift visibility, and review readiness.');
await expectPrimaryNavigation(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(),
).toBeVisible();
});

View File

@ -0,0 +1,45 @@
import { expect, type Page } from '@playwright/test';
export const primaryNavigationLabels = [
'Product',
'Solutions',
'Security & Trust',
'Integrations',
'Contact',
] as const;
export const footerLabels = ['Legal', 'Privacy', 'Terms', '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 expectShell(page: Page, heading: string | RegExp): Promise<void> {
await expect(page.getByRole('banner')).toBeVisible();
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByRole('contentinfo')).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: heading })).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();
}
}
export async function expectFooterLinks(page: Page): Promise<void> {
for (const label of footerLabels) {
await expect(page.getByRole('contentinfo').getByRole('link', { name: label })).toBeVisible();
}
}
export async function openMobileNavigation(page: Page): Promise<void> {
const menuTrigger = page.getByLabel('Open navigation menu');
if (await menuTrigger.isVisible()) {
await menuTrigger.click();
}
}

View File

@ -0,0 +1,37 @@
import { expect, test } from '@playwright/test';
import {
expectFooterLinks,
expectPrimaryNavigation,
expectShell,
visitPage,
} from './smoke-helpers';
test('solutions separates MSP and enterprise fit clearly', async ({ page }) => {
await visitPage(page, '/solutions');
await expectShell(page, /MSP|enterprise/i);
await expectPrimaryNavigation(page);
await expectFooterLinks(page);
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();
});
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);
await expectPrimaryNavigation(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();
});
test('integrations shows real ecosystem direction without wishlist claims', async ({ page }) => {
await visitPage(page, '/integrations');
await expectShell(page, /ecosystem fit|integrations/i);
await expectPrimaryNavigation(page);
await expectFooterLinks(page);
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();
});

View File

@ -0,0 +1,18 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
]
}

View File

@ -12,7 +12,7 @@ importers:
devDependencies: devDependencies:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) version: 4.2.2(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
axios: axios:
specifier: ^1.11.0 specifier: ^1.11.0
version: 1.14.0 version: 1.14.0
@ -27,7 +27,7 @@ importers:
version: 0.45.2(pg@8.20.0) version: 0.45.2(pg@8.20.0)
laravel-vite-plugin: laravel-vite-plugin:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.1.0(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) version: 2.1.0(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
pg: pg:
specifier: ^8.16.3 specifier: ^8.16.3
version: 8.20.0 version: 8.20.0
@ -39,13 +39,29 @@ importers:
version: 4.2.2 version: 4.2.2
vite: vite:
specifier: ^7.0.7 specifier: ^7.0.7
version: 7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) version: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
apps/website: apps/website:
dependencies: dependencies:
astro: astro:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(tsx@4.21.0) version: 6.1.4(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3)
devDependencies:
'@playwright/test':
specifier: ^1.59.1
version: 1.59.1
'@tailwindcss/vite':
specifier: ^4.2.2
version: 4.2.2(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
'@types/node':
specifier: ^24.7.2
version: 24.12.2
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
typescript:
specifier: ^5.9.3
version: 5.9.3
packages: packages:
@ -723,6 +739,11 @@ packages:
'@oslojs/encoding@1.1.0': '@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
'@playwright/test@1.59.1':
resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
engines: {node: '>=18'}
hasBin: true
'@rollup/pluginutils@5.3.0': '@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1013,6 +1034,9 @@ packages:
'@types/nlcst@2.0.3': '@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
'@types/node@24.12.2':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@ -2140,6 +2164,11 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.3: ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@ -2149,6 +2178,9 @@ packages:
uncrypto@0.1.3: uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@ -2784,6 +2816,10 @@ snapshots:
'@oslojs/encoding@1.1.0': {} '@oslojs/encoding@1.1.0': {}
'@playwright/test@1.59.1':
dependencies:
playwright: 1.59.1
'@rollup/pluginutils@5.3.0(rollup@4.60.1)': '@rollup/pluginutils@5.3.0(rollup@4.60.1)':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -2968,12 +3004,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': '@tailwindcss/vite@4.2.2(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))':
dependencies: dependencies:
'@tailwindcss/node': 4.2.2 '@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2 '@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2 tailwindcss: 4.2.2
vite: 7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) vite: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
'@types/debug@4.1.13': '@types/debug@4.1.13':
dependencies: dependencies:
@ -2995,6 +3031,10 @@ snapshots:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/node@24.12.2':
dependencies:
undici-types: 7.16.0
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@ -3016,7 +3056,7 @@ snapshots:
array-iterate@2.0.1: {} array-iterate@2.0.1: {}
astro@6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(tsx@4.21.0): astro@6.1.4(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(tsx@4.21.0)(typescript@5.9.3):
dependencies: dependencies:
'@astrojs/compiler': 3.0.1 '@astrojs/compiler': 3.0.1
'@astrojs/internal-helpers': 0.8.0 '@astrojs/internal-helpers': 0.8.0
@ -3062,14 +3102,14 @@ snapshots:
tinyclip: 0.1.12 tinyclip: 0.1.12
tinyexec: 1.1.1 tinyexec: 1.1.1
tinyglobby: 0.2.16 tinyglobby: 0.2.16
tsconfck: 3.1.6 tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0 ultrahtml: 1.6.0
unifont: 0.7.4 unifont: 0.7.4
unist-util-visit: 5.1.0 unist-util-visit: 5.1.0
unstorage: 1.17.5 unstorage: 1.17.5
vfile: 6.0.3 vfile: 6.0.3
vite: 7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) vite: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
vitefu: 1.1.3(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) vitefu: 1.1.3(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
xxhash-wasm: 1.1.0 xxhash-wasm: 1.1.0
yargs-parser: 22.0.0 yargs-parser: 22.0.0
zod: 4.3.6 zod: 4.3.6
@ -3614,10 +3654,10 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
laravel-vite-plugin@2.1.0(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)): laravel-vite-plugin@2.1.0(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)):
dependencies: dependencies:
picocolors: 1.1.1 picocolors: 1.1.1
vite: 7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) vite: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
vite-plugin-full-reload: 1.2.0 vite-plugin-full-reload: 1.2.0
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
@ -4411,7 +4451,9 @@ snapshots:
trough@2.2.0: {} trough@2.2.0: {}
tsconfck@3.1.6: {} tsconfck@3.1.6(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
tslib@2.8.1: {} tslib@2.8.1: {}
@ -4422,12 +4464,16 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
typescript@5.9.3: {}
ufo@1.6.3: {} ufo@1.6.3: {}
ultrahtml@1.6.0: {} ultrahtml@1.6.0: {}
uncrypto@0.1.3: {} uncrypto@0.1.3: {}
undici-types@7.16.0: {}
unified@11.0.5: unified@11.0.5:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@ -4517,7 +4563,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
picomatch: 2.3.2 picomatch: 2.3.2
vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0):
dependencies: dependencies:
esbuild: 0.27.7 esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
@ -4526,14 +4572,15 @@ snapshots:
rollup: 4.60.1 rollup: 4.60.1
tinyglobby: 0.2.16 tinyglobby: 0.2.16
optionalDependencies: optionalDependencies:
'@types/node': 24.12.2
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
lightningcss: 1.32.0 lightningcss: 1.32.0
tsx: 4.21.0 tsx: 4.21.0
vitefu@1.1.3(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)): vitefu@1.1.3(vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)):
optionalDependencies: optionalDependencies:
vite: 7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) vite: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
web-namespaces@2.0.1: {} web-namespaces@2.0.1: {}

View File

@ -0,0 +1,39 @@
# Specification Quality Checklist: Initial Website Foundation & v0 Product Site
**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 run: 2026-04-18
- No template placeholders or [NEEDS CLARIFICATION] markers remain.
- The spec stays outcome-first: it defines public product clarity, trust, audience fit, and conversion behavior without prescribing a framework or implementation stack.
- The scope is bounded to the smallest publishable website v0 and explicitly excludes product-app behavior, deep runtime coupling, full docs/blog/CMS rollout, and speculative trust claims.
- The spec records launch dependencies for legal facts, contact handling, and substantiated trust language so planning can treat them as explicit inputs rather than hidden assumptions.
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@ -0,0 +1,168 @@
openapi: 3.1.0
info:
title: TenantAtlas Public Website Surface Contract
version: 0.1.0
summary: Static route contract for the v0 TenantAtlas public website.
description: >-
This contract defines the required public routes for Spec 213. The website
serves static HTML pages only in this feature. Contact handling remains a
page-level conversion surface and does not introduce an internal submission
API in Spec 213.
servers:
- url: http://localhost:{port}
description: Local Astro development or preview server
variables:
port:
default: "4321"
tags:
- name: Public Pages
description: Public HTML routes required for v0 launch
paths:
/:
get:
tags: [Public Pages]
operationId: getHomePage
summary: Home page
description: Product-category framing, product pillars, trust positioning, and next-step CTA.
responses:
"200":
description: Home page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/product:
get:
tags: [Public Pages]
operationId: getProductPage
summary: Product page
description: Explains TenantAtlas as one connected governance model rather than a loose feature list.
responses:
"200":
description: Product page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/solutions:
get:
tags: [Public Pages]
operationId: getSolutionsPage
summary: Solutions page
description: Audience-specific fit for MSP and Enterprise IT visitors.
responses:
"200":
description: Solutions page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/security-trust:
get:
tags: [Public Pages]
operationId: getSecurityTrustPage
summary: Security and Trust page
description: Product principles, trust posture, and substantiated public claims.
responses:
"200":
description: Security and Trust page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/integrations:
get:
tags: [Public Pages]
operationId: getIntegrationsPage
summary: Integrations page
description: Public ecosystem fit for Microsoft-centric workflows and adjacent systems only where grounded in product truth.
responses:
"200":
description: Integrations page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/contact:
get:
tags: [Public Pages]
operationId: getContactPage
summary: Contact or demo page
description: Qualification and next-step page for contact intent.
responses:
"200":
description: Contact page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/legal:
get:
tags: [Public Pages]
operationId: getLegalIndexPage
summary: Legal index page
description: Landing page for public legal disclosures.
responses:
"200":
description: Legal index HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/privacy:
get:
tags: [Public Pages]
operationId: getPrivacyPage
summary: Privacy page
description: Public privacy disclosure required for launch.
responses:
"200":
description: Privacy page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/terms:
get:
tags: [Public Pages]
operationId: getTermsPage
summary: Terms page
description: Public terms disclosure required for launch.
responses:
"200":
description: Terms page HTML
content:
text/html:
schema:
$ref: "#/components/schemas/HtmlDocument"
/robots.txt:
get:
tags: [Public Pages]
operationId: getRobotsFile
summary: Robots file
description: Public robots instructions for search crawlers.
responses:
"200":
description: Robots text output
content:
text/plain:
schema:
type: string
/sitemap.xml:
get:
tags: [Public Pages]
operationId: getSitemapFile
summary: Sitemap file
description: Public XML sitemap covering the published website routes.
responses:
"200":
description: XML sitemap output
content:
application/xml:
schema:
type: string
components:
schemas:
HtmlDocument:
type: string
description: Server-rendered static HTML document

View File

@ -0,0 +1,158 @@
# Data Model: Initial Website Foundation & v0 Product Site
## Overview
This feature introduces no database schema. The model is file- and route-based inside `apps/website` and describes how public content, page composition, navigation, and conversion surfaces are structured.
## Entities
### Site Configuration
- **Purpose**: Global site metadata and shared shell settings used by every public page.
- **Key fields**:
- `siteName`
- `siteTagline`
- `defaultTitle`
- `defaultDescription`
- `defaultOgImage`
- `primaryNavigation`
- `footerNavigationGroups`
- `contactCta`
- **Relationships**:
- Owns many `NavigationItem` entries
- Supplies defaults to many `PublicPage` entries
- **Validation rules**:
- Must define a Home destination via brand/logo path
- Must include Product, Solutions, Security & Trust, Integrations, and Contact / Demo in primary navigation
- Must include Privacy and Terms in footer navigation
### Public Page
- **Purpose**: One published route in the v0 public website.
- **Key fields**:
- `slug`
- `title`
- `description`
- `routePath`
- `pageRole` (`home`, `product`, `solutions`, `trust`, `integrations`, `contact`, `legal`)
- `hero`
- `sections`
- `primaryCta`
- `secondaryCta`
- `seo`
- **Relationships**:
- Has one or more `NarrativeSection` entries
- May reference zero or more `AudienceNarrative` or `IntegrationEntry` entries depending on page role
- Consumes shared defaults from `SiteConfiguration`
- **Validation rules**:
- Each published page must have a unique `routePath`
- Each page must provide title, description, and at least one next-step path
- Product and Solutions must remain distinct page roles
### Narrative Section
- **Purpose**: Reusable section-level content block used to compose pages.
- **Key fields**:
- `sectionType` (`hero`, `problem-framing`, `feature-grid`, `trust-grid`, `logo-strip`, `cta`, `rich-text`, `audience-row`, `legal-prose`)
- `eyebrow`
- `headline`
- `body`
- `items`
- `themeVariant`
- **Relationships**:
- Belongs to one `PublicPage`
- May include many `SectionItem` entries through `items`
- **Validation rules**:
- Each section must support a clear narrative purpose
- Sections used on public core pages must remain semantically meaningful and not decorative-only
### Navigation Item
- **Purpose**: A user-facing route link in the header or footer.
- **Key fields**:
- `label`
- `href`
- `placement` (`header`, `footer`)
- `group`
- `order`
- `isExternal`
- **Relationships**:
- Belongs to `SiteConfiguration`
- Usually points to one `PublicPage`
- **Validation rules**:
- Footer items for legal routes must point to published pages
- No live navigation item may point to an unpublished placeholder route
### Call To Action
- **Purpose**: Standardized next-step action shown within or after narrative sections.
- **Key fields**:
- `label`
- `href`
- `variant` (`primary`, `secondary`)
- `context`
- **Relationships**:
- May belong to `PublicPage`, `NarrativeSection`, or `SiteConfiguration`
- **Validation rules**:
- Every core page must expose at least one CTA leading deeper into the product/trust/contact flow
- CTA labels must preserve consistent domain wording and avoid hype language
### Audience Narrative
- **Purpose**: Audience-specific framing used primarily on Solutions and supporting pages.
- **Key fields**:
- `audience`
- `problemStatement`
- `operatingModelFit`
- `proofPoints`
- `nextStep`
- **Relationships**:
- Belongs to the Solutions page or other audience-aware sections
- **Validation rules**:
- Must distinguish MSP and Enterprise IT narratives rather than collapsing them into generic copy
### Integration Entry
- **Purpose**: A public description of one real integration direction or ecosystem anchor.
- **Key fields**:
- `name`
- `summary`
- `scopeNote`
- `category`
- **Relationships**:
- Belongs to the Integrations page
- **Validation rules**:
- Must represent a real or explicitly planned integration direction
- Must not express speculative wishlist claims as if they were live product truth
### Legal Document
- **Purpose**: Public legal content required to support a credible launch.
- **Key fields**:
- `slug`
- `title`
- `summary`
- `sections`
- `lastReviewedAt`
- **Relationships**:
- Belongs to the Legal surface
- **Validation rules**:
- Privacy and Terms must both exist before launch
- Any required jurisdiction-specific public legal notice must exist either as a section of the Legal hub or as a dedicated linked legal document before launch
- Legal content must be reachable from the footer and the conversion path
## Relationship Summary
- `SiteConfiguration` has many `NavigationItem`
- `SiteConfiguration` provides defaults to many `PublicPage`
- `PublicPage` has many `NarrativeSection`
- `PublicPage` may have many `CallToAction`
- `Solutions` page has many `AudienceNarrative`
- `Integrations` page has many `IntegrationEntry`
- `Legal` surface has many `LegalDocument`
## State / Lifecycle Notes
- No persisted runtime states are introduced.
- Publishability is file- and route-driven: a route is “live” when the page exists and is intentionally linked; future sections remain absent from navigation until live.
- Trust and integration claims stay governed by content review rather than by a new application state machine.

View File

@ -0,0 +1,194 @@
# Implementation Plan: Initial Website Foundation & v0 Product Site
**Branch**: `213-website-foundation-v0` | **Date**: 2026-04-18 | **Spec**: `specs/213-website-foundation-v0/spec.md`
**Input**: Feature specification from `specs/213-website-foundation-v0/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
## Summary
- Keep `apps/website` as a fully static Astro 6 app and preserve its runtime separation from `apps/platform`.
- Introduce explicit TypeScript and Tailwind CSS v4, but implement the UI layer as custom shadcn-inspired Astro primitives instead of adding React plus official shadcn/ui.
- Ship a reusable public-site foundation with global layout, navigation, footer, seven core surfaces plus Privacy and Terms, SEO basics, and a lightweight browser-smoke validation path.
## Technical Context
**Language/Version**: Astro 6.0.0 templates + TypeScript 5.x (explicit setup in `apps/website`)
**Primary Dependencies**: Astro 6, Tailwind CSS v4, custom Astro component primitives (shadcn-inspired), lightweight Playwright browser smoke tests
**Storage**: Static filesystem content, styles, and assets under `apps/website/src` and `apps/website/public`; no database
**Testing**: Root build proof via `corepack pnpm build:website` plus Playwright browser smoke coverage for public routes
**Validation Lanes**: fast-feedback
**Target Platform**: Static public website for modern desktop/mobile browsers
**Project Type**: Web (standalone Astro app in a pnpm monorepo)
**Performance Goals**: Static HTML for all core routes, zero required framework hydration for reading/navigation flows, minimal client JS reserved for purposeful interactions only
**Constraints**: Preserve `@tenantatlas/website`, `WEBSITE_PORT`, root workspace scripts, and `apps/*` contracts; avoid platform auth/session/API coupling; ship one polished primary theme; publish only substantiated trust and integration claims
**Scale/Scope**: 7 core public surfaces plus Privacy and Terms as published legal routes, shared layout/content/conversion/trust primitives, and a future-ready structure for blog/docs/changelog without live rollout in v0
## 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 implementation stays inside `apps/website` and introduces no `/admin`, `/admin/t/{tenant}/...`, or `/system` runtime changes.
- Read/write separation: Pass. The feature is a public, static product site. Contact / Demo remains a static conversion surface in v0 and does not introduce a product-app write workflow.
- Workspace isolation: Pass. The website remains runtime-independent from `apps/platform`; no shared auth, session, tenant data, or implicit API dependency is introduced.
- Data minimization: Pass. Only public content, styles, metadata, and assets are authored; no tenant payloads, credentials, or operational records are stored.
- Test governance (TEST-GOV-001): Pass. Validation stays in `fast-feedback` with static build proof and lightweight browser smoke coverage; no DB/auth/provider fixtures or heavy-suite defaults are added.
- Proportionality / no premature abstraction: Pass. The plan adopts a small Astro/Tailwind primitive set instead of React + official shadcn/ui, a CMS, or a broader front-end framework.
- Persisted truth / new state: Pass. No database entities, queued work, run lifecycle, or new status families are introduced.
- UI semantics / few layers: Pass. Shared layout/content/conversion/trust primitives map directly to the website narrative without a presentation meta-framework.
Status: ✅ No constitution violations for this feature. The website remains public, static, repo-owned, and separate from platform runtime concerns.
## 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 public routes, navigation reachability, and layout stability; static build proof for artifact generation
- **Affected validation lanes**: fast-feedback
- **Why this lane mix is the narrowest sufficient proof**: The feature changes only a static Astro site. Build proof alone verifies output generation, while a tiny browser smoke suite is the smallest layer that can catch broken routes, broken navigation, and browser-visible regressions 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 pages do not require database, auth, provider, tenant, or workspace setup
- **Expensive defaults or shared helper growth introduced?**: no; any Playwright setup stays local to `apps/website`
- **Heavy-family additions, promotions, or visibility changes**: none
- **Closing validation and reviewer handoff**: Re-run the website build and smoke suite after page or layout changes. Reviewers should verify that core routes load, primary/footer navigation has no dead ends, the site stays mobile-usable, and no framework hydration is introduced without explicit justification.
- **Budget / baseline / trend follow-up**: none beyond tracking the small runtime cost of the website smoke suite inside this feature
- **Review-stop questions**: Does the proof stay in fast-feedback? Did any change introduce backend fixtures or hidden framework coupling? Is the smoke suite still small and route-focused?
- **Escalation path**: document-in-feature
- **Why no dedicated follow-up spec is needed**: The validation surface is tightly bounded to this features public pages. A follow-up spec is only needed if the website later grows interactive workflows, broader content tooling, or cross-app coupling.
## Project Structure
### Documentation (this feature)
```text
specs/213-website-foundation-v0/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
```text
apps/website/
├── astro.config.mjs
├── package.json
├── public/
├── src/
│ ├── components/
│ │ ├── layout/ # Navbar, Footer, page shell wrappers
│ │ ├── primitives/ # Container, Section, Button, Badge, Card, Input, Textarea
│ │ ├── sections/ # PageHero, FeatureGrid, TrustGrid, CTASection, LogoStrip
│ │ └── content/ # Audience rows, evidence callouts, legal prose helpers
│ ├── layouts/
│ │ └── BaseLayout.astro
│ ├── lib/ # Navigation, SEO, metadata, content helpers
│ ├── pages/
│ │ ├── index.astro
│ │ ├── product.astro
│ │ ├── solutions.astro
│ │ ├── security-trust.astro
│ │ ├── integrations.astro
│ │ ├── contact.astro
│ │ ├── legal.astro
│ │ ├── privacy.astro
│ │ ├── sitemap.xml.ts
│ │ └── terms.astro
│ ├── styles/
│ │ ├── global.css
│ │ └── tokens.css # Tailwind v4 theme tokens / bridge layer
│ ├── content/ # Future-ready content collections for docs/blog/changelog
│ └── types/ # Content and component prop types
└── tests/
└── smoke/ # Lightweight Playwright browser smoke coverage
```
**Structure Decision**: Keep the website fully isolated in `apps/website`. Use Astro pages plus a small internal design-system layer (`layout`, `primitives`, `sections`, `content`) and local browser smoke tests. Prepare a future `src/content/` and helper layer for later docs/blog/changelog expansion without shipping those sections in v0.
## Complexity Tracking
> **Fill when Constitution Check has violations that must be justified OR when BLOAT-001 is triggered by new persistence, abstractions, states, or semantic frameworks.**
None.
## Proportionality Review
> **Fill when the feature introduces a new enum/status family, DTO/presenter/envelope, persisted entity/table/artifact, interface/contract/registry/resolver, taxonomy/classification system, or cross-domain UI framework.**
- **Current operator problem**: Prospects and internal teams lack a credible public site that explains the product, trust model, and contact path without depending on the platform UI or ad hoc landing-page copy.
- **Existing structure is insufficient because**: The current Astro app only contains a single placeholder-style page, handwritten global CSS, and no reusable page system for navigation, legal pages, audience storytelling, or future content expansion.
- **Narrowest correct implementation**: A small Astro-native design-system layer backed by Tailwind CSS v4 and explicit TypeScript, with no React requirement and no CMS or runtime platform coupling.
- **Ownership cost created**: Ongoing maintenance of public copy, shared website primitives, Tailwind tokens, and a tiny local browser smoke suite.
- **Alternative intentionally rejected**: React + official shadcn/ui was rejected because it adds unnecessary client framework weight and maintenance cost for a static trust-first site; a single-page copy refresh was rejected because it would not establish a reusable v0 foundation.
- **Release truth**: Current-release truth
## Phase 0 — Outline & Research (complete)
- Output: `specs/213-website-foundation-v0/research.md`
- Key decisions captured:
- Keep Astro 6 static and runtime-independent from `apps/platform`.
- Add explicit TypeScript and Tailwind CSS v4 using the CSS-first Tailwind v4 model.
- Use custom shadcn-inspired Astro primitives instead of React + official shadcn/ui.
- Keep the Contact / Demo surface static in v0 and avoid introducing an Astro API/contact backend in this feature.
- Validate with root build proof plus a small Playwright browser smoke suite.
## Phase 1 — Design & Contracts (complete)
### Data model
- Output: `specs/213-website-foundation-v0/data-model.md`
- No database schema changes are required; the model is file- and route-based.
### Public site contracts
- Output: `specs/213-website-foundation-v0/contracts/public-site.openapi.yaml`
- Contract captures the required public GET routes and their HTML response expectations.
### Quickstart
- Output: `specs/213-website-foundation-v0/quickstart.md`
- Quickstart covers local development, build validation, and browser smoke execution.
### Agent context update
- Completed via `.specify/scripts/bash/update-agent-context.sh copilot` so the Copilot context reflects the website stack additions.
### Constitution re-check (post-design)
- ✅ Runtime separation remains intact: no platform auth/session/API coupling is introduced.
- ✅ No new persisted truth, state machines, or background operations are introduced.
- ✅ The chosen UI layer is the narrowest correct implementation for v0 and avoids premature React/framework abstraction.
- ✅ Validation remains cheap, local, and website-specific.
## Phase 2 — Implementation Plan (next)
### Story 1 (P1): Foundation and shared shell
- Add explicit TypeScript setup to `apps/website` and install Tailwind CSS v4 using the CSS-first configuration model.
- Introduce site metadata, navigation, footer configuration, and shared layout primitives (`Container`, `Section`, `SectionHeader`, `Button`, `Badge`, `Card`, `Input`, `Textarea`, `Navbar`, `Footer`, `PageHero`).
- Preserve the existing calm, enterprise-appropriate visual direction while shifting styling from page-local CSS toward reusable tokens and utilities.
- Tests / validation:
- Confirm the site still builds via `corepack pnpm build:website`.
- Add browser smoke coverage for Home plus one interior route.
### Story 2 (P1/P2): Core page shells and narrative system
- Build Home, Product, Solutions, Security & Trust, Integrations, Contact, Legal, Privacy, and Terms using shared `sections/` components (`FeatureGrid`, `TrustGrid`, `CTASection`, `LogoStrip`, audience-specific rows, evidence callouts).
- Ensure each core page exposes page purpose, clear hierarchy, semantic metadata, and visible next-step CTA.
- Keep Solutions separate from Product capabilities and keep Security & Trust limited to substantiated claims.
- Tests / validation:
- Extend browser smoke coverage to all published core routes.
- Assert primary and footer navigation reachability.
### Story 3 (P3): Delivery hardening and future-ready structure
- Add SEO and discovery basics (page titles, descriptions, Open Graph baseline, robots, explicit sitemap output, canonical metadata).
- Add a future-ready content helper layer and route organization for later docs/blog/changelog expansion without publishing those sections in v0.
- Ensure the Legal surface carries any required jurisdiction-specific public legal notice through the Legal hub or a linked dedicated notice page before launch.
- Keep contact handling static and repo-owned in this feature; document any later external form or scheduling integration as a follow-up decision rather than coupling it into the website foundation.
- Tests / validation:
- Finish Playwright smoke checks for mobile navigation, CTA reachability, and absence of dead-end routes.
- Re-verify root workspace scripts and package contracts remain intact.

View File

@ -0,0 +1,85 @@
# Quickstart: Initial Website Foundation & v0 Product Site
## Purpose
This quickstart describes the expected local workflow for implementing and validating the v0 TenantAtlas public website inside `apps/website`.
## Prerequisites
- Node 20+ available via the repo workspace tooling
- Corepack-enabled pnpm
- Repo root at `wt-website`
## Local Development
Start the website development server from the repo root:
```bash
corepack pnpm dev:website
```
The website must continue to honor the existing `WEBSITE_PORT` convention.
## Expected Foundation Work
Implement the feature in this order:
1. Add explicit TypeScript setup inside `apps/website`.
2. Add Tailwind CSS v4 using the CSS-first model and migrate styling toward shared tokens and reusable utilities.
3. Build shared layout, primitive, section, and content helpers.
4. Assemble the published core routes: Home, Product, Solutions, Security & Trust, Integrations, Contact, Legal, Privacy, Terms.
5. Add SEO/discovery basics and a lightweight Playwright browser smoke suite.
## Build Validation
Run the required website build proof from the repo root:
```bash
corepack pnpm build:website
```
Success means the Astro site builds cleanly without breaking root workspace contracts.
## Required Final Validation
Before considering the feature complete, run both proof commands:
```bash
corepack pnpm build:website
cd apps/website && corepack pnpm exec playwright test
```
## Browser Smoke Validation
After the Playwright smoke suite is added to `apps/website`, run:
```bash
cd apps/website
corepack pnpm exec playwright test
```
The smoke suite should remain intentionally small and verify:
- core public routes load successfully
- primary and footer navigation have no dead ends
- core CTAs remain reachable
- mobile navigation remains usable
## Content / Launch Review
Before calling the feature complete, confirm:
- every public claim is substantiated
- Product and Solutions stay distinct
- Security & Trust contains no speculative promises
- Integrations contains no wishlist-only entries presented as live truth
- Privacy and Terms are present and linked from footer and contact flow
- any required jurisdiction-specific public legal notice is present through the Legal hub or a linked dedicated notice page
## Out of Scope for This Feature
- product-app runtime coupling
- internal CRM / billing / customer portal logic
- a public docs/blog/changelog launch
- a required React layer
- an internal contact submission backend

View File

@ -0,0 +1,64 @@
# Research: Initial Website Foundation & v0 Product Site
## Decision 1: Keep `apps/website` as a fully static Astro 6 site
- **Decision**: Keep Astro 6 in static-output mode and preserve hard runtime separation from `apps/platform`.
- **Rationale**: The website is a trust-first product site, not an application surface. Static output keeps the site fast, cacheable, SEO-friendly, and operationally independent from Laravel, Filament, tenant state, and platform auth/session concerns.
- **Alternatives considered**:
- SSR or hybrid rendering: rejected because v0 does not need per-request logic and it would blur the separation between website and platform.
- A single ad hoc landing page refresh: rejected because it would not establish the reusable v0 website foundation required by the spec.
## Decision 2: Add explicit TypeScript and Tailwind CSS v4
- **Decision**: Introduce explicit TypeScript setup in `apps/website` and adopt Tailwind CSS v4 using the CSS-first configuration model.
- **Rationale**: The site needs a maintainable foundation for multiple routes, reusable primitives, and later content expansion. TypeScript makes component props, metadata, and content structures safer; Tailwind v4 scales layout and typography decisions better than continued page-local CSS growth.
- **Alternatives considered**:
- Keep handwritten global CSS only: rejected because the page count and reusable-section scope in v0 would quickly turn styling into duplicated local CSS islands.
- Delay TypeScript: rejected because the feature explicitly introduces a reusable component layer and content helpers that benefit from typed contracts immediately.
## Decision 3: Do not introduce React for v0
- **Decision**: Do not add React as part of Spec 213.
- **Rationale**: React is not required to deliver the v0 product site, and it adds framework, hydration, and dependency cost without solving a concrete launch problem. Astro already supports a static-first component model that fits the sites reading, navigation, and CTA flows.
- **Alternatives considered**:
- React islands for all UI: rejected because it introduces unnecessary client framework weight for a content-driven public site.
- “Add React now for future flexibility”: rejected under PROP-001 and ABSTR-001 because future optional interactivity is not enough reason to pay the cost in v0.
## Decision 4: Use custom shadcn-inspired Astro primitives instead of official shadcn/ui
- **Decision**: Use a custom Astro-native design-system layer inspired by shadcn/ui conventions rather than installing React plus official shadcn/ui.
- **Rationale**: The product needs an intentional, enterprise-credible public surface. Astro-native primitives keep ownership local, avoid framework coupling, and still allow the team to use shadcn-like conventions for buttons, cards, inputs, badges, sheets, tabs, and dialogs if needed later.
- **Alternatives considered**:
- Full React + official shadcn/ui: rejected because it solves a speed-of-assembly problem at the cost of unnecessary framework/runtime complexity.
- Ported third-party Astro shadcn clones: rejected because they split ownership and can still impose design decisions broader than the feature needs.
## Decision 5: Keep Contact / Demo static in v0
- **Decision**: Treat Contact / Demo as a static conversion surface in Spec 213. The page may point to a controlled external/manual intake path, but Spec 213 does not introduce an internal website backend or API contract for form submission.
- **Rationale**: The feature goal is to establish a trustworthy public site foundation, not to solve CRM or submission pipeline architecture. Keeping the contact surface static avoids backend/runtime coupling and leaves room for a later explicit decision on form handling.
- **Alternatives considered**:
- Build an Astro endpoint or app-backed form now: rejected because form handling is still an open product/ops decision and is not required to make the v0 site credible.
- Omit contact intent entirely: rejected because the spec requires a clear conversion path.
## Decision 6: Public route structure should be small, explicit, and future-ready
- **Decision**: Publish the core route set now and organize the source tree so docs/blog/changelog can be added later without rewriting the live page hierarchy.
- **Rationale**: The v0 website needs clarity more than breadth. A small public sitemap makes the product understandable now while avoiding future structural dead ends.
- **Alternatives considered**:
- Publish docs/blog/changelog placeholders now: rejected because dead or thin routes reduce trust.
- Keep all content in one giant home page: rejected because Product, Solutions, Trust, Integrations, and Contact need distinct narrative roles.
## Decision 7: Use lightweight browser smoke validation plus build proof
- **Decision**: Keep validation in `fast-feedback` with root build proof and a small Playwright browser smoke suite local to `apps/website`.
- **Rationale**: Build proof alone does not catch broken routes, broken navigation, or browser-visible regressions. A tiny smoke suite gives real confidence without importing backend fixtures or a heavy end-to-end lane.
- **Alternatives considered**:
- Build-only validation: rejected because it can still ship broken pages.
- Heavy end-to-end coverage: rejected because the site has no authenticated workflow or backend interaction that justifies it.
## Baseline Findings
- `apps/website` currently uses Astro 6 with `output: 'static'`.
- The current site consists of a single `index.astro`, a minimal `BaseLayout.astro`, and handwritten global CSS.
- No TypeScript setup, Tailwind install, React integration, or dedicated website test tooling is currently present.
- Root workspace contracts already expose `corepack pnpm dev:website` and `corepack pnpm build:website`, and those must remain intact.

View File

@ -0,0 +1,161 @@
# Feature Specification: Initial Website Foundation & v0 Product Site
**Feature Branch**: `213-website-foundation-v0`
**Created**: 2026-04-18
**Status**: Draft
**Input**: User description: "Establish the initial public TenantAtlas website foundation and ship the first trust-first v0 product site in `apps/website`."
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: The website track does not yet explain TenantAtlas credibly to enterprise and MSP buyers, so the product lacks a trustworthy public entry point.
- **Today's failure**: A first-time visitor cannot quickly tell what TenantAtlas is, how it differs from generic backup or reporting tools, or whether it is serious enough for enterprise evaluation.
- **User-visible improvement**: Visitors can understand the product model, assess trust posture, and reach a qualified next step without digging through incomplete or placeholder pages.
- **Smallest enterprise-capable version**: A public website v0 with a clear home page, core product/trust/solutions/integrations/contact/legal pages, shared layout primitives, and a direct contact path.
- **Explicit non-goals**: No product-app behavior, no deep runtime coupling to the platform, no public roadmap/community hub, no full docs/blog/CMS rollout, no multi-language launch, no template clone, and no speculative trust-center claims.
- **Permanent complexity imported**: A stable public information architecture, reusable website primitives, shared messaging structure, legal page responsibilities, and website-specific validation expectations.
- **Why now**: TenantAtlas needs a credible public surface before broader rollout, trust conversations, and later expansion into docs, changelog, or resource content.
- **Why not local**: A one-off landing page or copy-only patch would not create a stable public story, reusable page composition, or an extensible enterprise-ready website track.
- **Approval class**: Core Enterprise
- **Red flags triggered**: #4 sounds like foundation work; #5 broad concept vocabulary. The scope stays justified because it is constrained to the smallest publishable site that improves public product clarity, trust, and conversion.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: `/`, `/product`, `/solutions`, `/security-trust`, `/integrations`, `/contact`, `/legal`, `/privacy`, `/terms`
- **Data Ownership**: Workspace-owned public content, assets, reusable components, and site configuration inside `apps/website`; no tenant-owned records or platform runtime data.
- **RBAC**: Public-read runtime only. No authenticated membership or capability checks are required for v0 website browsing; content changes remain repo-controlled.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: yes
- **Current operator problem**: Prospects and internal teams lack a stable, trustworthy public site that explains the product, trust posture, and next step without resorting to ad hoc copy or app screenshots.
- **Existing structure is insufficient because**: The current website track is too minimal to support coherent product messaging, reusable page composition, clear trust framing, or later content expansion without rework.
- **Narrowest correct implementation**: One bounded website v0 with shared layout and content primitives, a small core route set, a clear contact path, and explicit messaging constraints; no app simulation, no CMS, and no broad content platform.
- **Ownership cost**: Ongoing maintenance of public copy, reusable website primitives, legal content, and a small public information architecture.
- **Alternative intentionally rejected**: A single landing page or imported theme was rejected because it would not create durable enterprise messaging, trustworthy trust surfaces, or an extensible public structure aligned with the product.
- **Release truth**: Current-release truth
## 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**: The feature is proven by public-page rendering, route reachability, content hierarchy, and responsive navigation rather than by tenant data or business-rule execution.
- **New or expanded test families**: Lightweight Playwright website smoke coverage for core published routes and contact/legal reachability, plus the existing static-build proof.
- **Fixture / helper cost impact**: Minimal. Public pages do not require seeded tenant, workspace, or authentication state.
- **Heavy-family visibility / justification**: none
- **Reviewer handoff**: Reviewers must confirm that core pages render, primary navigation has no dead ends, contact/legal routes remain reachable, the website build passes, and the website smoke suite passes while staying website-specific rather than borrowing platform-heavy lanes.
- **Budget / baseline / trend impact**: Small increase limited to website build and any added public-site smoke checks.
- **Escalation needed**: none
- **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 Quickly (Priority: P1)
A first-time enterprise or MSP visitor lands on the public site and can understand what TenantAtlas does, why it differs from generic backup or reporting tools, and what to do next without seeing the product app.
**Why this priority**: If the public site fails to create immediate product clarity, deeper trust and conversion flows do not matter.
**Independent Test**: This can be tested by visiting the Home page and Product page only and confirming that the product category, audience, and primary next step are understandable from public content alone.
**Acceptance Scenarios**:
1. **Given** a first-time visitor lands on Home, **When** they scan the primary sections, **Then** they can identify what TenantAtlas does, who it serves, and why it exists.
2. **Given** a visitor is unsure whether TenantAtlas is only a backup tool, **When** they open Product, **Then** the site explains a connected governance model rather than a loose feature list.
3. **Given** a visitor wants proof of seriousness, **When** they move from Home into deeper pages, **Then** the site provides trust and ecosystem context without hype or placeholder claims.
---
### User Story 2 - Evaluate Fit by Audience (Priority: P2)
An enterprise IT buyer, MSP operator, or governance stakeholder can navigate the public site to decide whether TenantAtlas fits their environment and operating model.
**Why this priority**: Qualified evaluation reduces mismatched leads and makes the site useful beyond top-level awareness.
**Independent Test**: This can be tested by navigating from the primary menu to Solutions and Integrations and confirming that audience-specific fit signals are present without requiring private collateral.
**Acceptance Scenarios**:
1. **Given** an MSP visitor opens Solutions, **When** they review the page, **Then** they find multi-tenant governance and operational-fit scenarios relevant to service providers.
2. **Given** an enterprise IT visitor opens Solutions, **When** they review the page, **Then** they see enterprise operating-model language clearly separated from MSP framing.
3. **Given** a technical evaluator opens Integrations, **When** they assess ecosystem fit, **Then** they see real integration direction only and no speculative wishlist.
---
### User Story 3 - Reach a Qualified Next Step (Priority: P3)
A serious buyer can move from any core page to a clear contact or demo path with realistic expectations about who should reach out and why.
**Why this priority**: Conversion should feel confident and intentional, not aggressive or ambiguous.
**Independent Test**: This can be tested by starting on any core page and reaching the contact path without dead ends, broken hierarchy, or unclear calls to action.
**Acceptance Scenarios**:
1. **Given** a visitor is on any core page, **When** they look for the next step, **Then** a visible CTA guides them toward contact or demo.
2. **Given** a visitor reaches Contact / Demo, **When** they review the page, **Then** they can tell who should get in touch, what topics the conversation covers, and what response to expect.
3. **Given** a visitor wants legal reassurance before submitting interest, **When** they use the footer or contact flow, **Then** privacy and terms information are reachable.
### Edge Cases
- What happens when a visitor lands directly on an interior page from search or a shared link? The page must still orient the visitor, reconnect them to the main product story, and expose a clear next step.
- How does the site handle claims that cannot yet be substantiated? The content must omit or soften the claim instead of implying unverified security, compliance, or automation guarantees.
- What happens when optional future sections such as blog, changelog, or docs are not yet live? Navigation and internal links must not expose dead or placeholder routes.
- How does the site behave on narrow screens or slower connections? Core navigation, copy hierarchy, and contact/legal reachability must remain usable without animation- or script-dependent gating.
## Requirements *(mandatory)*
This feature does not introduce product-app operator surfaces, Microsoft-tenant actions, queued work, or runtime authorization changes. The public website remains a separate, workspace-owned surface with no required auth, session, or API coupling to the platform for v0.
### Functional Requirements
- **FR-001**: The public website MUST position TenantAtlas as a trust-first governance-of-record product for Microsoft tenant and Intune configuration state, emphasizing clarity around backup, restore, version history, auditability, drift visibility, findings, exceptions, evidence, and reviews.
- **FR-002**: The v0 release MUST publish a Home page, Product page, Solutions page, Security & Trust page, Integrations page, Contact / Demo page, and a Legal surface. The Legal surface MUST expose Privacy and Terms content plus any jurisdiction-specific public legal notice required before launch, either within the Legal hub or via a linked dedicated notice page.
- **FR-003**: The site MUST provide a global page shell that includes a Home link through the brand mark, consistent primary navigation, and footer navigation that keeps core pages and legal pages reachable.
- **FR-004**: Home MUST explain the product category, frame the problem, present the major product pillars, establish why the product matters now, and direct visitors into deeper product, trust, and contact paths.
- **FR-005**: Product MUST explain the product as one connected operating model rather than as an unstructured feature inventory.
- **FR-006**: Solutions MUST speak to at least MSP and Enterprise IT audiences, showing how the product fits each operating model without collapsing them into one generic story.
- **FR-007**: Security & Trust MUST communicate product principles, trust posture, and handling of sensitive connections only at a level the team can substantiate at launch.
- **FR-008**: Integrations MUST show the real ecosystem fit for the product and MUST avoid speculative or wishlist-only integrations.
- **FR-009**: Contact / Demo MUST explain who should get in touch, common reasons to reach out, and what kind of discussion or follow-up the visitor should expect.
- **FR-010**: Legal content MUST be reachable from the footer and from relevant conversion paths before a visitor submits interest.
- **FR-011**: The website MUST use shared layout, content, conversion, and trust primitives so pages are assembled from reusable building blocks instead of page-by-page duplication.
- **FR-012**: The visual direction MUST feel calm, readable, technically serious, and enterprise-appropriate, while avoiding over-animation, consumer-style hype, and decorative patterns that undercut credibility.
- **FR-013**: Public copy MUST avoid misleading claims and banned framing such as guaranteed security outcomes, false reassurance, fully automated governance promises, or consumer/startup hype language.
- **FR-014**: The v0 website MUST remain independent from platform runtime concerns. It MUST NOT require product-app sessions, shared auth, or deep runtime coupling to explain the product or capture contact intent.
- **FR-015**: Content organization for v0 MAY remain page-managed, but the live structure MUST allow later expansion into concepts, changelog, resources, blog, or docs without rebuilding the published core-page information architecture.
- **FR-016**: Every published core page MUST include clear page purpose, semantic structure, discoverability basics, including page metadata, robots handling, and sitemap coverage, and a visible next step deeper into the product, trust story, or contact flow.
- **FR-017**: The website track MUST preserve existing workspace contracts, including the package identity, website port convention, workspace script names, and monorepo layout assumptions relied on elsewhere in the repo.
- **FR-018**: The launch version MUST be fully polished in one primary visual theme. Alternate theme support may be prepared, but it is not required for v0.
- **FR-019**: The v0 website MUST remain fully useful without introducing an unnecessary client-side application layer for core navigation, reading, and contact intent.
### Key Entities *(include if feature involves data)*
- **Core Public Page**: A top-level public route with a defined narrative purpose, section hierarchy, and next-step CTA.
- **Narrative Section**: A reusable content block that communicates product explanation, trust evidence, audience fit, or conversion intent.
- **Conversion Path**: The CTA and destination flow that moves an interested visitor from discovery into contact or demo.
- **Legal Surface**: The set of public pages that provide privacy, terms, and any other required legal disclosures supporting launch.
## Assumptions & Dependencies
- Actual company/legal facts needed for privacy, terms, and any jurisdiction-specific notice will be available before release.
- Contact handling may begin with a lightweight team-owned intake path, provided the public site sets clear expectations for purpose and follow-up.
- Product and trust claims will be limited to capabilities and operational truths that can be substantiated at launch.
- Future article, changelog, and documentation areas may remain unpublished in v0.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: A first-time visitor can identify what TenantAtlas does, who it is for, and the primary next step from the Home page within 60 seconds of reading.
- **SC-002**: The v0 release publishes at least seven core public surfaces, including legal access points, with no dead-end navigation from the primary menu or footer.
- **SC-003**: A visitor can reach a contact or demo path from any core page in two clicks or fewer.
- **SC-004**: Every core page includes a visible next-step CTA and at least one deeper path into the product, trust, or contact story.
- **SC-005**: No released page contains placeholder copy, unsubstantiated trust or compliance claims, or speculative integration promises.
- **SC-006**: Core pages remain readable and navigable on both desktop and mobile widths without horizontal scrolling or hidden primary navigation.

View File

@ -0,0 +1,201 @@
# Tasks: Initial Website Foundation & v0 Product Site
**Input**: Design documents from `/specs/213-website-foundation-v0/`
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/public-site.openapi.yaml`
**Tests**: Browser smoke coverage is required for this runtime-changing website feature, together with the root website build proof.
## 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 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 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.
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Establish the minimal website tooling and config baseline required before reusable UI work begins.
- [X] T001 Add explicit website TypeScript configuration in `apps/website/tsconfig.json`
- [X] T002 [P] Add Tailwind CSS v4 and Playwright dev dependencies plus website test scripts in `apps/website/package.json`
- [X] T003 [P] Update Astro aliases and website-safe build configuration in `apps/website/astro.config.mjs`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Create the shared shell, primitives, tokens, metadata, and smoke-test harness that every page depends on.
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
- [X] T004 Configure Tailwind v4 theme tokens and base website styles in `apps/website/src/styles/global.css` and `apps/website/src/styles/tokens.css`
- [X] T005 [P] Create shared site metadata, navigation, and footer configuration in `apps/website/src/lib/site.ts` and `apps/website/src/types/site.ts`
- [X] T006 [P] Add the browser smoke-test harness in `apps/website/playwright.config.ts` and `apps/website/tests/smoke/smoke-helpers.ts`
- [X] T007 Build the base document and page shell wrappers in `apps/website/src/layouts/BaseLayout.astro` and `apps/website/src/components/layout/PageShell.astro`
- [X] T008 [P] Build global navigation and footer components in `apps/website/src/components/layout/Navbar.astro` and `apps/website/src/components/layout/Footer.astro`
- [X] T009 [P] Implement layout 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] T010 [P] Implement shared UI primitives 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] T011 Build reusable section scaffolds 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**: Foundation ready. User-story page work can now begin.
---
## Phase 3: User Story 1 - Understand the Product Quickly (Priority: P1) 🎯 MVP
**Goal**: Deliver the first credible public explanation of TenantAtlas through Home and Product.
**Independent Test**: Visit Home and Product, verify both routes render from the shared shell, and confirm the smoke suite proves product-category clarity and next-step CTA reachability without platform coupling.
### Tests for User Story 1
> **NOTE**: Write this test first and confirm it fails before implementing the story.
- [X] T012 [P] [US1] Write failing smoke coverage for Home and Product in `apps/website/tests/smoke/home-product.spec.ts`
### Implementation for User Story 1
- [X] T013 [P] [US1] Create Home and Product content modules in `apps/website/src/content/pages/home.ts` and `apps/website/src/content/pages/product.ts`
- [X] T014 [P] [US1] Create product-explanation content primitives in `apps/website/src/components/content/Eyebrow.astro`, `apps/website/src/components/content/Headline.astro`, `apps/website/src/components/content/Lead.astro`, and `apps/website/src/components/content/FeatureItem.astro`
- [X] T015 [P] [US1] Create supporting proof blocks in `apps/website/src/components/content/Callout.astro` and `apps/website/src/components/content/Metric.astro`
- [X] T016 [US1] Implement the Home and Product routes in `apps/website/src/pages/index.astro` and `apps/website/src/pages/product.astro`
**Checkpoint**: Home and Product are independently functional and demonstrable as the MVP slice.
---
## Phase 4: User Story 2 - Evaluate Fit by Audience (Priority: P2)
**Goal**: Add audience-fit, trust framing, and ecosystem-fit pages that help visitors judge whether TenantAtlas fits their environment.
**Independent Test**: Visit Solutions, Security & Trust, and Integrations from the shared navigation and verify the smoke suite proves audience-specific framing, substantiated trust messaging, and non-speculative integrations.
### Tests for User Story 2
> **NOTE**: Write this test first and confirm it fails before implementing the story.
- [X] T017 [P] [US2] Write failing smoke coverage for Solutions, Security & Trust, and Integrations in `apps/website/tests/smoke/solutions-trust-integrations.spec.ts`
### Implementation for User Story 2
- [X] T018 [P] [US2] Create Solutions, Security & Trust, and Integrations content modules in `apps/website/src/content/pages/solutions.ts`, `apps/website/src/content/pages/security-trust.ts`, and `apps/website/src/content/pages/integrations.ts`
- [X] T019 [P] [US2] Create audience and trust content primitives in `apps/website/src/components/content/AudienceRow.astro`, `apps/website/src/components/content/TrustPrincipleCard.astro`, and `apps/website/src/components/content/IntegrationBadge.astro`
- [X] T020 [P] [US2] Implement the Solutions route in `apps/website/src/pages/solutions.astro`
- [X] T021 [P] [US2] Implement the Security & Trust route in `apps/website/src/pages/security-trust.astro`
- [X] T022 [P] [US2] Implement the Integrations route in `apps/website/src/pages/integrations.astro`
**Checkpoint**: Audience fit, trust framing, and integration fit are independently functional and testable.
---
## Phase 5: User Story 3 - Reach a Qualified Next Step (Priority: P3)
**Goal**: Deliver the contact/demo path, legal surfaces, and cross-page CTA/legal reachability needed for a trustworthy conversion flow.
**Independent Test**: Visit Contact, Legal, Privacy, and Terms and confirm the smoke suite proves CTA reachability and footer/legal access from every published core page.
### Tests for User Story 3
> **NOTE**: Write this test first and confirm it fails before implementing the story.
- [X] T023 [P] [US3] Write failing smoke coverage for Contact, Legal, Privacy, Terms, footer links, CTA reachability, and legal-surface availability in `apps/website/tests/smoke/contact-legal.spec.ts`
### Implementation for User Story 3
- [X] T024 [P] [US3] Create Contact and legal content modules, including any required jurisdiction-specific public legal notice content, in `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] T025 [P] [US3] Create conversion and legal content primitives in `apps/website/src/components/content/PrimaryCTA.astro`, `apps/website/src/components/content/SecondaryCTA.astro`, `apps/website/src/components/content/ContactPanel.astro`, `apps/website/src/components/content/DemoPrompt.astro`, and `apps/website/src/components/content/RichText.astro`
- [X] T026 [P] [US3] Implement the Contact route in `apps/website/src/pages/contact.astro`
- [X] T027 [P] [US3] Implement the Legal hub route, including any required jurisdiction-specific public legal notice path, in `apps/website/src/pages/legal.astro`
- [X] T028 [P] [US3] Implement the Privacy and Terms routes in `apps/website/src/pages/privacy.astro` and `apps/website/src/pages/terms.astro`
- [X] T029 [US3] Add SEO and future-ready content scaffolding, including explicit sitemap output, in `apps/website/src/lib/seo.ts`, `apps/website/src/content.config.ts`, `apps/website/public/robots.txt`, `apps/website/src/pages/sitemap.xml.ts`, and `apps/website/astro.config.mjs`
- [X] T030 [US3] Wire final CTA and footer/legal reachability across `apps/website/src/pages/index.astro`, `apps/website/src/pages/product.astro`, `apps/website/src/pages/solutions.astro`, `apps/website/src/pages/security-trust.astro`, `apps/website/src/pages/integrations.astro`, and `apps/website/src/pages/contact.astro`
**Checkpoint**: The qualified contact path and legal reachability are functional across the full published site.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Final validation, wording review, and workspace-contract verification across all stories.
- [X] T031 [P] Validate both required fast-feedback proof commands against `apps/website/package.json`, `apps/website/playwright.config.ts`, and `specs/213-website-foundation-v0/quickstart.md`
- [X] T032 [P] Review and tighten public wording against substantiated-claim and banned-wording rules in `apps/website/src/content/pages/home.ts`, `apps/website/src/content/pages/product.ts`, `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] T033 Verify the workspace script and package contracts remain intact in `package.json` and `apps/website/package.json`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: Starts immediately.
- **Foundational (Phase 2)**: Depends on Setup and blocks all story work.
- **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 and needs the core routes from US1 and US2 in place to satisfy “from any core page” CTA/legal reachability.
- **Polish (Phase 6)**: Depends on all targeted stories being complete.
### User Story Dependencies
- **US1**: No dependency on other user stories; this is the MVP slice.
- **US2**: No dependency on US1 for implementation, but it reuses the same shared shell and primitives from Foundational.
- **US3**: Depends on the published route set from US1 and US2 to complete the full cross-page CTA/legal contract.
### Within Each User Story
- Write the browser smoke test first and verify it fails.
- Create content modules and story-specific primitives before assembling the route files.
- Finish route implementation before cross-page or SEO wiring tasks.
---
## Parallel Opportunities
- `T002` and `T003` can run in parallel once `T001` is scoped.
- `T005`, `T006`, `T008`, `T009`, and `T010` can run in parallel after `T004` begins.
- In US1, `T013`, `T014`, and `T015` can run in parallel before `T016`.
- In US2, `T018`, `T019`, `T020`, `T021`, and `T022` can be split across contributors once shared story inputs are clear.
- In US3, `T024`, `T025`, `T026`, `T027`, and `T028` can run in parallel before `T029` and `T030`.
- `T031` and `T032` can run in parallel during final polish.
---
## Parallel Example: User Story 2
```bash
# Launch the audience/trust/integration building blocks together:
Task: "T018 [US2] Create Solutions, Security & Trust, and Integrations content modules"
Task: "T019 [US2] Create audience and trust content primitives"
# Then split the route assembly work:
Task: "T020 [US2] Implement the Solutions route"
Task: "T021 [US2] Implement the Security & Trust route"
Task: "T022 [US2] Implement the Integrations route"
```
---
## Implementation Strategy
### MVP First
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Run the website build and the US1 smoke test.
5. Demo the MVP with Home + Product.
### Incremental Delivery
1. Setup + Foundational establish the reusable website system.
2. US1 ships the first clear product explanation.
3. US2 expands into audience fit, trust, and integration context.
4. US3 completes the conversion/legal path and SEO/discovery scaffolding.
5. Polish validates wording, scripts, and proof commands before merge.
### Suggested MVP Scope
- Deliver through **User Story 1** if a smaller first release is needed.
- Add **User Story 2** next for evaluation depth.
- Finish with **User Story 3** for full contact/legal/SEO readiness.