feat: public website launch readiness updates #394
@ -944,6 +944,7 @@ ## Active Technologies
|
||||
- Tailwind CSS v4
|
||||
- TypeScript 5.9, Astro 6 static components, HTML, CSS + Astro 6.0.0, Tailwind CSS 4.2.2 through CSS-first `@theme` and `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, Playwright 1.59.1 (400-tenantial-homepage-visual-rebuild)
|
||||
- TypeScript 5.9.3, Astro 6.0.0, Tailwind CSS v4.2.2 via `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, and Playwright smoke tests for the static website; no database, CMS, API, customer data, tenant data, or runtime persistence. (401-tenantial-platform-page)
|
||||
- TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwright; static website content only, no database or product persistence. (feat/403-public-website-launch-readiness)
|
||||
|
||||
## Recent Changes
|
||||
- 066-rbac-ui-enforcement-helper-v2-session-1769732329: Planned UiEnforcement v2 (spec + plan + design artifacts)
|
||||
|
||||
@ -11,6 +11,11 @@ const redirectOnlyPaths = new Set([
|
||||
'/services/',
|
||||
'/blog/',
|
||||
'/insights/',
|
||||
'/en/product/',
|
||||
'/en/products/',
|
||||
'/en/services/',
|
||||
'/en/blog/',
|
||||
'/en/insights/',
|
||||
]);
|
||||
|
||||
const isRedirectOnlySitemapPath = page => {
|
||||
@ -30,23 +35,14 @@ export default defineConfig({
|
||||
image: {
|
||||
domains: ['images.unsplash.com'],
|
||||
},
|
||||
// i18n: {
|
||||
// defaultLocale: "en",
|
||||
// locales: ["en", "fr"],
|
||||
// fallback: {
|
||||
// fr: "en",
|
||||
// },
|
||||
// routing: {
|
||||
// prefixDefaultLocale: false,
|
||||
// },
|
||||
// },
|
||||
prefetch: true,
|
||||
integrations: [
|
||||
sitemap({
|
||||
filter: page => !isRedirectOnlySitemapPath(page),
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'de',
|
||||
locales: {
|
||||
de: 'de',
|
||||
en: 'en',
|
||||
},
|
||||
},
|
||||
@ -60,6 +56,10 @@ export default defineConfig({
|
||||
// If both an Astro and Starlight i18n configurations are provided, an error is thrown.
|
||||
locales: {
|
||||
root: {
|
||||
label: 'Deutsch',
|
||||
lang: 'de',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
@ -67,20 +67,20 @@ export default defineConfig({
|
||||
// https://starlight.astro.build/guides/sidebar/
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Evaluation Guides',
|
||||
label: 'Evaluierungsleitfaeden',
|
||||
translations: {
|
||||
de: 'Schnellstartanleitungen',
|
||||
es: 'Guías de Inicio Rápido',
|
||||
fa: 'راهنمای شروع سریع',
|
||||
fr: 'Guides de Démarrage Rapide',
|
||||
ja: 'クイックスタートガイド',
|
||||
'zh-cn': '快速入门指南',
|
||||
en: 'Evaluation Guides',
|
||||
},
|
||||
items: [{ autogenerate: { directory: 'guides' } }],
|
||||
},
|
||||
{
|
||||
label: 'Platform Notes',
|
||||
items: [{ label: 'Evidence Review', link: 'platform/evidence-review/' }],
|
||||
label: 'Plattform-Notizen',
|
||||
translations: {
|
||||
en: 'Platform Notes',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Evidence Review', link: 'platform/evidence-review/' },
|
||||
],
|
||||
},
|
||||
],
|
||||
social: [],
|
||||
@ -94,22 +94,6 @@ export default defineConfig({
|
||||
'./src/components/ui/starlight/MobileMenuFooter.astro',
|
||||
ThemeSelect: './src/components/ui/starlight/ThemeSelect.astro',
|
||||
},
|
||||
head: [
|
||||
{
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
property: 'og:image',
|
||||
content: 'https://tenantial.com' + '/social.webp',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
property: 'twitter:image',
|
||||
content: 'https://tenantial.com' + '/social.webp',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
mdx(),
|
||||
],
|
||||
|
||||
@ -1,13 +1,54 @@
|
||||
---
|
||||
import logoLockupMask from '@images/tenantial-logo-lockup-mask.png';
|
||||
|
||||
const {
|
||||
class: className,
|
||||
style: inlineStyle,
|
||||
'aria-label': ariaLabel = 'Tenantial',
|
||||
...attrs
|
||||
} = Astro.props;
|
||||
|
||||
const logoMaskUrl =
|
||||
typeof logoLockupMask === 'string' ? logoLockupMask : logoLockupMask.src;
|
||||
const logoStyle = `--tenantial-logo-mask: url("${logoMaskUrl}");${inlineStyle ?? ''}`;
|
||||
---
|
||||
|
||||
<span
|
||||
{...Astro.props}
|
||||
{...attrs}
|
||||
class:list={[
|
||||
'inline-flex items-center gap-2 text-neutral-800 dark:text-neutral-100',
|
||||
Astro.props.class,
|
||||
'tenantial-wordmark inline-block shrink-0 align-middle text-neutral-700 transition-colors duration-300 dark:text-white',
|
||||
className,
|
||||
]}
|
||||
style={logoStyle}
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<span
|
||||
class="inline-grid size-9 place-items-center rounded-xl bg-neutral-800 text-sm font-black text-yellow-400 dark:bg-yellow-400 dark:text-neutral-900"
|
||||
>T</span
|
||||
>
|
||||
<span class="text-xl font-bold tracking-tight">Tenantial</span>
|
||||
<span class="tenantial-wordmark__shape" aria-hidden="true"></span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.tenantial-wordmark {
|
||||
color: #4b4b4f;
|
||||
}
|
||||
|
||||
:global(.dark) .tenantial-wordmark,
|
||||
:global([data-theme='dark']) .tenantial-wordmark {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tenantial-wordmark__shape {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 898 / 167;
|
||||
background: currentColor;
|
||||
mask-image: var(--tenantial-logo-mask);
|
||||
mask-mode: alpha;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-image: var(--tenantial-logo-mask);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,6 +3,14 @@ import { getImage } from 'astro:assets';
|
||||
import { OG, SEO, SITE } from '@data/constants';
|
||||
import faviconSvgSrc from '@images/icon.svg';
|
||||
import faviconSrc from '@images/icon.png';
|
||||
import {
|
||||
getLocaleFromPath,
|
||||
isLocale,
|
||||
localeOg,
|
||||
localizedPath,
|
||||
stripLocalePrefix,
|
||||
type Locale,
|
||||
} from '@/i18n';
|
||||
|
||||
// Default properties for the Meta component. These values are used if props are not provided.
|
||||
// 'meta' sets a default description meta tag to describe the page content.
|
||||
@ -22,8 +30,11 @@ const {
|
||||
structuredData = defaultProps.structuredData,
|
||||
customDescription = defaultProps.customDescription,
|
||||
customOgTitle = defaultProps.customOgTitle,
|
||||
locale: rawLocale = getLocaleFromPath(Astro.url.pathname),
|
||||
} = Astro.props;
|
||||
|
||||
const locale: Locale = isLocale(rawLocale) ? rawLocale : 'de';
|
||||
|
||||
// Use custom description if provided, otherwise use default meta
|
||||
const description = customDescription || meta;
|
||||
// Use custom OG title if provided, otherwise use default OG title
|
||||
@ -33,32 +44,22 @@ const ogDescription = customDescription || OG.description;
|
||||
// Define the metadata for your website and individual pages
|
||||
const siteURL = `${Astro.site}`; // Set the website URL in astro.config.mjs
|
||||
const author = SITE.author;
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site || Astro.url.origin)
|
||||
.href;
|
||||
const basePath = Astro.url.pathname;
|
||||
const cleanPath = stripLocalePrefix(Astro.url.pathname);
|
||||
const canonical = new URL(
|
||||
localizedPath(cleanPath, locale),
|
||||
Astro.site || Astro.url.origin
|
||||
).href;
|
||||
const socialImageRes = await getImage({
|
||||
src: OG.image,
|
||||
width: 1200,
|
||||
height: 600,
|
||||
format: 'png',
|
||||
});
|
||||
const socialImage = Astro.url.origin + socialImageRes.src; // Get the full URL of the image (https://stackoverflow.com/a/9858694)
|
||||
const socialImage = new URL(socialImageRes.src, Astro.site || Astro.url.origin)
|
||||
.href;
|
||||
const twitterDomain = new URL(siteURL).hostname;
|
||||
|
||||
const languages: { [key: string]: string } = {
|
||||
en: '',
|
||||
};
|
||||
|
||||
function createHref(lang: string, prefix: string, path: string): string {
|
||||
// Remove any existing language prefix
|
||||
const cleanPath = path.replace(/^\/(en)\//, '/');
|
||||
|
||||
// Add the correct language prefix if needed
|
||||
const basePath = prefix ? `/${prefix}${cleanPath}` : cleanPath;
|
||||
const normalizedBasePath = basePath.replace(/\/\/+/g, '/');
|
||||
|
||||
return `${siteURL.slice(0, -1)}${normalizedBasePath}`;
|
||||
}
|
||||
|
||||
const fullPath: string = Astro.url.pathname;
|
||||
const alternateLocales: Locale[] = ['de', 'en'];
|
||||
|
||||
// Generate and optimize the favicon images
|
||||
const faviconSvg = await getImage({
|
||||
@ -93,19 +94,24 @@ const appleTouchIcon = await getImage({
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
{
|
||||
Object.entries(languages).map(([lang, prefix]) => {
|
||||
const cleanPath = fullPath.replace(/^\/(en)\//, '/');
|
||||
const href = createHref(lang, prefix, cleanPath);
|
||||
alternateLocales.map(lang => {
|
||||
const href = new URL(
|
||||
localizedPath(cleanPath, lang),
|
||||
Astro.site || Astro.url.origin
|
||||
).href;
|
||||
return <link rel="alternate" hreflang={lang} href={href} />;
|
||||
})
|
||||
}
|
||||
<link
|
||||
rel="alternate"
|
||||
hreflang="x-default"
|
||||
href={new URL(localizedPath(cleanPath, 'de'), Astro.site || Astro.url.origin)
|
||||
.href}
|
||||
/>
|
||||
|
||||
{/* Facebook Meta Tags */}
|
||||
<meta
|
||||
property="og:locale"
|
||||
content="en_US"
|
||||
/>
|
||||
<meta property="og:url" content={siteURL} />
|
||||
<meta property="og:locale" content={localeOg[locale]} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:site_name" content={SITE.title} />
|
||||
@ -117,8 +123,8 @@ const appleTouchIcon = await getImage({
|
||||
|
||||
{/* Twitter Meta Tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content={siteURL} />
|
||||
<meta property="twitter:url" content={siteURL} />
|
||||
<meta property="twitter:domain" content={twitterDomain} />
|
||||
<meta property="twitter:url" content={canonical} />
|
||||
<meta name="twitter:title" content={ogTitle} />
|
||||
<meta name="twitter:description" content={ogDescription} />
|
||||
<meta name="twitter:image" content={socialImage} />
|
||||
|
||||
41
apps/website/src/components/pages/ContactPage.astro
Normal file
41
apps/website/src/components/pages/ContactPage.astro
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import ContactSection from '@components/sections/misc/ContactSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
import { localeHtmlLang, localizedPath, type Locale } from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].contact;
|
||||
const siteDescription = siteCopy[locale].site.description;
|
||||
const canonicalPath = localizedPath('/contact', locale);
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': `${SITE.url}${canonicalPath}`,
|
||||
url: `${SITE.url}${canonicalPath}`,
|
||||
name: copy.pageTitle,
|
||||
description: copy.metaDescription,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
url: SITE.url,
|
||||
name: SITE.title,
|
||||
description: siteDescription,
|
||||
},
|
||||
inLanguage: localeHtmlLang[locale],
|
||||
}}
|
||||
>
|
||||
<ContactSection locale={locale} />
|
||||
</MainLayout>
|
||||
65
apps/website/src/components/pages/HomePage.astro
Normal file
65
apps/website/src/components/pages/HomePage.astro
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import HeroSection from '@components/sections/landing/HeroSection.astro';
|
||||
import FeaturesGeneral from '@components/sections/features/FeaturesGeneral.astro';
|
||||
import FeaturesNavs from '@components/sections/features/FeaturesNavs.astro';
|
||||
import PricingSection from '@components/sections/pricing/PricingSection.astro';
|
||||
import FAQ from '@components/sections/misc/FAQ.astro';
|
||||
import heroImage from '@images/tenantial-dashboard.avif';
|
||||
import featureImage from '@images/tenantial-review-board.avif';
|
||||
import reviewImage from '@images/tenantial-evidence-intake.avif';
|
||||
import evidenceImage from '@images/tenantial-decision-review.avif';
|
||||
import governanceImage from '@images/tenantial-restore-plan.avif';
|
||||
import {
|
||||
faqsByLocale,
|
||||
featuresByLocale,
|
||||
pricingByLocale,
|
||||
siteCopy,
|
||||
} from '@data/site-copy';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].home;
|
||||
const tabs = copy.tabs.map((tab: any, index: number) => ({
|
||||
...tab,
|
||||
src: [reviewImage, evidenceImage, governanceImage][index],
|
||||
}));
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
>
|
||||
<HeroSection
|
||||
title={copy.heroTitle}
|
||||
subTitle={copy.heroSubtitle}
|
||||
primaryBtn={copy.primaryCta}
|
||||
primaryBtnURL={localizeHref('/contact', locale)}
|
||||
secondaryBtn={copy.secondaryCta}
|
||||
secondaryBtnURL={localizeHref('/platform', locale)}
|
||||
withReview={false}
|
||||
src={heroImage}
|
||||
alt={copy.heroAlt}
|
||||
/>
|
||||
|
||||
<FeaturesGeneral
|
||||
title={copy.featureTitle}
|
||||
subTitle={copy.featureSubtitle}
|
||||
src={featureImage}
|
||||
alt={copy.featureAlt}
|
||||
features={featuresByLocale[locale]}
|
||||
/>
|
||||
|
||||
<FeaturesNavs title={copy.workflowTitle} tabs={tabs} />
|
||||
|
||||
<PricingSection pricing={pricingByLocale[locale]} locale={locale} />
|
||||
|
||||
<FAQ title={copy.faqTitle} faqs={faqsByLocale[locale]} />
|
||||
</MainLayout>
|
||||
35
apps/website/src/components/pages/LegalPage.astro
Normal file
35
apps/website/src/components/pages/LegalPage.astro
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].legal;
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
>
|
||||
<MainSection title={copy.heading} subTitle={copy.subtitle} />
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<PrimaryCTA title={copy.privacy} url={localizeHref('/privacy', locale)} />
|
||||
<SecondaryCTA title={copy.terms} url={localizeHref('/terms', locale)} />
|
||||
<SecondaryCTA
|
||||
title={copy.imprint}
|
||||
url={localizeHref('/imprint', locale)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</MainLayout>
|
||||
98
apps/website/src/components/pages/PlatformPage.astro
Normal file
98
apps/website/src/components/pages/PlatformPage.astro
Normal file
@ -0,0 +1,98 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import LeftSection from '@components/ui/blocks/LeftSection.astro';
|
||||
import RightSection from '@components/ui/blocks/RightSection.astro';
|
||||
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
|
||||
import dashboard from '@images/tenantial-dashboard.avif';
|
||||
import evidenceImage from '@images/tenantial-evidence-panel.avif';
|
||||
import reviewImage from '@images/tenantial-drift-workflow.avif';
|
||||
import restoreImage from '@images/tenantial-rollout-plan.avif';
|
||||
import { SITE } from '@data/constants';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
import {
|
||||
localeHtmlLang,
|
||||
localizeHref,
|
||||
localizedPath,
|
||||
type Locale,
|
||||
} from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].platform;
|
||||
const siteDescription = siteCopy[locale].site.description;
|
||||
const canonicalPath = localizedPath('/platform', locale);
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': `${SITE.url}${canonicalPath}`,
|
||||
url: `${SITE.url}${canonicalPath}`,
|
||||
name: copy.pageTitle,
|
||||
description: copy.metaDescription,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
url: SITE.url,
|
||||
name: SITE.title,
|
||||
description: siteDescription,
|
||||
},
|
||||
inLanguage: localeHtmlLang[locale],
|
||||
}}
|
||||
>
|
||||
<MainSection
|
||||
title={copy.heading}
|
||||
subTitle={copy.subtitle}
|
||||
btnExists={true}
|
||||
btnTitle={siteCopy[locale].auth.walkthrough}
|
||||
btnURL={localizeHref('/contact', locale)}
|
||||
/>
|
||||
|
||||
<RightSection
|
||||
title={copy.backupTitle}
|
||||
subTitle={copy.backupSubtitle}
|
||||
single={false}
|
||||
imgOne={dashboard}
|
||||
imgOneAlt={copy.dashboardAlt}
|
||||
imgTwo={evidenceImage}
|
||||
imgTwoAlt={copy.evidenceAlt}
|
||||
/>
|
||||
|
||||
<LeftSection
|
||||
title={copy.driftTitle}
|
||||
subTitle={copy.driftSubtitle}
|
||||
img={reviewImage}
|
||||
imgAlt={copy.driftAlt}
|
||||
btnExists={true}
|
||||
btnTitle={copy.trustCta}
|
||||
btnURL={localizeHref('/trust', locale)}
|
||||
/>
|
||||
|
||||
<RightSection
|
||||
title={copy.restoreTitle}
|
||||
subTitle={copy.restoreSubtitle}
|
||||
single={true}
|
||||
imgOne={restoreImage}
|
||||
imgOneAlt={copy.restoreAlt}
|
||||
btnExists={true}
|
||||
btnTitle={copy.rolloutCta}
|
||||
btnURL={localizeHref('/contact', locale)}
|
||||
/>
|
||||
|
||||
<FeaturesStats
|
||||
title={copy.boundaryTitle}
|
||||
subTitle={copy.boundarySubtitle}
|
||||
mainStatTitle={copy.mainStatTitle}
|
||||
mainStatSubTitle={copy.mainStatSubTitle}
|
||||
stats={copy.stats}
|
||||
/>
|
||||
</MainLayout>
|
||||
25
apps/website/src/components/pages/PricingPage.astro
Normal file
25
apps/website/src/components/pages/PricingPage.astro
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import PricingSection from '@components/sections/pricing/PricingSection.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import { pricingByLocale, siteCopy } from '@data/site-copy';
|
||||
import type { Locale } from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].pricingIntro;
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
>
|
||||
<MainSection title={copy.heading} subTitle={copy.subtitle} />
|
||||
<PricingSection pricing={pricingByLocale[locale]} locale={locale} />
|
||||
</MainLayout>
|
||||
24
apps/website/src/components/pages/TextPage.astro
Normal file
24
apps/website/src/components/pages/TextPage.astro
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
import type { Locale } from '@/i18n';
|
||||
|
||||
const { locale, page } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
page: 'privacy' | 'terms' | 'imprint';
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale][page];
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
>
|
||||
<MainSection title={copy.heading} subTitle={copy.subtitle} />
|
||||
</MainLayout>
|
||||
37
apps/website/src/components/pages/TrustPage.astro
Normal file
37
apps/website/src/components/pages/TrustPage.astro
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].trust;
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
lang={locale}
|
||||
title={copy.pageTitle}
|
||||
customDescription={copy.metaDescription}
|
||||
customOgTitle={copy.pageTitle}
|
||||
>
|
||||
<MainSection
|
||||
title={copy.heading}
|
||||
subTitle={copy.subtitle}
|
||||
btnExists={true}
|
||||
btnTitle={copy.cta}
|
||||
btnURL={localizeHref('/contact', locale)}
|
||||
/>
|
||||
<FeaturesStats
|
||||
title={copy.statsTitle}
|
||||
subTitle={copy.statsSubtitle}
|
||||
mainStatTitle={copy.mainStatTitle}
|
||||
mainStatSubTitle={copy.mainStatSubTitle}
|
||||
stats={copy.stats}
|
||||
/>
|
||||
</MainLayout>
|
||||
@ -14,6 +14,10 @@ interface Props {
|
||||
subTitle?: string;
|
||||
partners: Partner[];
|
||||
}
|
||||
|
||||
const visiblePartners = partners.filter(
|
||||
partner => partner.href && partner.icon
|
||||
);
|
||||
---
|
||||
|
||||
<section
|
||||
@ -39,8 +43,13 @@ interface Props {
|
||||
>
|
||||
{/* Clients Group SVGs */}
|
||||
{
|
||||
partners.map(partner => (
|
||||
<a href={partner.href} target="_blank" rel="noopener noreferrer">
|
||||
visiblePartners.map(partner => (
|
||||
<a
|
||||
href={partner.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={partner.name}
|
||||
>
|
||||
<div set:html={partner.icon} />
|
||||
</a>
|
||||
))
|
||||
|
||||
@ -15,7 +15,6 @@ const {
|
||||
secondaryBtnURL,
|
||||
withReview,
|
||||
avatars,
|
||||
starCount,
|
||||
rating,
|
||||
reviews,
|
||||
src,
|
||||
@ -32,7 +31,6 @@ interface Props {
|
||||
secondaryBtnURL?: string;
|
||||
withReview?: boolean;
|
||||
avatars?: Array<string>;
|
||||
starCount?: number;
|
||||
rating?: string;
|
||||
reviews?: string;
|
||||
src?: any;
|
||||
@ -81,12 +79,7 @@ interface Props {
|
||||
}
|
||||
{
|
||||
withReview ? (
|
||||
<ReviewComponent
|
||||
avatars={avatars}
|
||||
starCount={starCount}
|
||||
rating={rating}
|
||||
reviews={reviews}
|
||||
/>
|
||||
<ReviewComponent avatars={avatars} rating={rating} reviews={reviews} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
|
||||
@ -1,5 +1,36 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
|
||||
const { locale = 'de' } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const userSVG = `<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>`;
|
||||
|
||||
const copy = siteCopy[locale].auth;
|
||||
---
|
||||
|
||||
<PrimaryCTA title="Request walkthrough" url="/contact" />
|
||||
<a
|
||||
href={localizeHref('/contact', locale)}
|
||||
data-astro-prefetch
|
||||
class="flex items-center gap-x-2 text-base font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-orange-400 focus-visible:ring-3 md:my-6 md:border-s md:border-neutral-300 md:ps-6 md:text-sm 2xl:text-base dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-hidden"
|
||||
>
|
||||
<Fragment set:html={userSVG} />
|
||||
{copy.walkthrough}
|
||||
</a>
|
||||
|
||||
@ -7,14 +7,16 @@ import EmailContactInput from '@components/ui/forms/input/EmailContactInput.astr
|
||||
import PhoneInput from '@components/ui/forms/input/PhoneInput.astro';
|
||||
import TextAreaInput from '@components/ui/forms/input/TextAreaInput.astro';
|
||||
import Icon from '@components/ui/icons/Icon.astro';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
|
||||
// Define the variables that will be used in this component
|
||||
const title: string = 'Contact Tenantial';
|
||||
const subTitle: string =
|
||||
'Request a walkthrough or start a scoped rollout conversation. Do not send secrets, credentials, or tenant exports through this public website.';
|
||||
const formTitle: string = 'Prepare a walkthrough request';
|
||||
const formSubTitle: string =
|
||||
'The public website form is static; use email for business contact context.';
|
||||
const { locale = 'de' } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const copy = siteCopy[locale].contact;
|
||||
---
|
||||
|
||||
{/* Contact Us */}
|
||||
@ -24,10 +26,10 @@ const formSubTitle: string =
|
||||
<h1
|
||||
class="text-2xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
{copy.title}
|
||||
</h1>
|
||||
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
|
||||
{subTitle}
|
||||
{copy.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -36,7 +38,7 @@ const formSubTitle: string =
|
||||
<h2
|
||||
class="mb-8 text-xl font-bold text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
{formTitle}
|
||||
{copy.formTitle}
|
||||
</h2>
|
||||
{
|
||||
/* Form for user input with various input fields.-->
|
||||
@ -47,31 +49,31 @@ const formSubTitle: string =
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<TextInput
|
||||
id="hs-firstname-contacts"
|
||||
label="First Name"
|
||||
label={copy.firstName}
|
||||
name="hs-firstname-contacts"
|
||||
/>
|
||||
<TextInput
|
||||
id="hs-lastname-contacts"
|
||||
label="Last Name"
|
||||
label={copy.lastName}
|
||||
name="hs-lastname-contacts"
|
||||
/>
|
||||
</div>
|
||||
<EmailContactInput id="hs-email-contacts" />
|
||||
<PhoneInput id="hs-phone-number" />
|
||||
<EmailContactInput id="hs-email-contacts" label={copy.email} />
|
||||
<PhoneInput id="hs-phone-number" label={copy.phone} />
|
||||
<TextAreaInput
|
||||
id="hs-about-contacts"
|
||||
label="Details"
|
||||
label={copy.details}
|
||||
name="hs-about-contacts"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid">
|
||||
<AuthBtn title="Prepare Email" />
|
||||
<AuthBtn title={copy.submit} />
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{formSubTitle}
|
||||
{copy.formSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@ -82,35 +84,35 @@ const formSubTitle: string =
|
||||
}
|
||||
<div class="divide-y divide-neutral-300 dark:divide-neutral-700">
|
||||
<ContactIconBlock
|
||||
heading="Knowledgebase"
|
||||
content="Read public notes about the Tenantial review model."
|
||||
heading={copy.blocks.docsHeading}
|
||||
content={copy.blocks.docsContent}
|
||||
isLinkVisible={true}
|
||||
linkTitle="Visit docs"
|
||||
linkURL="/welcome-to-docs/"
|
||||
linkTitle={copy.blocks.docsLink}
|
||||
linkURL={localizeHref('/welcome-to-docs/', locale)}
|
||||
isArrowVisible={true}
|
||||
><Icon name="question" />
|
||||
</ContactIconBlock>
|
||||
|
||||
<ContactIconBlock
|
||||
heading="FAQ"
|
||||
content="Explore our FAQ for quick, clear answers to common queries."
|
||||
heading={copy.blocks.faqHeading}
|
||||
content={copy.blocks.faqContent}
|
||||
isLinkVisible={true}
|
||||
linkTitle="Visit FAQ"
|
||||
linkURL="/#faq"
|
||||
linkTitle={copy.blocks.faqLink}
|
||||
linkURL={localizeHref('/#faq', locale)}
|
||||
isArrowVisible={true}
|
||||
><Icon name="chatBubble" />
|
||||
</ContactIconBlock>
|
||||
|
||||
<ContactIconBlock
|
||||
heading="Product preview boundary"
|
||||
content="The website does not connect to live tenant data or start backend workflows."
|
||||
heading={copy.blocks.boundaryHeading}
|
||||
content={copy.blocks.boundaryContent}
|
||||
isAddressVisible={false}
|
||||
><Icon name="mapPin" />
|
||||
</ContactIconBlock>
|
||||
|
||||
<ContactIconBlock
|
||||
heading="Contact us by email"
|
||||
content="Prefer the written word? Drop us an email at"
|
||||
heading={copy.blocks.emailHeading}
|
||||
content={copy.blocks.emailContent}
|
||||
isLinkVisible={true}
|
||||
linkTitle="hello@tenantial.com"
|
||||
linkURL="mailto:hello@tenantial.com"
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
---
|
||||
// Import the necessary dependencies
|
||||
import EmailFooterInput from '@components/ui/forms/input/EmailFooterInput.astro';
|
||||
import enStrings from '@utils/navigation.ts';
|
||||
import BrandLogo from '@components/BrandLogo.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import { getLocaleFromPath, localizeHref } from '@/i18n';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
|
||||
const strings = enStrings;
|
||||
const locale = getLocaleFromPath(Astro.url.pathname);
|
||||
const copy = siteCopy[locale].footer;
|
||||
|
||||
// Define the variables that will be used in this component
|
||||
const sectionThreeTitle: string =
|
||||
'Start a scoped conversation';
|
||||
const sectionThreeContent: string =
|
||||
'Use email for business contact context only. Do not send secrets, credentials, or tenant exports through the public website.';
|
||||
type FooterSection = {
|
||||
section: string;
|
||||
links: Array<{ name: string; url: string }>;
|
||||
};
|
||||
---
|
||||
|
||||
<footer class="w-full bg-neutral-300 dark:bg-neutral-900">
|
||||
@ -21,21 +22,23 @@ const sectionThreeContent: string =
|
||||
<div class="grid grid-cols-2 gap-6 md:grid-cols-4 lg:grid-cols-5">
|
||||
<div class="col-span-full lg:col-span-1">
|
||||
{/* Brand Logo */}
|
||||
<BrandLogo class="h-auto w-32" />
|
||||
<BrandLogo class="h-auto w-40" />
|
||||
</div>
|
||||
{/* An array of links for Product and Company sections */}
|
||||
{
|
||||
strings.footerLinks.map(section => (
|
||||
<div class="col-span-1">
|
||||
<h3 class="font-bold text-neutral-800 dark:text-neutral-200">
|
||||
copy.sections.map((section: FooterSection) => (
|
||||
<div class="col-span-1 min-w-0">
|
||||
<h3
|
||||
class="break-words font-bold text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
{section.section}
|
||||
</h3>
|
||||
<ul class="mt-3 grid space-y-3">
|
||||
{section.links.map((link, index) => (
|
||||
{section.links.map(link => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex gap-x-2 rounded-lg text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-neutral-500 focus-visible:ring-3 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-300 dark:focus:outline-hidden"
|
||||
href={localizeHref(link.url, locale)}
|
||||
class="inline-flex max-w-full gap-x-2 rounded-lg break-words whitespace-normal text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-neutral-500 focus-visible:ring-3 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-300 dark:focus:outline-hidden"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
@ -46,17 +49,20 @@ const sectionThreeContent: string =
|
||||
))
|
||||
}
|
||||
|
||||
<div class="col-span-2">
|
||||
<h3 class="font-bold text-neutral-800 dark:text-neutral-200">
|
||||
{sectionThreeTitle}
|
||||
<div class="col-span-2 min-w-0">
|
||||
<h3 class="break-words font-bold text-neutral-800 dark:text-neutral-200">
|
||||
{copy.conversationTitle}
|
||||
</h3>
|
||||
|
||||
<form>
|
||||
<EmailFooterInput />
|
||||
<p class="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{sectionThreeContent}
|
||||
<div>
|
||||
<EmailFooterInput
|
||||
title={copy.contactButton}
|
||||
url={localizeHref('/contact', locale)}
|
||||
/>
|
||||
<p class="mt-3 break-words text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{copy.conversationContent}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -64,9 +70,9 @@ const sectionThreeContent: string =
|
||||
class="mt-9 grid gap-y-2 sm:mt-12 sm:flex sm:items-center sm:justify-between sm:gap-y-0"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<p class="break-words text-sm text-neutral-600 dark:text-neutral-400">
|
||||
© <span id="current-year"></span>
|
||||
{SITE.title}. Public website content only.
|
||||
{SITE.title}. {copy.copyrightSuffix}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,11 +3,18 @@
|
||||
import ThemeIcon from '@components/ThemeIcon.astro';
|
||||
import NavLink from '@components/ui/links/NavLink.astro';
|
||||
import Authentication from '../misc/Authentication.astro';
|
||||
import enStrings from '@utils/navigation.ts';
|
||||
import BrandLogo from '@components/BrandLogo.astro';
|
||||
import LanguagePicker from '@components/ui/LanguagePicker.astro';
|
||||
import { getLocaleFromPath, localizeHref } from '@/i18n';
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
|
||||
const strings = enStrings;
|
||||
const homeUrl = '/';
|
||||
const locale = getLocaleFromPath(Astro.url.pathname);
|
||||
const strings = siteCopy[locale];
|
||||
const homeUrl = localizeHref('/', locale);
|
||||
type NavItem = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
---
|
||||
|
||||
{/* Main header component */}
|
||||
@ -26,7 +33,7 @@ const homeUrl = '/';
|
||||
href={homeUrl}
|
||||
aria-label="Brand"
|
||||
>
|
||||
<BrandLogo class="h-auto w-24" />
|
||||
<BrandLogo class="h-auto w-36 sm:w-40" />
|
||||
</a>
|
||||
{/* Collapse toggle for smaller screens */}
|
||||
<div class="mr-5 ml-auto md:hidden">
|
||||
@ -84,12 +91,13 @@ const homeUrl = '/';
|
||||
>
|
||||
{/* Navigation links and Authentication component */}
|
||||
{
|
||||
strings.navBarLinks.map(link => (
|
||||
<NavLink url={link.url} name={link.name} />
|
||||
strings.nav.map((link: NavItem) => (
|
||||
<NavLink url={localizeHref(link.url, locale)} name={link.name} />
|
||||
))
|
||||
}
|
||||
|
||||
<Authentication />
|
||||
<Authentication locale={locale} />
|
||||
<LanguagePicker />
|
||||
{/* ThemeIcon component specifically for larger screens */}
|
||||
<span class="hidden md:inline-block">
|
||||
<ThemeIcon />
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
// Import SecondaryCTA component for use in this module
|
||||
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
||||
import Icon from '@components/ui/icons/Icon.astro';
|
||||
import { localizeHref, type Locale } from '@/i18n';
|
||||
|
||||
// Define props from Astro
|
||||
const { pricing } = Astro.props;
|
||||
const { pricing, locale = 'de' } = Astro.props;
|
||||
|
||||
// Define TypeScript type for products.
|
||||
type Product = {
|
||||
@ -18,18 +19,19 @@ type Product = {
|
||||
purchaseLink: string;
|
||||
};
|
||||
|
||||
interface PricingSectionProps {
|
||||
type Pricing = {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
badge: string;
|
||||
thirdOption: string;
|
||||
btnText: string;
|
||||
pricing: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
starterKit: Product;
|
||||
professionalToolbox: Product;
|
||||
};
|
||||
starterKit: Product;
|
||||
professionalToolbox: Product;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
pricing: Pricing;
|
||||
locale?: Locale;
|
||||
}
|
||||
---
|
||||
|
||||
@ -87,7 +89,7 @@ interface PricingSectionProps {
|
||||
</ul>
|
||||
{/* CTA for purchasing the product */}
|
||||
<a
|
||||
href={pricing.starterKit.purchaseLink}
|
||||
href={localizeHref(pricing.starterKit.purchaseLink, locale)}
|
||||
class="block rounded-lg bg-gray-500 px-8 py-3 text-center text-sm font-bold text-gray-100 ring-indigo-300 outline-hidden transition duration-100 hover:bg-gray-600 focus-visible:ring-3 active:text-gray-300 md:text-base"
|
||||
>{pricing.starterKit.purchaseBtnTitle}</a
|
||||
>
|
||||
@ -141,7 +143,7 @@ interface PricingSectionProps {
|
||||
</ul>
|
||||
{/* CTA for purchasing the product */}
|
||||
<a
|
||||
href={pricing.professionalToolbox.purchaseLink}
|
||||
href={localizeHref(pricing.professionalToolbox.purchaseLink, locale)}
|
||||
class="bg-opacity-50 block rounded-lg bg-orange-200/40 px-8 py-3 text-center text-sm font-bold text-neutral-100 ring-orange-300 outline-hidden transition duration-300 hover:bg-orange-300 focus-visible:ring-3 active:bg-orange-400 md:text-base"
|
||||
>{pricing.professionalToolbox.purchaseBtnTitle}</a
|
||||
>
|
||||
@ -153,6 +155,9 @@ interface PricingSectionProps {
|
||||
{pricing.thirdOption}
|
||||
</p>
|
||||
|
||||
<SecondaryCTA title={pricing.btnText} url="/contact" />
|
||||
<SecondaryCTA
|
||||
title={pricing.btnText}
|
||||
url={localizeHref('/contact', locale)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -27,7 +27,7 @@ interface Props {
|
||||
<Image
|
||||
class="h-8 w-8 rounded-full"
|
||||
src={avatarSrc}
|
||||
alt="Avatar Description"
|
||||
alt="Illustrative testimonial avatar"
|
||||
loading={'eager'}
|
||||
inferSize
|
||||
/>
|
||||
|
||||
58
apps/website/src/components/ui/LanguagePicker.astro
Normal file
58
apps/website/src/components/ui/LanguagePicker.astro
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
import {
|
||||
getLocaleFromPath,
|
||||
localeLabels,
|
||||
localizedPath,
|
||||
stripLocalePrefix,
|
||||
} from '@/i18n';
|
||||
import Icon from './icons/Icon.astro';
|
||||
|
||||
const currentLocale = getLocaleFromPath(Astro.url.pathname);
|
||||
const currentPath = stripLocalePrefix(Astro.url.pathname);
|
||||
---
|
||||
|
||||
<div class="hs-dropdown relative inline-flex">
|
||||
<button
|
||||
id="hs-language-dropdown"
|
||||
type="button"
|
||||
aria-label="Sprache wechseln"
|
||||
title="Sprache wechseln"
|
||||
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-1.5 py-1.5 text-sm font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-200 hover:text-orange-400 dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-hidden"
|
||||
>
|
||||
<Icon name="earth" />
|
||||
<span class="sr-only">{localeLabels[currentLocale]}</span>
|
||||
<svg
|
||||
class="hs-dropdown-open:rotate-180 size-4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 top-[98%]! left-[20%]! mt-2 hidden transform-none! rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] before:absolute before:start-0 before:-top-4 before:h-4 before:w-full after:absolute after:start-0 after:-bottom-4 after:h-4 after:w-full md:top-[80%]! md:left-[90%]! dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
|
||||
aria-labelledby="hs-language-dropdown"
|
||||
>
|
||||
{
|
||||
Object.entries(localeLabels).map(([locale, label]) => (
|
||||
<a
|
||||
class:list={[
|
||||
'flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-hidden dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700',
|
||||
locale === currentLocale
|
||||
? 'font-semibold text-orange-500 dark:text-orange-300'
|
||||
: 'text-neutral-800 dark:text-neutral-400',
|
||||
]}
|
||||
href={localizedPath(currentPath, locale as 'de' | 'en')}
|
||||
aria-current={locale === currentLocale ? 'true' : undefined}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -18,16 +18,16 @@ interface Props {
|
||||
<section
|
||||
class="mx-auto mt-10 max-w-[85rem] px-4 py-10 sm:px-6 sm:py-16 lg:px-8 lg:py-14 2xl:max-w-full"
|
||||
>
|
||||
<div class="max-w-(--breakpoint-md)">
|
||||
<div class="min-w-0 max-w-(--breakpoint-md)">
|
||||
{/* Section title */}
|
||||
<h1
|
||||
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||
class="mb-4 text-4xl font-extrabold tracking-tight break-words text-balance text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{/* Section subtitle */}
|
||||
<p
|
||||
class="mb-8 max-w-prose font-normal text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400"
|
||||
class="mb-8 max-w-prose font-normal break-words text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400"
|
||||
>
|
||||
{subTitle}
|
||||
</p>
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
---
|
||||
import Avatar from '@components/ui/avatars/Avatar.astro';
|
||||
import FullStar from '@components/ui/stars/FullStar.astro';
|
||||
import HalfStar from '@components/ui/stars/HalfStar.astro';
|
||||
|
||||
const { avatars, starCount = 0, rating, reviews } = Astro.props;
|
||||
const { avatars, rating, reviews } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
avatars?: Array<string>;
|
||||
starCount?: number;
|
||||
rating?: string;
|
||||
reviews?: string;
|
||||
}
|
||||
@ -19,12 +16,16 @@ interface Props {
|
||||
<div class="shrink-0 pb-5 sm:flex sm:pe-5 sm:pb-0">
|
||||
{/* Avatar Group */}
|
||||
<div class="flex justify-center -space-x-3">
|
||||
{avatars?.map(src => <Avatar src={src} alt="Avatar Description" />)}
|
||||
{
|
||||
avatars?.map(src => (
|
||||
<Avatar src={src} alt="Illustrative reviewer avatar" />
|
||||
))
|
||||
}
|
||||
<span
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-zinc-800 ring-2 ring-white dark:bg-zinc-900 dark:ring-zinc-800"
|
||||
>
|
||||
<span class="text-xs leading-none font-medium text-white uppercase"
|
||||
>7k+</span
|
||||
>Demo</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
@ -33,26 +34,15 @@ interface Props {
|
||||
class="mx-auto h-px w-32 border-t border-neutral-400 sm:mx-0 sm:h-8 sm:w-auto sm:border-s sm:border-t-0 dark:border-neutral-500"
|
||||
>
|
||||
</div>
|
||||
{/* Review Ratings */}
|
||||
<div class="flex flex-col items-center sm:items-start">
|
||||
<div class="flex items-baseline space-x-1 pt-5 sm:ps-5 sm:pt-0">
|
||||
<div class="flex space-x-1">
|
||||
{/* Your star ratings */}
|
||||
{
|
||||
Array(starCount)
|
||||
.fill(0)
|
||||
.map((_, i) => <FullStar key={i} />)
|
||||
}
|
||||
{/* Adding additional half-star */}
|
||||
<HalfStar />
|
||||
</div>
|
||||
<p class="text-neutral-800 dark:text-neutral-200">
|
||||
<Fragment set:html={rating} />
|
||||
<Fragment set:html={rating || 'Illustrative review preview'} />
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-sm text-neutral-800 sm:ps-5 dark:text-neutral-200">
|
||||
<p>
|
||||
<Fragment set:html={reviews} />
|
||||
<Fragment set:html={reviews || 'Static example only'} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,6 +21,6 @@ interface Props {
|
||||
id={id}
|
||||
autocomplete="email"
|
||||
class="block w-full rounded-lg border border-neutral-200 bg-neutral-50 px-4 py-3 text-sm text-neutral-700 placeholder:text-neutral-500 focus:border-neutral-200 focus:ring-3 focus:ring-neutral-400 focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-700/30 dark:text-neutral-300 dark:placeholder:text-neutral-400 dark:focus:ring-1"
|
||||
placeholder="Email"
|
||||
placeholder={label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,36 +1,18 @@
|
||||
---
|
||||
const {
|
||||
label = 'Search',
|
||||
title = Astro.currentLocale === 'fr' ? "S'abonner" : 'Subscribe',
|
||||
id = 'footer-input',
|
||||
} = Astro.props;
|
||||
const { title = 'Kontaktseite oeffnen', url = '/contact' } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
title?: string;
|
||||
id?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const placeholder =
|
||||
Astro.currentLocale === 'fr' ? 'Entrez votre email' : 'Enter your email';
|
||||
---
|
||||
|
||||
<div
|
||||
class="mt-4 flex flex-col items-center gap-2 rounded-lg bg-neutral-200 p-2 sm:flex-row sm:gap-3 dark:bg-neutral-800"
|
||||
class="mt-4 flex flex-col items-start gap-2 rounded-lg bg-neutral-200 p-2 dark:bg-neutral-800"
|
||||
>
|
||||
<div class="w-full">
|
||||
<label for={id} class="sr-only">{label}</label>
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
name="footer-input"
|
||||
class="block w-full rounded-lg border-transparent bg-neutral-100 px-4 py-3 text-sm text-neutral-600 caret-orange-400 focus:border-orange-400 focus:ring-orange-400 disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:bg-neutral-700 dark:text-gray-300 dark:placeholder:text-neutral-300"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
class="inline-flex w-full items-center justify-center gap-x-2 rounded-lg border border-transparent bg-orange-400 p-3 text-sm font-bold whitespace-nowrap text-neutral-50 ring-zinc-500 outline-hidden transition duration-300 hover:bg-orange-500 focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 sm:w-auto dark:ring-zinc-200 dark:focus:ring-1 dark:focus:outline-hidden"
|
||||
href="/contact"
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
|
||||
@ -1,11 +1,33 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import { getImage } from 'astro:assets';
|
||||
import StarlightHead from '@astrojs/starlight/components/Head.astro';
|
||||
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
|
||||
import { OG } from '@data/constants';
|
||||
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site || Astro.url.origin)
|
||||
.href;
|
||||
const socialImageRes = await getImage({
|
||||
src: OG.image,
|
||||
width: 1200,
|
||||
height: 600,
|
||||
format: 'png',
|
||||
});
|
||||
const socialImage = new URL(socialImageRes.src, Astro.site || Astro.url.origin)
|
||||
.href;
|
||||
---
|
||||
|
||||
<VtbotStarlight {...Astro.props} viewTransitionsFallback="animate">
|
||||
<StarlightHead {...Astro.props}><slot /></StarlightHead>
|
||||
<StarlightHead {...Astro.props}>
|
||||
<slot />
|
||||
</StarlightHead>
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={socialImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="600" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="twitter:url" content={canonical} />
|
||||
<meta name="twitter:image" content={socialImage} />
|
||||
<script>
|
||||
import '@scripts/lenisSmoothScroll.js';
|
||||
</script>
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
---
|
||||
const docs = '/welcome-to-docs/';
|
||||
import { getLocaleFromPath, localizeHref } from '@/i18n';
|
||||
|
||||
const locale = getLocaleFromPath(Astro.url.pathname);
|
||||
const home = localizeHref('/', locale);
|
||||
const docs = localizeHref('/welcome-to-docs/', locale);
|
||||
---
|
||||
|
||||
<span class="site-title flex">
|
||||
<a class="main-logo" href="/" aria-label="Tenantial home">Tenantial</a>
|
||||
<a class="main-logo" href={home} aria-label="Tenantial home">Tenantial</a>
|
||||
<a class="docs-logo" href={docs} aria-label="Tenantial docs">Docs</a>
|
||||
</span>
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Evaluation Checklist
|
||||
description: A conservative checklist for early Tenantial evaluation.
|
||||
sidebar:
|
||||
label: Evaluation Checklist
|
||||
order: 3
|
||||
---
|
||||
|
||||
Use this checklist before a product walkthrough:
|
||||
|
||||
- Confirm which Microsoft tenant workflows are in scope.
|
||||
- Separate read-only review needs from any future write or restore needs.
|
||||
- Identify audit and approval expectations.
|
||||
- Plan staging validation before production rollout.
|
||||
- Keep private tenant data out of public website communication.
|
||||
18
apps/website/src/content/docs/en/guides/getting-started.mdx
Normal file
18
apps/website/src/content/docs/en/guides/getting-started.mdx
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: How evaluators should approach a Tenantial walkthrough.
|
||||
sidebar:
|
||||
label: Getting Started
|
||||
order: 2
|
||||
---
|
||||
|
||||
Start with a scoped conversation about the tenant governance problem you want to solve.
|
||||
|
||||
Useful preparation:
|
||||
|
||||
- Identify the Microsoft tenant administration workflows that need review.
|
||||
- List which policy families, backup flows, or restore planning scenarios matter first.
|
||||
- Decide who needs to participate in evidence review and approval.
|
||||
- Avoid sending private tenant exports or credentials through public website contact paths.
|
||||
|
||||
Tenantial walkthroughs should stay focused on review clarity, least-privilege rollout planning, and staging validation before production use.
|
||||
18
apps/website/src/content/docs/en/guides/intro.mdx
Normal file
18
apps/website/src/content/docs/en/guides/intro.mdx
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Introduction to Tenantial
|
||||
description: Public introduction to Tenantial's evidence-first Microsoft tenant governance model.
|
||||
sidebar:
|
||||
label: Introduction
|
||||
order: 1
|
||||
---
|
||||
|
||||
Tenantial is positioned around careful Microsoft tenant governance. Public materials focus on observed inventory, immutable policy snapshots, drift review, findings, exceptions, audit context, and defensive restore planning.
|
||||
|
||||
The website documentation is intentionally public and static. It does not expose live tenant data, operational evidence, private support material, credentials, or connected Microsoft accounts.
|
||||
|
||||
## What to review first
|
||||
|
||||
- How policy evidence is collected and normalized for review.
|
||||
- How snapshots support comparison and restore planning.
|
||||
- How findings and exceptions make decisions attributable.
|
||||
- How restore is treated as preview-first work.
|
||||
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Evidence Review
|
||||
description: Public platform note for Tenantial evidence review workflows.
|
||||
sidebar:
|
||||
label: Evidence Review
|
||||
order: 1
|
||||
---
|
||||
|
||||
Evidence review is the public framing for Tenantial's product direction.
|
||||
|
||||
The model is intentionally cautious:
|
||||
|
||||
- Inventory is treated as observed state.
|
||||
- Snapshots are explicit records for comparison and planning.
|
||||
- Drift and findings become review work.
|
||||
- Exceptions and audit notes keep decisions attributable.
|
||||
- Restore is planned through preview, validation, selective scope, and explicit confirmation.
|
||||
31
apps/website/src/content/docs/en/welcome-to-docs.mdx
Normal file
31
apps/website/src/content/docs/en/welcome-to-docs.mdx
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Tenantial Docs
|
||||
head:
|
||||
- tag: title
|
||||
content: Tenantial Docs
|
||||
description: Public notes for understanding Tenantial's evidence-first Microsoft tenant governance model.
|
||||
editUrl: false
|
||||
lastUpdated: false
|
||||
next: false
|
||||
hero:
|
||||
title: Tenantial documentation
|
||||
tagline: Public notes for evidence review, snapshot discipline, drift review, and cautious restore planning.
|
||||
actions:
|
||||
- text: Get started
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
link: /en/guides/getting-started/
|
||||
---
|
||||
|
||||
import '@styles/starlight_main.css';
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Evaluation Guides" icon="document">
|
||||
Understand the public product model before planning a rollout conversation.
|
||||
</Card>
|
||||
|
||||
<Card title="Platform Notes" icon="seti:terraform">
|
||||
Review how Tenantial frames inventory evidence, snapshots, findings, and restore planning.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
@ -1,15 +1,15 @@
|
||||
---
|
||||
title: Evaluation Checklist
|
||||
description: A conservative checklist for early Tenantial evaluation.
|
||||
title: Evaluierungscheckliste
|
||||
description: Eine konservative Checkliste für eine frühe Tenantial-Evaluierung.
|
||||
sidebar:
|
||||
label: Evaluation Checklist
|
||||
label: Evaluierungscheckliste
|
||||
order: 3
|
||||
---
|
||||
|
||||
Use this checklist before a product walkthrough:
|
||||
Nutze diese Checkliste vor einem Produkt-Walkthrough:
|
||||
|
||||
- Confirm which Microsoft tenant workflows are in scope.
|
||||
- Separate read-only review needs from any future write or restore needs.
|
||||
- Identify audit and approval expectations.
|
||||
- Plan staging validation before production rollout.
|
||||
- Keep private tenant data out of public website communication.
|
||||
- Bestätige, welche Microsoft-Tenant-Workflows im Scope sind.
|
||||
- Trenne Read-only-Review-Bedarf von künftigem Write- oder Restore-Bedarf.
|
||||
- Identifiziere Audit- und Approval-Erwartungen.
|
||||
- Plane Staging-Validierung vor einem Produktionsrollout.
|
||||
- Halte private Tenant-Daten aus öffentlicher Website-Kommunikation heraus.
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: How evaluators should approach a Tenantial walkthrough.
|
||||
description: Wie Evaluatoren einen Tenantial Walkthrough vorbereiten sollten.
|
||||
sidebar:
|
||||
label: Getting Started
|
||||
order: 2
|
||||
---
|
||||
|
||||
Start with a scoped conversation about the tenant governance problem you want to solve.
|
||||
Starte mit einem scoped Gespräch über das Tenant-Governance-Problem, das gelöst werden soll.
|
||||
|
||||
Useful preparation:
|
||||
Sinnvolle Vorbereitung:
|
||||
|
||||
- Identify the Microsoft tenant administration workflows that need review.
|
||||
- List which policy families, backup flows, or restore planning scenarios matter first.
|
||||
- Decide who needs to participate in evidence review and approval.
|
||||
- Avoid sending private tenant exports or credentials through public website contact paths.
|
||||
- Identifiziere die Microsoft-Tenant-Administrationsworkflows, die Review brauchen.
|
||||
- Liste die Policy-Familien, Backup-Flows oder Restore-Planungsszenarien, die zuerst relevant sind.
|
||||
- Entscheide, wer an Evidence Review und Approval teilnehmen muss.
|
||||
- Sende keine privaten Tenant-Exporte oder Credentials über öffentliche Website-Kontaktpfade.
|
||||
|
||||
Tenantial walkthroughs should stay focused on review clarity, least-privilege rollout planning, and staging validation before production use.
|
||||
Tenantial Walkthroughs sollten auf Review-Klarheit, Least-Privilege-Rollout-Planung und Staging-Validierung vor Produktionseinsatz fokussiert bleiben.
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
---
|
||||
title: Introduction to Tenantial
|
||||
description: Public introduction to Tenantial's evidence-first Microsoft tenant governance model.
|
||||
title: Einführung in Tenantial
|
||||
description: Öffentliche Einführung in Tenantials evidenzbasiertes Microsoft-Tenant-Governance-Modell.
|
||||
sidebar:
|
||||
label: Introduction
|
||||
label: Einführung
|
||||
order: 1
|
||||
---
|
||||
|
||||
Tenantial is positioned around careful Microsoft tenant governance. Public materials focus on observed inventory, immutable policy snapshots, drift review, findings, exceptions, audit context, and defensive restore planning.
|
||||
Tenantial ist auf vorsichtige Microsoft-Tenant-Governance ausgerichtet. Öffentliche Materialien fokussieren beobachtetes Inventory, unveränderliche Policy-Snapshots, Drift Review, Findings, Exceptions, Audit-Kontext und defensive Restore-Planung.
|
||||
|
||||
The website documentation is intentionally public and static. It does not expose live tenant data, operational evidence, private support material, credentials, or connected Microsoft accounts.
|
||||
Die Website-Dokumentation ist bewusst öffentlich und statisch. Sie legt keine Live-Tenant-Daten, operative Evidence, private Support-Materialien, Credentials oder verbundenen Microsoft-Konten offen.
|
||||
|
||||
## What to review first
|
||||
## Was zuerst geprüft werden sollte
|
||||
|
||||
- How policy evidence is collected and normalized for review.
|
||||
- How snapshots support comparison and restore planning.
|
||||
- How findings and exceptions make decisions attributable.
|
||||
- How restore is treated as preview-first work.
|
||||
- Wie Policy Evidence für Reviews gesammelt und normalisiert wird.
|
||||
- Wie Snapshots Vergleich und Restore-Planung unterstützen.
|
||||
- Wie Findings und Exceptions Entscheidungen zuordenbar machen.
|
||||
- Wie Restore als preview-first Arbeit behandelt wird.
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
---
|
||||
title: Evidence Review
|
||||
description: Public platform note for Tenantial evidence review workflows.
|
||||
description: Öffentliche Plattform-Notiz für Tenantial Evidence-Review-Workflows.
|
||||
sidebar:
|
||||
label: Evidence Review
|
||||
order: 1
|
||||
---
|
||||
|
||||
Evidence review is the public framing for Tenantial's product direction.
|
||||
Evidence Review ist die öffentliche Rahmung für Tenantials Produktrichtung.
|
||||
|
||||
The model is intentionally cautious:
|
||||
Das Modell ist bewusst vorsichtig:
|
||||
|
||||
- Inventory is treated as observed state.
|
||||
- Snapshots are explicit records for comparison and planning.
|
||||
- Drift and findings become review work.
|
||||
- Exceptions and audit notes keep decisions attributable.
|
||||
- Restore is planned through preview, validation, selective scope, and explicit confirmation.
|
||||
- Inventory wird als beobachteter Zustand behandelt.
|
||||
- Snapshots sind explizite Records für Vergleich und Planung.
|
||||
- Drift und Findings werden zu Review-Arbeit.
|
||||
- Exceptions und Audit-Notizen halten Entscheidungen zuordenbar.
|
||||
- Restore wird über Preview, Validierung, selektiven Scope und explizite Bestätigung geplant.
|
||||
|
||||
@ -3,28 +3,29 @@ title: Tenantial Docs
|
||||
head:
|
||||
- tag: title
|
||||
content: Tenantial Docs
|
||||
description: Public notes for understanding Tenantial's evidence-first Microsoft tenant governance model.
|
||||
description: Öffentliche Notizen zum evidenzbasierten Microsoft-Tenant-Governance-Modell von Tenantial.
|
||||
editUrl: false
|
||||
lastUpdated: false
|
||||
next: false
|
||||
hero:
|
||||
title: Tenantial documentation
|
||||
tagline: Public notes for evidence review, snapshot discipline, drift review, and cautious restore planning.
|
||||
title: Tenantial Dokumentation
|
||||
tagline: Öffentliche Notizen zu Evidence Review, Snapshot-Disziplin, Drift Review und vorsichtiger Restore-Planung.
|
||||
actions:
|
||||
- text: Get started
|
||||
- text: Starten
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
link: /guides/getting-started/
|
||||
---
|
||||
import "@styles/starlight_main.css";
|
||||
|
||||
import '@styles/starlight_main.css';
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Evaluation Guides" icon="document">
|
||||
Understand the public product model before planning a rollout conversation.
|
||||
<Card title="Evaluierungsleitfäden" icon="document">
|
||||
Verstehe das öffentliche Produktmodell, bevor ein Rollout-Gespräch geplant wird.
|
||||
</Card>
|
||||
|
||||
<Card title="Platform Notes" icon="seti:terraform">
|
||||
Review how Tenantial frames inventory evidence, snapshots, findings, and restore planning.
|
||||
<Card title="Plattform-Notizen" icon="seti:terraform">
|
||||
Prüfe, wie Tenantial Inventory Evidence, Snapshots, Findings und Restore-Planung rahmt.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
@ -2,11 +2,11 @@ import ogImageSrc from '@images/social.png';
|
||||
|
||||
export const SITE = {
|
||||
title: 'Tenantial',
|
||||
tagline: 'Evidence-first Microsoft tenant governance',
|
||||
tagline: 'Evidenzbasierte Microsoft-Tenant-Governance',
|
||||
description:
|
||||
'Tenantial helps Microsoft tenant administrators review policy evidence, snapshots, drift, findings, and restore plans before high-impact changes move forward.',
|
||||
'Tenantial hilft Microsoft-Tenant-Administratoren, Policy-Evidence, Snapshots, Drift, Findings und Restore-Pläne vor kritischen Änderungen zu prüfen.',
|
||||
description_short:
|
||||
'Evidence-first governance workflows for Microsoft tenant review, backup, drift, and restore planning.',
|
||||
'Evidenzbasierte Governance-Workflows für Microsoft-Tenant-Review, Backup, Drift und Restore-Planung.',
|
||||
url: 'https://tenantial.com',
|
||||
author: 'Tenantial',
|
||||
};
|
||||
@ -17,7 +17,7 @@ export const SEO = {
|
||||
structuredData: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
inLanguage: 'en-US',
|
||||
inLanguage: 'de',
|
||||
'@id': SITE.url,
|
||||
url: SITE.url,
|
||||
name: SITE.title,
|
||||
@ -32,12 +32,12 @@ export const SEO = {
|
||||
};
|
||||
|
||||
export const OG = {
|
||||
locale: 'en_US',
|
||||
locale: 'de_DE',
|
||||
type: 'website',
|
||||
url: SITE.url,
|
||||
title: `${SITE.title}: Evidence-first Microsoft tenant governance`,
|
||||
title: `${SITE.title}: Evidenzbasierte Microsoft-Tenant-Governance`,
|
||||
description:
|
||||
'Review tenant inventory, snapshots, drift, findings, exceptions, and restore plans with conservative, audit-friendly workflows.',
|
||||
'Prüfe Tenant Inventory, Snapshots, Drift, Findings, Exceptions und Restore-Pläne mit konservativen, auditfreundlichen Workflows.',
|
||||
image: ogImageSrc,
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{
|
||||
"subTitle": "Straight answers for public website visitors.",
|
||||
"faqs": [
|
||||
{
|
||||
"question": "What does Tenantial help teams understand?",
|
||||
"answer": "Tenantial is positioned around backup evidence, restore planning, drift detection, findings, auditability, exceptions, and reviews for Microsoft tenant configuration."
|
||||
},
|
||||
{
|
||||
"question": "Does this public website connect to a live tenant?",
|
||||
"answer": "No. Product previews and examples are static demonstrations and do not authenticate visitors or read tenant data."
|
||||
@ -9,10 +13,6 @@
|
||||
"question": "Is restore positioned as an automatic outcome?",
|
||||
"answer": "No. Tenantial describes restore as a cautious workflow with preview, validation, selective scope, explicit confirmation, and review context."
|
||||
},
|
||||
{
|
||||
"question": "Are pricing plans self-serve?",
|
||||
"answer": "Not from this website. Pricing is framed around evaluation and scoped rollout conversations until commercial packaging is finalized."
|
||||
},
|
||||
{
|
||||
"question": "Does Tenantial publish customer proof here?",
|
||||
"answer": "No customer logos, third-party endorsements, external assurance statements, service-level commitments, or recovery promises are published without supplied and reviewed evidence."
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
[
|
||||
{
|
||||
"heading": "Inventory evidence",
|
||||
"content": "Normalize observed policy metadata so reviews start from what was last seen, not scattered exports.",
|
||||
"heading": "Backup evidence",
|
||||
"content": "Review observed policy metadata and backup snapshots from one readable source before decisions start.",
|
||||
"svg": "frame"
|
||||
},
|
||||
{
|
||||
"heading": "Immutable snapshots",
|
||||
"content": "Keep point-in-time policy records that can be compared, reviewed, and used for restore planning.",
|
||||
"heading": "Restore planning",
|
||||
"content": "Compare immutable snapshots and plan restore work with validation, selective scope, and explicit review.",
|
||||
"svg": "dashboard"
|
||||
},
|
||||
{
|
||||
"heading": "Drift review",
|
||||
"content": "Turn differences and findings into readable review work before administrators decide what to change.",
|
||||
"heading": "Drift detection",
|
||||
"content": "Turn differences, findings, and evidence into readable review work before administrators decide what to change.",
|
||||
"svg": "verified"
|
||||
},
|
||||
{
|
||||
"heading": "Restore caution",
|
||||
"content": "Frame restore around preview, validation, conflict awareness, selective scope, and explicit confirmation.",
|
||||
"heading": "Auditability and exceptions",
|
||||
"content": "Keep exceptions, review notes, and auditability visible so decisions stay attributable.",
|
||||
"svg": "checkCircle"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
{
|
||||
"title": "Evaluation and rollout options",
|
||||
"subTitle": "Commercial packaging is intentionally conservative on the public website. No self-serve checkout or fixed entitlement claim is implied.",
|
||||
"subTitle": "Commercial packaging is intentionally conservative on the public website. No instant payment path or fixed access package is implied.",
|
||||
"badge": "Scoped",
|
||||
"thirdOption": "Need a narrower advisory conversation?",
|
||||
"btnText": "Contact us",
|
||||
"thirdOption": "Need a narrower evaluation conversation?",
|
||||
"btnText": "Contact Tenantial",
|
||||
"starterKit": {
|
||||
"name": "Evaluation",
|
||||
"description": "For teams reviewing fit before rollout.",
|
||||
"description": "For teams reviewing fit before any rollout commitment.",
|
||||
"price": "Contact",
|
||||
"cents": "",
|
||||
"billingFrequency": "for scope",
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
"professionalToolbox": {
|
||||
"name": "Managed rollout",
|
||||
"description": "For planned staging and operator review.",
|
||||
"description": "For planned staging and operator review conversations.",
|
||||
"price": "Scoped",
|
||||
"cents": "",
|
||||
"billingFrequency": "quote",
|
||||
|
||||
651
apps/website/src/data_files/site-copy.ts
Normal file
651
apps/website/src/data_files/site-copy.ts
Normal file
@ -0,0 +1,651 @@
|
||||
import type { Locale } from '@/i18n';
|
||||
|
||||
type Feature = {
|
||||
heading: string;
|
||||
content: string;
|
||||
svg: string;
|
||||
};
|
||||
|
||||
type PricingProduct = {
|
||||
name: string;
|
||||
description: string;
|
||||
price: string;
|
||||
cents: string;
|
||||
billingFrequency: string;
|
||||
features: string[];
|
||||
purchaseBtnTitle: string;
|
||||
purchaseLink: string;
|
||||
};
|
||||
|
||||
type Pricing = {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
badge: string;
|
||||
thirdOption: string;
|
||||
btnText: string;
|
||||
starterKit: PricingProduct;
|
||||
professionalToolbox: PricingProduct;
|
||||
};
|
||||
|
||||
type FaqGroup = {
|
||||
subTitle: string;
|
||||
faqs: Array<{ question: string; answer: string }>;
|
||||
};
|
||||
|
||||
export const siteCopy: Record<Locale, any> = {
|
||||
de: {
|
||||
site: {
|
||||
tagline: 'Evidenzbasierte Microsoft-Tenant-Governance',
|
||||
description:
|
||||
'Tenantial hilft Microsoft-Tenant-Administratoren, Policy-Evidence, Snapshots, Drift, Findings und Restore-Pläne vor kritischen Änderungen zu prüfen.',
|
||||
descriptionShort:
|
||||
'Evidenzbasierte Governance-Workflows für Microsoft-Tenant-Reviews, Backup, Drift und Restore-Planung.',
|
||||
},
|
||||
nav: [
|
||||
{ name: 'Start', url: '/' },
|
||||
{ name: 'Plattform', url: '/platform' },
|
||||
{ name: 'Preise', url: '/pricing' },
|
||||
{ name: 'Vertrauen', url: '/trust' },
|
||||
{ name: 'Docs', url: '/welcome-to-docs/' },
|
||||
{ name: 'Kontakt', url: '/contact' },
|
||||
],
|
||||
footer: {
|
||||
sections: [
|
||||
{
|
||||
section: 'Produkt',
|
||||
links: [
|
||||
{ name: 'Plattform', url: '/platform' },
|
||||
{ name: 'Preise', url: '/pricing' },
|
||||
{ name: 'Vertrauen', url: '/trust' },
|
||||
{ name: 'Docs', url: '/welcome-to-docs/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Rechtliches',
|
||||
links: [
|
||||
{ name: 'Datenschutz', url: '/privacy' },
|
||||
{ name: 'Nutzungsbedingungen', url: '/terms' },
|
||||
{ name: 'Rechtliches', url: '/legal' },
|
||||
{ name: 'Impressum', url: '/imprint' },
|
||||
],
|
||||
},
|
||||
],
|
||||
conversationTitle: 'Scoped Conversation starten',
|
||||
conversationContent:
|
||||
'Nutze die Kontaktseite oder E-Mail nur für Business-Kontext. Sende keine Secrets, Credentials oder Tenant-Exporte über die öffentliche Website.',
|
||||
contactButton: 'Kontaktseite öffnen',
|
||||
copyrightSuffix: 'Nur öffentliche Website-Inhalte.',
|
||||
},
|
||||
auth: {
|
||||
walkthrough: 'Walkthrough anfragen',
|
||||
},
|
||||
home: {
|
||||
pageTitle: 'Tenantial | Evidenzbasierte Microsoft-Tenant-Governance',
|
||||
metaDescription:
|
||||
'Tenantial ist evidenzbasierte Governance für Microsoft-Tenant-Konfiguration mit Backup, Restore, Drift Detection, Findings, Evidence, Auditability, Exceptions und Reviews.',
|
||||
heroTitle:
|
||||
'Evidenzbasierte Governance für <span class="text-yellow-500 dark:text-yellow-400">Microsoft-Tenant-Konfiguration</span>',
|
||||
heroSubtitle:
|
||||
'Tenantial hilft Administratoren, Backup-Evidence, unveränderliche Snapshots, Drift Detection, Findings, Exceptions, Auditability und Restore-Pläne zu prüfen, bevor kritische Änderungen umgesetzt werden.',
|
||||
primaryCta: 'Walkthrough anfragen',
|
||||
secondaryCta: 'Plattform ansehen',
|
||||
heroAlt: 'Statische Tenantial Governance-Dashboard-Vorschau',
|
||||
featureTitle: 'Backup, Restore und Drift Detection mit Review-Kontext',
|
||||
featureSubtitle:
|
||||
'Tenantial hält Backup-Snapshots, Findings, Evidence, Auditability, Exceptions und Reviews sichtbar, damit Restore-Entscheidungen preview-first bleiben. Die öffentliche Vorschau ist statisch und verbindet sich nicht mit Live-Tenant-Daten.',
|
||||
featureAlt: 'Statische Tenantial Review-Board-Vorschau',
|
||||
workflowTitle:
|
||||
'Ein <span class="text-yellow-500 dark:text-yellow-400">Review-Workflow</span> für evidenzbasierte Tenant-Governance.',
|
||||
tabs: [
|
||||
{
|
||||
heading: 'Evidence Intake',
|
||||
content:
|
||||
'Normalisiere Inventory-, Backup- und Snapshot-Kontext, damit Reviews mit einer lesbaren Sicht auf den beobachteten Policy-Zustand starten.',
|
||||
svg: 'frame',
|
||||
alt: 'Statische Tenantial Evidence-Intake-Vorschau',
|
||||
first: true,
|
||||
},
|
||||
{
|
||||
heading: 'Decision Review',
|
||||
content:
|
||||
'Mache Drift Detection, Findings, Exceptions, Auditability und Review-Notizen sichtbar, bevor Administratoren die nächste Aktion wählen.',
|
||||
svg: 'dashboard',
|
||||
alt: 'Statische Tenantial Decision-Review-Vorschau',
|
||||
second: true,
|
||||
},
|
||||
{
|
||||
heading: 'Restore-Planung',
|
||||
content:
|
||||
'Behandle Restore als preview-first Arbeit mit Validierung, Konfliktbewusstsein, selektivem Scope und expliziter Bestätigung.',
|
||||
svg: 'verified',
|
||||
alt: 'Statische Tenantial Restore-Planungs-Vorschau',
|
||||
},
|
||||
],
|
||||
faqTitle: 'Häufige<br />Fragen',
|
||||
},
|
||||
platform: {
|
||||
pageTitle: 'Plattform | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial öffentliches Produktmodell für Microsoft-Tenant Backup-Evidence, Restore-Planung, Drift Detection, Findings, Auditability, Exceptions und Reviews.',
|
||||
heading: 'Plattform-Review-Modell',
|
||||
subtitle:
|
||||
'Tenantial ist auf Backup-Evidence, unveränderliche Policy-Snapshots, strukturierte Diffs, Drift Detection, Findings, Exceptions, Auditability, Governance-Reviews und vorsichtige Restore-Workflows für Microsoft-Tenant-Administratoren ausgerichtet.',
|
||||
backupTitle: 'Backup- und Snapshot-Evidence',
|
||||
backupSubtitle:
|
||||
'Administratoren brauchen einen reproduzierbaren Nachweis dessen, was beobachtet wurde, bevor entschieden wird, ob eine Änderung sicher ist. Tenantial rahmt Backup-Snapshots als Review-Evidence, nicht als versteckte Automation.',
|
||||
dashboardAlt: 'Statische Tenantial Inventory-Dashboard-Vorschau',
|
||||
evidenceAlt: 'Statische Tenantial Evidence-Review-Vorschau',
|
||||
driftTitle: 'Drift Detection, Findings und Exceptions',
|
||||
driftSubtitle:
|
||||
'Unterschiede und Findings sollen lesbare Entscheidungsarbeit werden. Exceptions, Evidence, Review-Notizen und Auditability halten die Begründung sichtbar, ohne zu behaupten, dass die öffentliche Website mit Live-Tenant-Daten verbunden ist.',
|
||||
driftAlt: 'Statische Tenantial Drift-Workflow-Vorschau',
|
||||
trustCta: 'Trust-Posture ansehen',
|
||||
restoreTitle: 'Restore-Planung bleibt defensiv',
|
||||
restoreSubtitle:
|
||||
'Restore-Pfade werden als preview-first Workflows mit Validierung, Konfliktbewusstsein, selektivem Scope und expliziter Bestätigung vor sensiblen Aktionen beschrieben.',
|
||||
restoreAlt: 'Statische Tenantial Rollout-Readiness-Plan-Vorschau',
|
||||
rolloutCta: 'Rollout besprechen',
|
||||
boundaryTitle: 'Grenzen der öffentlichen Vorschau',
|
||||
boundarySubtitle:
|
||||
'Die Website nutzt statische Demo-Previews, um Richtung und visuelle Grundlage zu zeigen. Sie authentifiziert keine Besucher, liest keinen Microsoft Tenant, führt keine Operationen aus und speichert keine Tenant-Exporte.',
|
||||
stats: [
|
||||
{ stat: 'Statisch', description: 'Produkt-Previews' },
|
||||
{ stat: 'Preview', description: 'vor Restore-Ausführung' },
|
||||
{ stat: 'Review', description: 'vor kritischen Änderungen' },
|
||||
],
|
||||
mainStatTitle: '0',
|
||||
mainStatSubTitle:
|
||||
'Live-Tenant-Datensätze werden von dieser öffentlichen Website genutzt',
|
||||
},
|
||||
pricingIntro: {
|
||||
pageTitle: 'Preise | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial Preise nutzen konservative Evaluierungs- und Rollout-Gespräche statt sofortiger Zahlung oder Access-Claims.',
|
||||
heading: 'Preise bleiben scoped',
|
||||
subtitle:
|
||||
'Tenantial behauptet auf der öffentlichen Website keine Sofortzahlung, keinen festen Paket-Zugang und keine automatische Tenant-Verbindung.',
|
||||
},
|
||||
contact: {
|
||||
pageTitle: 'Kontakt | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial Walkthrough anfragen oder ein scoped Rollout-Gespräch starten. Die öffentliche Website sammelt keine Live-Tenant-Daten.',
|
||||
title: 'Kontakt zu Tenantial',
|
||||
subtitle:
|
||||
'Fordere einen Walkthrough an oder starte ein scoped Rollout-Gespräch. Sende keine Secrets, Credentials oder Tenant-Exporte über diese öffentliche Website.',
|
||||
formTitle: 'Walkthrough-Anfrage vorbereiten',
|
||||
formSubtitle:
|
||||
'Dieses statische Formular sendet nicht an ein Backend. Nutze es, um Kontext vor einer E-Mail an Tenantial vorzubereiten.',
|
||||
firstName: 'Vorname',
|
||||
lastName: 'Nachname',
|
||||
email: 'E-Mail',
|
||||
phone: 'Telefonnummer',
|
||||
details: 'Details',
|
||||
submit: 'E-Mail-Entwurf vorbereiten',
|
||||
blocks: {
|
||||
docsHeading: 'Knowledgebase',
|
||||
docsContent: 'Lies öffentliche Notizen zum Tenantial-Review-Modell.',
|
||||
docsLink: 'Docs besuchen',
|
||||
faqHeading: 'FAQ',
|
||||
faqContent:
|
||||
'Durchsuche die FAQ für kurze, klare Antworten auf häufige Fragen.',
|
||||
faqLink: 'FAQ besuchen',
|
||||
boundaryHeading: 'Grenze der Produktvorschau',
|
||||
boundaryContent:
|
||||
'Die Website verbindet sich nicht mit Live-Tenant-Daten und startet keine Backend-Workflows.',
|
||||
emailHeading: 'Kontakt per E-Mail',
|
||||
emailContent: 'Lieber schriftlich? Schreibe uns eine E-Mail an',
|
||||
},
|
||||
},
|
||||
trust: {
|
||||
pageTitle: 'Vertrauen | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial öffentliche Trust-Posture mit konservativen Claims und klaren Grenzen für statische Previews.',
|
||||
heading: 'Vertrauen beginnt mit klaren Grenzen',
|
||||
subtitle:
|
||||
'Tenantial Public Copy vermeidet Customer-Logo-Proof, Third-Party-Endorsements, externe Assurance-Aussagen, Service-Level-Commitments und Recovery-Versprechen, solange sie nicht geliefert und geprüft wurden.',
|
||||
cta: 'Tenantial kontaktieren',
|
||||
statsTitle: 'Public Website Posture',
|
||||
statsSubtitle:
|
||||
'Die Website beschreibt eine vorsichtige Produktrichtung. Sie legt keine privaten Tenant-Daten offen, führt keine Tenant-Operationen aus und ist kein Support-Evidence-Portal.',
|
||||
mainStatTitle: 'Statisch',
|
||||
mainStatSubTitle: 'nur Produkt-Previews',
|
||||
stats: [
|
||||
{ stat: 'Kein', description: 'Customer-Logo-Proof' },
|
||||
{ stat: 'Kein', description: 'externer Endorsement-Claim' },
|
||||
{ stat: 'Keine', description: 'Live-Tenant-Verbindung' },
|
||||
],
|
||||
},
|
||||
legal: {
|
||||
pageTitle: 'Rechtliches | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial öffentliche rechtliche Website-Informationen.',
|
||||
heading: 'Öffentliche rechtliche Informationen',
|
||||
subtitle:
|
||||
'Diese Seiten enthalten konservative Informationen zur öffentlichen Website und sollten vor einer produktiven Veröffentlichung in regulierten Kontexten geprüft werden.',
|
||||
privacy: 'Datenschutz',
|
||||
terms: 'Nutzungsbedingungen',
|
||||
imprint: 'Impressum',
|
||||
},
|
||||
privacy: {
|
||||
pageTitle: 'Datenschutz | Tenantial',
|
||||
metaDescription: 'Tenantial öffentlicher Website-Datenschutzhinweis.',
|
||||
heading: 'Datenschutzhinweis',
|
||||
subtitle:
|
||||
'Diese statische öffentliche Website beschreibt Tenantial und die Produktrichtung. Produkt-Previews verbinden sich nicht mit einem Live-Tenant. Kontaktlinks können deinen E-Mail-Client öffnen; sende keine Secrets, Credentials oder privaten Tenant-Exporte.',
|
||||
},
|
||||
terms: {
|
||||
pageTitle: 'Nutzungsbedingungen | Tenantial',
|
||||
metaDescription: 'Tenantial öffentliche Website-Nutzungsbedingungen.',
|
||||
heading: 'Website-Nutzungsbedingungen',
|
||||
subtitle:
|
||||
'Die öffentliche Website beschreibt Tenantial-Positionierung und statische Produktkonzepte. Sie stellt keinen Produktionsservice bereit, verbindet sich nicht mit einem Tenant und führt keine administrativen Aktionen aus. Kommerzielle Bedingungen erfordern eine separate schriftliche Vereinbarung.',
|
||||
},
|
||||
imprint: {
|
||||
pageTitle: 'Impressum | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial öffentliche Website-Veröffentlichungsinformationen.',
|
||||
heading: 'Veröffentlichungsinformationen',
|
||||
subtitle:
|
||||
'Publisher: Tenantial. Business-Kontakt: hello@tenantial.com. Formale Registrierungs- und Verantwortlichkeitsangaben sollten im schriftlichen Evaluierungsprozess bestätigt werden, bevor diese Seite für regulierte Publikationsanforderungen genutzt wird.',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
site: {
|
||||
tagline: 'Evidence-first Microsoft tenant governance',
|
||||
description:
|
||||
'Tenantial helps Microsoft tenant administrators review policy evidence, snapshots, drift, findings, and restore plans before high-impact changes move forward.',
|
||||
descriptionShort:
|
||||
'Evidence-first governance workflows for Microsoft tenant review, backup, drift, and restore planning.',
|
||||
},
|
||||
nav: [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Platform', url: '/platform' },
|
||||
{ name: 'Pricing', url: '/pricing' },
|
||||
{ name: 'Trust', url: '/trust' },
|
||||
{ name: 'Docs', url: '/welcome-to-docs/' },
|
||||
{ name: 'Contact', url: '/contact' },
|
||||
],
|
||||
footer: {
|
||||
sections: [
|
||||
{
|
||||
section: 'Product',
|
||||
links: [
|
||||
{ name: 'Platform', url: '/platform' },
|
||||
{ name: 'Pricing', url: '/pricing' },
|
||||
{ name: 'Trust', url: '/trust' },
|
||||
{ name: 'Docs', url: '/welcome-to-docs/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Legal',
|
||||
links: [
|
||||
{ name: 'Privacy', url: '/privacy' },
|
||||
{ name: 'Terms', url: '/terms' },
|
||||
{ name: 'Legal', url: '/legal' },
|
||||
{ name: 'Imprint', url: '/imprint' },
|
||||
],
|
||||
},
|
||||
],
|
||||
conversationTitle: 'Start a scoped conversation',
|
||||
conversationContent:
|
||||
'Use the contact page or email for business context only. Do not send secrets, credentials, or tenant exports through the public website.',
|
||||
contactButton: 'Open contact page',
|
||||
copyrightSuffix: 'Public website content only.',
|
||||
},
|
||||
auth: {
|
||||
walkthrough: 'Request walkthrough',
|
||||
},
|
||||
home: {
|
||||
pageTitle: 'Tenantial | Evidence-first Microsoft tenant governance',
|
||||
metaDescription:
|
||||
'Tenantial is evidence-first governance for Microsoft tenant configuration, with backup, restore, drift detection, findings, evidence, auditability, exceptions, and reviews.',
|
||||
heroTitle:
|
||||
'Evidence-first governance for <span class="text-yellow-500 dark:text-yellow-400">Microsoft tenant configuration</span>',
|
||||
heroSubtitle:
|
||||
'Tenantial helps administrators review backup evidence, immutable snapshots, drift detection, findings, exceptions, auditability, and restore plans before high-impact changes move forward.',
|
||||
primaryCta: 'Request walkthrough',
|
||||
secondaryCta: 'Explore platform',
|
||||
heroAlt: 'Static Tenantial governance dashboard preview',
|
||||
featureTitle: 'Backup, restore, and drift detection with review context',
|
||||
featureSubtitle:
|
||||
'Tenantial keeps backup snapshots, findings, evidence, auditability, exceptions, and reviews visible so restore decisions stay preview-first. The public preview is static and does not connect to live tenant data.',
|
||||
featureAlt: 'Static Tenantial review board preview',
|
||||
workflowTitle:
|
||||
'A <span class="text-yellow-500 dark:text-yellow-400">review workflow</span> for evidence-first tenant governance.',
|
||||
tabs: [
|
||||
{
|
||||
heading: 'Evidence intake',
|
||||
content:
|
||||
'Normalize inventory, backup, and snapshot context so reviews start from one readable view of observed policy state.',
|
||||
svg: 'frame',
|
||||
alt: 'Static Tenantial evidence intake preview',
|
||||
first: true,
|
||||
},
|
||||
{
|
||||
heading: 'Decision review',
|
||||
content:
|
||||
'Surface drift detection, findings, exceptions, auditability, and review notes before administrators choose the next action.',
|
||||
svg: 'dashboard',
|
||||
alt: 'Static Tenantial decision review preview',
|
||||
second: true,
|
||||
},
|
||||
{
|
||||
heading: 'Restore planning',
|
||||
content:
|
||||
'Treat restore as preview-first work with validation, conflict awareness, selective scope, and explicit confirmation.',
|
||||
svg: 'verified',
|
||||
alt: 'Static Tenantial restore planning preview',
|
||||
},
|
||||
],
|
||||
faqTitle: 'Frequently<br />asked questions',
|
||||
},
|
||||
platform: {
|
||||
pageTitle: 'Platform | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial public product model for Microsoft tenant backup evidence, restore planning, drift detection, findings, auditability, exceptions, and reviews.',
|
||||
heading: 'Platform review model',
|
||||
subtitle:
|
||||
'Tenantial is positioned around backup evidence, immutable policy snapshots, structured diffs, drift detection, findings, exceptions, auditability, governance reviews, and cautious restore workflows for Microsoft tenant administrators.',
|
||||
backupTitle: 'Backup and snapshot evidence',
|
||||
backupSubtitle:
|
||||
'Administrators need a reproducible record of what was observed before deciding whether a change is safe. Tenantial frames backup snapshots as review evidence, not as hidden automation.',
|
||||
dashboardAlt: 'Static Tenantial inventory dashboard preview',
|
||||
evidenceAlt: 'Static Tenantial evidence review panel preview',
|
||||
driftTitle: 'Drift detection, findings, and exceptions',
|
||||
driftSubtitle:
|
||||
'Differences and findings should become readable decision work. Exceptions, evidence, review notes, and auditability keep the reasoning visible without claiming that the public website is connected to live tenant data.',
|
||||
driftAlt: 'Static Tenantial drift workflow preview',
|
||||
trustCta: 'Review trust posture',
|
||||
restoreTitle: 'Restore planning stays defensive',
|
||||
restoreSubtitle:
|
||||
'Restore paths are described as preview-first workflows with validation, conflict awareness, selective scope, and explicit confirmation before any sensitive action.',
|
||||
restoreAlt: 'Static Tenantial rollout readiness plan preview',
|
||||
rolloutCta: 'Discuss rollout',
|
||||
boundaryTitle: 'Public preview boundaries',
|
||||
boundarySubtitle:
|
||||
'The website uses static/demo previews to demonstrate the direction and visual foundation. It does not authenticate visitors, read a Microsoft tenant, execute operations, or store tenant exports.',
|
||||
stats: [
|
||||
{ stat: 'Static', description: 'product previews' },
|
||||
{ stat: 'Preview', description: 'before restore execution' },
|
||||
{ stat: 'Review', description: 'before high-impact changes' },
|
||||
],
|
||||
mainStatTitle: '0',
|
||||
mainStatSubTitle: 'live tenant records used by this public website',
|
||||
},
|
||||
pricingIntro: {
|
||||
pageTitle: 'Pricing | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial pricing uses conservative evaluation and rollout conversation paths, not instant public payment or access claims.',
|
||||
heading: 'Pricing stays scoped',
|
||||
subtitle:
|
||||
'Tenantial does not claim instant payment, fixed package access, or automatic tenant connection from the public website.',
|
||||
},
|
||||
contact: {
|
||||
pageTitle: 'Contact | Tenantial',
|
||||
metaDescription:
|
||||
'Request a Tenantial walkthrough or start a scoped rollout conversation. The public website does not collect live tenant data.',
|
||||
title: 'Contact Tenantial',
|
||||
subtitle:
|
||||
'Request a walkthrough or start a scoped rollout conversation. Do not send secrets, credentials, or tenant exports through this public website.',
|
||||
formTitle: 'Draft a walkthrough request',
|
||||
formSubtitle:
|
||||
'This static form does not submit to a backend. Use it to draft context before emailing Tenantial.',
|
||||
firstName: 'First Name',
|
||||
lastName: 'Last Name',
|
||||
email: 'Email',
|
||||
phone: 'Phone Number',
|
||||
details: 'Details',
|
||||
submit: 'Prepare email draft',
|
||||
blocks: {
|
||||
docsHeading: 'Knowledgebase',
|
||||
docsContent: 'Read public notes about the Tenantial review model.',
|
||||
docsLink: 'Visit docs',
|
||||
faqHeading: 'FAQ',
|
||||
faqContent:
|
||||
'Explore our FAQ for quick, clear answers to common queries.',
|
||||
faqLink: 'Visit FAQ',
|
||||
boundaryHeading: 'Product preview boundary',
|
||||
boundaryContent:
|
||||
'The website does not connect to live tenant data or start backend workflows.',
|
||||
emailHeading: 'Contact us by email',
|
||||
emailContent: 'Prefer the written word? Drop us an email at',
|
||||
},
|
||||
},
|
||||
trust: {
|
||||
pageTitle: 'Trust | Tenantial',
|
||||
metaDescription:
|
||||
'Tenantial public trust posture with conservative claims and clear static preview boundaries.',
|
||||
heading: 'Trust starts with clear boundaries',
|
||||
subtitle:
|
||||
'Tenantial public copy avoids customer-logo proof, third-party endorsement language, external assurance statements, service-level commitments, and recovery promises unless supplied and reviewed.',
|
||||
cta: 'Contact Tenantial',
|
||||
statsTitle: 'Public website posture',
|
||||
statsSubtitle:
|
||||
'The website describes a cautious product direction. It does not expose private tenant data, run tenant operations, or act as a support evidence portal.',
|
||||
mainStatTitle: 'Static',
|
||||
mainStatSubTitle: 'product previews only',
|
||||
stats: [
|
||||
{ stat: 'No', description: 'customer-logo proof' },
|
||||
{ stat: 'No', description: 'external endorsement claim' },
|
||||
{ stat: 'No', description: 'live tenant connection' },
|
||||
],
|
||||
},
|
||||
legal: {
|
||||
pageTitle: 'Legal | Tenantial',
|
||||
metaDescription: 'Tenantial public website legal information.',
|
||||
heading: 'Public legal information',
|
||||
subtitle:
|
||||
'These pages provide conservative public website information and should be reviewed before production publication in any regulated context.',
|
||||
privacy: 'Privacy',
|
||||
terms: 'Terms',
|
||||
imprint: 'Imprint',
|
||||
},
|
||||
privacy: {
|
||||
pageTitle: 'Privacy | Tenantial',
|
||||
metaDescription: 'Tenantial public website privacy notice.',
|
||||
heading: 'Privacy notice',
|
||||
subtitle:
|
||||
'This static public website describes Tenantial and its product direction. Product previews do not connect to a live tenant. Contact links may open your email client; do not send secrets, credentials, or private tenant exports.',
|
||||
},
|
||||
terms: {
|
||||
pageTitle: 'Terms | Tenantial',
|
||||
metaDescription: 'Tenantial public website terms.',
|
||||
heading: 'Website terms',
|
||||
subtitle:
|
||||
'The public website describes Tenantial positioning and static product concepts. It does not provide a production service, connect to a tenant, or execute administrative actions. Commercial terms require a separate written agreement.',
|
||||
},
|
||||
imprint: {
|
||||
pageTitle: 'Imprint | Tenantial',
|
||||
metaDescription: 'Tenantial public website publication information.',
|
||||
heading: 'Publication information',
|
||||
subtitle:
|
||||
'Publisher: Tenantial. Business contact: hello@tenantial.com. Formal registration and responsible-party details should be confirmed through the written evaluation process before relying on this page for regulated publication needs.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const featuresByLocale: Record<Locale, Feature[]> = {
|
||||
de: [
|
||||
{
|
||||
heading: 'Backup-Evidence',
|
||||
content:
|
||||
'Prüfe beobachtete Policy-Metadaten und Backup-Snapshots aus einer lesbaren Quelle, bevor Entscheidungen beginnen.',
|
||||
svg: 'frame',
|
||||
},
|
||||
{
|
||||
heading: 'Restore-Planung',
|
||||
content:
|
||||
'Vergleiche unveränderliche Snapshots und plane Restore-Arbeit mit Validierung, selektivem Scope und explizitem Review.',
|
||||
svg: 'dashboard',
|
||||
},
|
||||
{
|
||||
heading: 'Drift Detection',
|
||||
content:
|
||||
'Verwandle Unterschiede, Findings und Evidence in lesbare Review-Arbeit, bevor Administratoren Änderungen entscheiden.',
|
||||
svg: 'verified',
|
||||
},
|
||||
{
|
||||
heading: 'Auditability und Exceptions',
|
||||
content:
|
||||
'Halte Exceptions, Review-Notizen und Auditability sichtbar, damit Entscheidungen zuordenbar bleiben.',
|
||||
svg: 'checkCircle',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
heading: 'Backup evidence',
|
||||
content:
|
||||
'Review observed policy metadata and backup snapshots from one readable source before decisions start.',
|
||||
svg: 'frame',
|
||||
},
|
||||
{
|
||||
heading: 'Restore planning',
|
||||
content:
|
||||
'Compare immutable snapshots and plan restore work with validation, selective scope, and explicit review.',
|
||||
svg: 'dashboard',
|
||||
},
|
||||
{
|
||||
heading: 'Drift detection',
|
||||
content:
|
||||
'Turn differences, findings, and evidence into readable review work before administrators decide what to change.',
|
||||
svg: 'verified',
|
||||
},
|
||||
{
|
||||
heading: 'Auditability and exceptions',
|
||||
content:
|
||||
'Keep exceptions, review notes, and auditability visible so decisions stay attributable.',
|
||||
svg: 'checkCircle',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const pricingByLocale: Record<Locale, Pricing> = {
|
||||
de: {
|
||||
title: 'Evaluierungs- und Rollout-Optionen',
|
||||
subTitle:
|
||||
'Kommerzielle Pakete bleiben auf der öffentlichen Website bewusst konservativ. Es wird kein Sofortkauf oder fester Access-Plan behauptet.',
|
||||
badge: 'Scoped',
|
||||
thirdOption: 'Brauchst du ein engeres Evaluierungsgespräch?',
|
||||
btnText: 'Tenantial kontaktieren',
|
||||
starterKit: {
|
||||
name: 'Evaluation',
|
||||
description: 'Für Teams, die Fit vor einer Rollout-Zusage prüfen.',
|
||||
price: 'Kontakt',
|
||||
cents: '',
|
||||
billingFrequency: 'für Scope',
|
||||
features: [
|
||||
'Geführter Produkt-Walkthrough',
|
||||
'Statische Demo-Review',
|
||||
'Deployment-Fit-Gespräch',
|
||||
],
|
||||
purchaseBtnTitle: 'Walkthrough anfragen',
|
||||
purchaseLink: '/contact',
|
||||
},
|
||||
professionalToolbox: {
|
||||
name: 'Managed Rollout',
|
||||
description: 'Für geplante Staging- und Operator-Review-Gespräche.',
|
||||
price: 'Scoped',
|
||||
cents: '',
|
||||
billingFrequency: 'Angebot',
|
||||
features: [
|
||||
'Readiness Review',
|
||||
'Staging-Validierungsplan',
|
||||
'Operational Handoff Notes',
|
||||
'Trust- und Legal-Copy-Review',
|
||||
],
|
||||
purchaseBtnTitle: 'Rollout besprechen',
|
||||
purchaseLink: '/contact',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Evaluation and rollout options',
|
||||
subTitle:
|
||||
'Commercial packaging is intentionally conservative on the public website. No instant payment path or fixed access package is implied.',
|
||||
badge: 'Scoped',
|
||||
thirdOption: 'Need a narrower evaluation conversation?',
|
||||
btnText: 'Contact Tenantial',
|
||||
starterKit: {
|
||||
name: 'Evaluation',
|
||||
description: 'For teams reviewing fit before any rollout commitment.',
|
||||
price: 'Contact',
|
||||
cents: '',
|
||||
billingFrequency: 'for scope',
|
||||
features: [
|
||||
'Guided public product walkthrough',
|
||||
'Static demo review',
|
||||
'Deployment-fit discussion',
|
||||
],
|
||||
purchaseBtnTitle: 'Request walkthrough',
|
||||
purchaseLink: '/contact',
|
||||
},
|
||||
professionalToolbox: {
|
||||
name: 'Managed rollout',
|
||||
description: 'For planned staging and operator review conversations.',
|
||||
price: 'Scoped',
|
||||
cents: '',
|
||||
billingFrequency: 'quote',
|
||||
features: [
|
||||
'Readiness review',
|
||||
'Staging validation plan',
|
||||
'Operational handoff notes',
|
||||
'Trust and legal copy review',
|
||||
],
|
||||
purchaseBtnTitle: 'Discuss rollout',
|
||||
purchaseLink: '/contact',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const faqsByLocale: Record<Locale, FaqGroup> = {
|
||||
de: {
|
||||
subTitle: 'Klare Antworten für Besucher der öffentlichen Website.',
|
||||
faqs: [
|
||||
{
|
||||
question: 'Was hilft Tenantial Teams zu verstehen?',
|
||||
answer:
|
||||
'Tenantial ist auf Backup-Evidence, Restore-Planung, Drift Detection, Findings, Auditability, Exceptions und Reviews für Microsoft-Tenant-Konfiguration ausgerichtet.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
'Verbindet sich diese öffentliche Website mit einem Live-Tenant?',
|
||||
answer:
|
||||
'Nein. Produkt-Previews und Beispiele sind statische Demonstrationen und authentifizieren keine Besucher oder lesen Tenant-Daten.',
|
||||
},
|
||||
{
|
||||
question: 'Wird Restore als automatisches Ergebnis dargestellt?',
|
||||
answer:
|
||||
'Nein. Tenantial beschreibt Restore als vorsichtigen Workflow mit Preview, Validierung, selektivem Scope, expliziter Bestätigung und Review-Kontext.',
|
||||
},
|
||||
{
|
||||
question: 'Veröffentlicht Tenantial hier Customer Proof?',
|
||||
answer:
|
||||
'Ohne gelieferte und geprüfte Evidence werden keine Kundenlogos, Third-Party-Endorsements, externen Assurance-Aussagen, Service-Level-Commitments oder Recovery-Versprechen veröffentlicht.',
|
||||
},
|
||||
],
|
||||
},
|
||||
en: {
|
||||
subTitle: 'Straight answers for public website visitors.',
|
||||
faqs: [
|
||||
{
|
||||
question: 'What does Tenantial help teams understand?',
|
||||
answer:
|
||||
'Tenantial is positioned around backup evidence, restore planning, drift detection, findings, auditability, exceptions, and reviews for Microsoft tenant configuration.',
|
||||
},
|
||||
{
|
||||
question: 'Does this public website connect to a live tenant?',
|
||||
answer:
|
||||
'No. Product previews and examples are static demonstrations and do not authenticate visitors or read tenant data.',
|
||||
},
|
||||
{
|
||||
question: 'Is restore positioned as an automatic outcome?',
|
||||
answer:
|
||||
'No. Tenantial describes restore as a cautious workflow with preview, validation, selective scope, explicit confirmation, and review context.',
|
||||
},
|
||||
{
|
||||
question: 'Does Tenantial publish customer proof here?',
|
||||
answer:
|
||||
'No customer logos, third-party endorsements, external assurance statements, service-level commitments, or recovery promises are published without supplied and reviewed evidence.',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
75
apps/website/src/i18n.ts
Normal file
75
apps/website/src/i18n.ts
Normal file
@ -0,0 +1,75 @@
|
||||
export const defaultLocale = 'de';
|
||||
export const supportedLocales = ['de', 'en'] as const;
|
||||
|
||||
export type Locale = (typeof supportedLocales)[number];
|
||||
|
||||
export const localeLabels: Record<Locale, string> = {
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
};
|
||||
|
||||
export const localeHtmlLang: Record<Locale, string> = {
|
||||
de: 'de',
|
||||
en: 'en',
|
||||
};
|
||||
|
||||
export const localeOg: Record<Locale, string> = {
|
||||
de: 'de_DE',
|
||||
en: 'en_US',
|
||||
};
|
||||
|
||||
export function isLocale(value: string | undefined): value is Locale {
|
||||
return supportedLocales.includes(value as Locale);
|
||||
}
|
||||
|
||||
export function getLocaleFromPath(pathname: string): Locale {
|
||||
const firstSegment = pathname.split('/').filter(Boolean)[0];
|
||||
|
||||
return isLocale(firstSegment) ? firstSegment : defaultLocale;
|
||||
}
|
||||
|
||||
export function stripLocalePrefix(pathname: string): string {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
|
||||
if (isLocale(segments[0])) {
|
||||
segments.shift();
|
||||
}
|
||||
|
||||
const stripped = `/${segments.join('/')}`;
|
||||
|
||||
return stripped === '/' ? '/' : stripped.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function localizedPath(pathname: string, locale: Locale): string {
|
||||
const basePath = stripLocalePrefix(pathname);
|
||||
const keepTrailingSlash = pathname !== '/' && pathname.endsWith('/');
|
||||
const withTrailingSlash = (path: string) =>
|
||||
keepTrailingSlash && !path.endsWith('/') ? `${path}/` : path;
|
||||
|
||||
if (locale === defaultLocale) {
|
||||
return withTrailingSlash(basePath === '' ? '/' : basePath);
|
||||
}
|
||||
|
||||
if (basePath === '/' || basePath === '') {
|
||||
return `/${locale}/`;
|
||||
}
|
||||
|
||||
return withTrailingSlash(`/${locale}${basePath}`);
|
||||
}
|
||||
|
||||
export function localizeHref(href: string, locale: Locale): string {
|
||||
if (
|
||||
href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('mailto:') ||
|
||||
href.startsWith('tel:') ||
|
||||
href.startsWith('#')
|
||||
) {
|
||||
return href;
|
||||
}
|
||||
|
||||
const [path, hash] = href.split('#');
|
||||
const localized = localizedPath(path || '/', locale);
|
||||
|
||||
return hash ? `${localized}#${hash}` : localized;
|
||||
}
|
||||
BIN
apps/website/src/images/tenantial-logo-lockup-mask.png
Normal file
BIN
apps/website/src/images/tenantial-logo-lockup-mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@ -4,6 +4,7 @@ import Meta from '@components/Meta.astro';
|
||||
import Navbar from '@components/sections/navbar&footer/Navbar.astro';
|
||||
import FooterSection from '@components/sections/navbar&footer/FooterSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import { getLocaleFromPath, localeHtmlLang, type Locale } from '@/i18n';
|
||||
import '@styles/global.css';
|
||||
|
||||
// Setting expected props
|
||||
@ -11,28 +12,32 @@ const {
|
||||
title = SITE.title,
|
||||
meta,
|
||||
structuredData,
|
||||
lang = 'en',
|
||||
lang,
|
||||
customDescription = null,
|
||||
customOgTitle = null,
|
||||
} = Astro.props;
|
||||
|
||||
const locale = (lang || getLocaleFromPath(Astro.url.pathname)) as Locale;
|
||||
const htmlLang = localeHtmlLang[locale] || localeHtmlLang.de;
|
||||
|
||||
// Interface to type-check the properties
|
||||
interface Props {
|
||||
title?: string;
|
||||
meta?: string;
|
||||
structuredData?: object;
|
||||
lang?: string;
|
||||
lang?: Locale;
|
||||
customDescription?: string | null;
|
||||
customOgTitle?: string | null;
|
||||
}
|
||||
---
|
||||
|
||||
<html lang={lang} class="scrollbar-hide lenis lenis-smooth scroll-pt-16">
|
||||
<html lang={htmlLang} class="scrollbar-hide lenis lenis-smooth scroll-pt-16">
|
||||
<head>
|
||||
{/* Adding metadata to the HTML document */}
|
||||
<Meta
|
||||
meta={meta}
|
||||
structuredData={structuredData}
|
||||
locale={locale}
|
||||
customDescription={customDescription}
|
||||
customOgTitle={customOgTitle}
|
||||
/>
|
||||
|
||||
@ -1,34 +1,5 @@
|
||||
---
|
||||
// Import the necessary components
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import ContactSection from '@components/sections/misc/ContactSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
|
||||
const pageTitle: string = `Contact | ${SITE.title}`;
|
||||
const metaDescription =
|
||||
'Request a Tenantial walkthrough or start a scoped rollout conversation. The public website does not collect live tenant data.';
|
||||
const ogTitle = 'Contact | Tenantial';
|
||||
import ContactPage from '@components/pages/ContactPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
customDescription={metaDescription}
|
||||
customOgTitle={ogTitle}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://tenantial.com/contact',
|
||||
url: 'https://tenantial.com/contact',
|
||||
name: 'Contact | Tenantial',
|
||||
description: metaDescription,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
url: 'https://tenantial.com',
|
||||
name: 'Tenantial',
|
||||
description: SITE.description,
|
||||
},
|
||||
inLanguage: 'en-US',
|
||||
}}
|
||||
>
|
||||
<ContactSection />
|
||||
</MainLayout>
|
||||
<ContactPage locale="de" />
|
||||
|
||||
3
apps/website/src/pages/en/blog/index.astro
Normal file
3
apps/website/src/pages/en/blog/index.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
return Astro.redirect('/en/platform', 302);
|
||||
---
|
||||
5
apps/website/src/pages/en/contact.astro
Normal file
5
apps/website/src/pages/en/contact.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import ContactPage from '@components/pages/ContactPage.astro';
|
||||
---
|
||||
|
||||
<ContactPage locale="en" />
|
||||
5
apps/website/src/pages/en/imprint.astro
Normal file
5
apps/website/src/pages/en/imprint.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<TextPage locale="en" page="imprint" />
|
||||
5
apps/website/src/pages/en/index.astro
Normal file
5
apps/website/src/pages/en/index.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import HomePage from '@components/pages/HomePage.astro';
|
||||
---
|
||||
|
||||
<HomePage locale="en" />
|
||||
3
apps/website/src/pages/en/insights/index.astro
Normal file
3
apps/website/src/pages/en/insights/index.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
return Astro.redirect('/en/platform', 302);
|
||||
---
|
||||
5
apps/website/src/pages/en/legal.astro
Normal file
5
apps/website/src/pages/en/legal.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import LegalPage from '@components/pages/LegalPage.astro';
|
||||
---
|
||||
|
||||
<LegalPage locale="en" />
|
||||
5
apps/website/src/pages/en/platform.astro
Normal file
5
apps/website/src/pages/en/platform.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import PlatformPage from '@components/pages/PlatformPage.astro';
|
||||
---
|
||||
|
||||
<PlatformPage locale="en" />
|
||||
5
apps/website/src/pages/en/pricing.astro
Normal file
5
apps/website/src/pages/en/pricing.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import PricingPage from '@components/pages/PricingPage.astro';
|
||||
---
|
||||
|
||||
<PricingPage locale="en" />
|
||||
5
apps/website/src/pages/en/privacy.astro
Normal file
5
apps/website/src/pages/en/privacy.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<TextPage locale="en" page="privacy" />
|
||||
3
apps/website/src/pages/en/product.astro
Normal file
3
apps/website/src/pages/en/product.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
return Astro.redirect('/en/platform', 301);
|
||||
---
|
||||
3
apps/website/src/pages/en/products/index.astro
Normal file
3
apps/website/src/pages/en/products/index.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
return Astro.redirect('/en/platform', 301);
|
||||
---
|
||||
3
apps/website/src/pages/en/services.astro
Normal file
3
apps/website/src/pages/en/services.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
return Astro.redirect('/en/platform', 301);
|
||||
---
|
||||
5
apps/website/src/pages/en/terms.astro
Normal file
5
apps/website/src/pages/en/terms.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<TextPage locale="en" page="terms" />
|
||||
5
apps/website/src/pages/en/trust.astro
Normal file
5
apps/website/src/pages/en/trust.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import TrustPage from '@components/pages/TrustPage.astro';
|
||||
---
|
||||
|
||||
<TrustPage locale="en" />
|
||||
@ -1,16 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={`Imprint | ${SITE.title}`}
|
||||
customDescription="Tenantial public website imprint placeholder."
|
||||
customOgTitle="Imprint | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Publication information"
|
||||
subTitle="Publisher: Tenantial. Contact: hello@tenantial.com. Replace this placeholder with reviewed business registration and responsible-party details before public production use."
|
||||
/>
|
||||
</MainLayout>
|
||||
<TextPage locale="de" page="imprint" />
|
||||
|
||||
@ -1,74 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import HeroSection from '@components/sections/landing/HeroSection.astro';
|
||||
import FeaturesGeneral from '@components/sections/features/FeaturesGeneral.astro';
|
||||
import FeaturesNavs from '@components/sections/features/FeaturesNavs.astro';
|
||||
import PricingSection from '@components/sections/pricing/PricingSection.astro';
|
||||
import FAQ from '@components/sections/misc/FAQ.astro';
|
||||
import heroImage from '@images/tenantial-dashboard.avif';
|
||||
import faqs from '@data/faqs.json';
|
||||
import features from '@data/features.json';
|
||||
import pricing from '@data/pricing.json';
|
||||
import featureImage from '@images/tenantial-review-board.avif';
|
||||
import reviewImage from '@images/tenantial-evidence-intake.avif';
|
||||
import evidenceImage from '@images/tenantial-decision-review.avif';
|
||||
import governanceImage from '@images/tenantial-restore-plan.avif';
|
||||
import HomePage from '@components/pages/HomePage.astro';
|
||||
---
|
||||
|
||||
<MainLayout>
|
||||
<HeroSection
|
||||
title=`Evidence-first governance for <span class="text-yellow-500 dark:text-yellow-400">Microsoft tenants</span>`
|
||||
subTitle="Tenantial helps administrators review policy evidence, snapshots, drift, findings, exceptions, and restore plans before high-impact changes move forward."
|
||||
primaryBtn="Request walkthrough"
|
||||
primaryBtnURL="/contact"
|
||||
secondaryBtn="Explore platform"
|
||||
secondaryBtnURL="/platform"
|
||||
withReview={false}
|
||||
src={heroImage}
|
||||
alt="Static Tenantial governance dashboard preview"
|
||||
/>
|
||||
|
||||
<FeaturesGeneral
|
||||
title="Review tenant change before execution"
|
||||
subTitle="Tenantial focuses on evidence, reproducible snapshots, readable drift, and conservative restore planning. The public preview is static and does not connect to live tenant data."
|
||||
src={featureImage}
|
||||
alt="Static Tenantial review board preview"
|
||||
features={features}
|
||||
/>
|
||||
|
||||
<FeaturesNavs
|
||||
title=`A <span class="text-yellow-500 dark:text-yellow-400">stable website foundation</span> adapted for Tenantial review workflows.`
|
||||
tabs={[
|
||||
{
|
||||
heading: 'Evidence intake',
|
||||
content:
|
||||
'Normalize inventory and snapshot context so reviews start from one readable view of observed policy state.',
|
||||
svg: 'frame',
|
||||
src: reviewImage,
|
||||
alt: 'Static Tenantial evidence intake preview',
|
||||
first: true,
|
||||
},
|
||||
{
|
||||
heading: 'Decision review',
|
||||
content:
|
||||
'Surface drift, findings, exceptions, and audit context before administrators choose the next action.',
|
||||
svg: 'dashboard',
|
||||
src: evidenceImage,
|
||||
alt: 'Static Tenantial decision review preview',
|
||||
second: true,
|
||||
},
|
||||
{
|
||||
heading: 'Restore planning',
|
||||
content:
|
||||
'Treat restore as preview-first work with validation, conflict awareness, selective scope, and explicit confirmation.',
|
||||
svg: 'verified',
|
||||
src: governanceImage,
|
||||
alt: 'Static Tenantial restore planning preview',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<PricingSection pricing={pricing} />
|
||||
|
||||
<FAQ title="Frequently<br />asked questions" faqs={faqs} />
|
||||
</MainLayout>
|
||||
<HomePage locale="de" />
|
||||
|
||||
@ -1,27 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
|
||||
const pageTitle = `Legal | ${SITE.title}`;
|
||||
import LegalPage from '@components/pages/LegalPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
customDescription="Tenantial public website legal information."
|
||||
customOgTitle="Legal | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Public legal information"
|
||||
subTitle="These pages provide conservative public website information and should be reviewed before production publication in any regulated context."
|
||||
/>
|
||||
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<PrimaryCTA title="Privacy" url="/privacy" />
|
||||
<SecondaryCTA title="Terms" url="/terms" />
|
||||
<SecondaryCTA title="Imprint" url="/imprint" />
|
||||
</div>
|
||||
</section>
|
||||
</MainLayout>
|
||||
<LegalPage locale="de" />
|
||||
|
||||
@ -1,98 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import LeftSection from '@components/ui/blocks/LeftSection.astro';
|
||||
import RightSection from '@components/ui/blocks/RightSection.astro';
|
||||
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
|
||||
import dashboard from '@images/tenantial-dashboard.avif';
|
||||
import evidenceImage from '@images/tenantial-evidence-panel.avif';
|
||||
import reviewImage from '@images/tenantial-drift-workflow.avif';
|
||||
import restoreImage from '@images/tenantial-rollout-plan.avif';
|
||||
import { SITE } from '@data/constants';
|
||||
|
||||
const pageTitle: string = `Platform | ${SITE.title}`;
|
||||
const metaDescription =
|
||||
'Tenantial platform overview for Microsoft tenant evidence, snapshots, drift review, findings, audit context, exceptions, and restore planning.';
|
||||
const ogTitle = 'Platform | Tenantial';
|
||||
import PlatformPage from '@components/pages/PlatformPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
customDescription={metaDescription}
|
||||
customOgTitle={ogTitle}
|
||||
structuredData={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://tenantial.com/platform',
|
||||
url: 'https://tenantial.com/platform',
|
||||
name: 'Platform | Tenantial',
|
||||
description: metaDescription,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
url: 'https://tenantial.com',
|
||||
name: 'Tenantial',
|
||||
description: SITE.description,
|
||||
},
|
||||
inLanguage: 'en-US',
|
||||
}}
|
||||
>
|
||||
<MainSection
|
||||
title="Platform review model"
|
||||
subTitle="Tenantial is positioned around inventory evidence, immutable policy snapshots, structured diffs, findings, exceptions, audit trail, governance reviews, and cautious restore workflows for Microsoft tenant administrators."
|
||||
btnExists={true}
|
||||
btnTitle="Request walkthrough"
|
||||
btnURL="/contact"
|
||||
/>
|
||||
|
||||
<RightSection
|
||||
title="Inventory and snapshot evidence"
|
||||
subTitle="Administrators need a reproducible record of what was observed before deciding whether a change is safe. Tenantial frames snapshots as review evidence, not as hidden automation."
|
||||
single={false}
|
||||
imgOne={dashboard}
|
||||
imgOneAlt="Static Tenantial inventory dashboard preview"
|
||||
imgTwo={evidenceImage}
|
||||
imgTwoAlt="Static Tenantial evidence review panel preview"
|
||||
/>
|
||||
|
||||
<LeftSection
|
||||
title="Drift, findings, and exceptions"
|
||||
subTitle="Differences and findings should become readable decision work. Exceptions and review notes keep the reasoning visible without claiming that the public website is connected to live tenant data."
|
||||
img={reviewImage}
|
||||
imgAlt="Static Tenantial drift workflow preview"
|
||||
btnExists={true}
|
||||
btnTitle="Review trust posture"
|
||||
btnURL="/trust"
|
||||
/>
|
||||
|
||||
<RightSection
|
||||
title="Restore planning stays defensive"
|
||||
subTitle="Restore paths are described as preview-first workflows with validation, conflict awareness, selective scope, and explicit confirmation before any sensitive action."
|
||||
single={true}
|
||||
imgOne={restoreImage}
|
||||
imgOneAlt="Static Tenantial rollout readiness plan preview"
|
||||
btnExists={true}
|
||||
btnTitle="Discuss rollout"
|
||||
btnURL="/contact"
|
||||
/>
|
||||
|
||||
<FeaturesStats
|
||||
title="Public preview boundaries"
|
||||
subTitle="The website demonstrates the direction and visual foundation. It does not authenticate visitors, read a Microsoft tenant, execute operations, or store tenant exports."
|
||||
mainStatTitle="0"
|
||||
mainStatSubTitle="live tenant records used by this public website"
|
||||
stats={[
|
||||
{
|
||||
stat: 'Static',
|
||||
description: 'product previews',
|
||||
},
|
||||
{
|
||||
stat: 'Preview',
|
||||
description: 'before restore execution',
|
||||
},
|
||||
{
|
||||
stat: 'Review',
|
||||
description: 'before high-impact changes',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MainLayout>
|
||||
<PlatformPage locale="de" />
|
||||
|
||||
@ -1,23 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import PricingSection from '@components/sections/pricing/PricingSection.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import pricing from '@data/pricing.json';
|
||||
import { SITE } from '@data/constants';
|
||||
|
||||
const pageTitle = `Pricing | ${SITE.title}`;
|
||||
const metaDescription =
|
||||
'Tenantial pricing is presented as conservative evaluation and rollout conversation paths, not unsupported self-serve billing.';
|
||||
import PricingPage from '@components/pages/PricingPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
customDescription={metaDescription}
|
||||
customOgTitle="Pricing | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Pricing stays scoped"
|
||||
subTitle="Tenantial does not claim self-serve billing, fixed plan entitlements, or automatic tenant connection from the public website."
|
||||
/>
|
||||
<PricingSection pricing={pricing} />
|
||||
</MainLayout>
|
||||
<PricingPage locale="de" />
|
||||
|
||||
@ -1,16 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={`Privacy | ${SITE.title}`}
|
||||
customDescription="Tenantial public website privacy notice."
|
||||
customOgTitle="Privacy | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Privacy notice"
|
||||
subTitle="This static public website describes Tenantial and its product direction. Product previews do not connect to a live tenant. Contact links may open your email client; do not send secrets, credentials, or private tenant exports."
|
||||
/>
|
||||
</MainLayout>
|
||||
<TextPage locale="de" page="privacy" />
|
||||
|
||||
@ -1,16 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
import TextPage from '@components/pages/TextPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={`Terms | ${SITE.title}`}
|
||||
customDescription="Tenantial public website terms."
|
||||
customOgTitle="Terms | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Website terms"
|
||||
subTitle="The public website describes Tenantial positioning and static product concepts. It does not provide a production service, connect to a tenant, or execute administrative actions. Commercial terms require a separate written agreement."
|
||||
/>
|
||||
</MainLayout>
|
||||
<TextPage locale="de" page="terms" />
|
||||
|
||||
@ -1,44 +1,5 @@
|
||||
---
|
||||
import MainLayout from '@/layouts/MainLayout.astro';
|
||||
import MainSection from '@components/ui/blocks/MainSection.astro';
|
||||
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
|
||||
import { SITE } from '@data/constants';
|
||||
|
||||
const pageTitle = `Trust | ${SITE.title}`;
|
||||
const metaDescription =
|
||||
'Tenantial public trust posture with conservative claims and clear static preview boundaries.';
|
||||
import TrustPage from '@components/pages/TrustPage.astro';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title={pageTitle}
|
||||
customDescription={metaDescription}
|
||||
customOgTitle="Trust | Tenantial"
|
||||
>
|
||||
<MainSection
|
||||
title="Trust starts with clear boundaries"
|
||||
subTitle="Tenantial public copy avoids customer-logo proof, third-party endorsement language, external assurance statements, service-level commitments, and recovery promises unless supplied and reviewed."
|
||||
btnExists={true}
|
||||
btnTitle="Contact Tenantial"
|
||||
btnURL="/contact"
|
||||
/>
|
||||
<FeaturesStats
|
||||
title="Public website posture"
|
||||
subTitle="The website describes a cautious product direction. It does not expose private tenant data, run tenant operations, or act as a support evidence portal."
|
||||
mainStatTitle="Static"
|
||||
mainStatSubTitle="product previews only"
|
||||
stats={[
|
||||
{
|
||||
stat: 'No',
|
||||
description: 'customer-logo proof',
|
||||
},
|
||||
{
|
||||
stat: 'No',
|
||||
description: 'external endorsement claim',
|
||||
},
|
||||
{
|
||||
stat: 'No',
|
||||
description: 'live tenant connection',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MainLayout>
|
||||
<TrustPage locale="de" />
|
||||
|
||||
@ -1,39 +1,10 @@
|
||||
const navBarLinks = [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Platform', url: '/platform' },
|
||||
{ name: 'Pricing', url: '/pricing' },
|
||||
{ name: 'Trust', url: '/trust' },
|
||||
{ name: 'Contact', url: '/contact' },
|
||||
];
|
||||
import { siteCopy } from '@data/site-copy';
|
||||
|
||||
const footerLinks = [
|
||||
{
|
||||
section: 'Product',
|
||||
links: [
|
||||
{ name: 'Platform', url: '/platform' },
|
||||
{ name: 'Pricing', url: '/pricing' },
|
||||
{ name: 'Trust', url: '/trust' },
|
||||
{ name: 'Docs', url: '/welcome-to-docs/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Legal',
|
||||
links: [
|
||||
{ name: 'Privacy', url: '/privacy' },
|
||||
{ name: 'Terms', url: '/terms' },
|
||||
{ name: 'Legal', url: '/legal' },
|
||||
{ name: 'Imprint', url: '/imprint' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const navBarLinks = siteCopy.de.nav;
|
||||
|
||||
const socialLinks = {
|
||||
facebook: '#',
|
||||
x: '#',
|
||||
github: '#',
|
||||
google: '#',
|
||||
slack: '#',
|
||||
};
|
||||
const footerLinks = siteCopy.de.footer.sections;
|
||||
|
||||
const socialLinks = {};
|
||||
|
||||
export default {
|
||||
navBarLinks,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export const languages = {
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
fr: 'Français',
|
||||
};
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expectNoHorizontalOverflow } from './smoke-helpers';
|
||||
import {
|
||||
expectCoreCapabilitiesVisible,
|
||||
expectNoHorizontalOverflow,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('mobile navigation opens with vendored foundation behavior', async ({
|
||||
page,
|
||||
@ -13,10 +16,13 @@ test('mobile navigation opens with vendored foundation behavior', async ({
|
||||
const mobilePanel = page.locator('#navbar-collapse-with-animation');
|
||||
|
||||
await expect(
|
||||
mobilePanel.getByRole('link', { name: 'Platform', exact: true }),
|
||||
mobilePanel.getByRole('link', { name: 'Plattform', exact: true })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
mobilePanel.getByRole('link', { name: 'Request walkthrough' }),
|
||||
mobilePanel.getByRole('link', { name: 'Docs', exact: true })
|
||||
).toHaveAttribute('href', '/welcome-to-docs/');
|
||||
await expect(
|
||||
mobilePanel.getByRole('link', { name: 'Walkthrough anfragen' })
|
||||
).toBeVisible();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
@ -25,10 +31,137 @@ test('theme toggle keeps page content visible', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.locator('button[aria-label="Dark Theme Toggle"]:visible').click();
|
||||
await expect(page.getByRole('heading', { name: /Evidence-first governance/ }))
|
||||
.toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: /Evidenzbasierte Governance für Microsoft-Tenant-Konfiguration/,
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await page.locator('button[aria-label="Light Theme Toggle"]:visible').click();
|
||||
await expect(page.getByRole('heading', { name: /Evidence-first governance/ }))
|
||||
.toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: /Evidenzbasierte Governance für Microsoft-Tenant-Konfiguration/,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('homepage mobile first viewport remains readable', async ({
|
||||
page,
|
||||
isMobile,
|
||||
}) => {
|
||||
test.skip(!isMobile, 'mobile viewport is covered by the mobile project');
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const heading = page.getByRole('heading', {
|
||||
name: /Evidenzbasierte Governance für Microsoft-Tenant-Konfiguration/i,
|
||||
});
|
||||
await expect(heading).toBeVisible();
|
||||
|
||||
const box = await heading.boundingBox();
|
||||
expect(box?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(360);
|
||||
await expectCoreCapabilitiesVisible(page);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test('desktop keyboard reaches navigation, CTAs, footer, and contact controls', async ({
|
||||
page,
|
||||
isMobile,
|
||||
}) => {
|
||||
test.skip(
|
||||
isMobile,
|
||||
'desktop keyboard order is covered by the desktop project'
|
||||
);
|
||||
|
||||
await page.goto('/contact');
|
||||
|
||||
const reached = new Set<string>();
|
||||
|
||||
for (let index = 0; index < 80; index += 1) {
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const active = await page.evaluate(() => {
|
||||
const element = document.activeElement as HTMLElement | null;
|
||||
|
||||
return {
|
||||
id: element?.id || '',
|
||||
href: element?.getAttribute('href') || '',
|
||||
text: element?.textContent?.replace(/\s+/g, ' ').trim() || '',
|
||||
aria: element?.getAttribute('aria-label') || '',
|
||||
tag: element?.tagName || '',
|
||||
};
|
||||
});
|
||||
|
||||
for (const value of [
|
||||
active.id,
|
||||
active.href,
|
||||
active.text,
|
||||
active.aria,
|
||||
active.tag,
|
||||
]) {
|
||||
if (value) {
|
||||
reached.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect([...reached].join('\n')).toContain('/platform');
|
||||
expect([...reached].join('\n')).toContain('/welcome-to-docs/');
|
||||
expect([...reached].join('\n')).toContain('Kontakt');
|
||||
expect([...reached].join('\n')).toContain('/privacy');
|
||||
expect([...reached].join('\n')).toContain('hs-firstname-contacts');
|
||||
expect([...reached].join('\n')).toContain('E-Mail-Entwurf vorbereiten');
|
||||
});
|
||||
|
||||
test('reduced motion keeps preview pages understandable', async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
|
||||
for (const route of ['/', '/platform', '/pricing'] as const) {
|
||||
await page.goto(route);
|
||||
|
||||
await expect(page.locator('h1').first()).toBeVisible();
|
||||
await expect(page.locator('main')).toContainText('Tenantial');
|
||||
await expectNoHorizontalOverflow(page);
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('without JavaScript', () => {
|
||||
test.use({ javaScriptEnabled: false });
|
||||
|
||||
test('primary content and links remain usable', async ({ page }) => {
|
||||
for (const route of ['/', '/platform', '/contact'] as const) {
|
||||
await page.goto(route);
|
||||
|
||||
await expect(page.locator('h1').first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('link', { name: /Kontakt|Walkthrough/i }).first()
|
||||
).toBeVisible();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('language picker switches between German default and English routes', async ({
|
||||
page,
|
||||
isMobile,
|
||||
}) => {
|
||||
test.skip(isMobile, 'desktop language picker is covered by this interaction');
|
||||
|
||||
await page.goto('/platform');
|
||||
|
||||
await page.getByLabel('Sprache wechseln').click();
|
||||
await page.getByRole('link', { name: 'English', exact: true }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/en\/platform\/?$/);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Platform review model/i })
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Sprache wechseln').click();
|
||||
await page.getByRole('link', { name: 'Deutsch', exact: true }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/platform\/?$/);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Plattform-Review-Modell/i })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@ -1,42 +1,288 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
expectNoForbiddenPublicText,
|
||||
canonicalSiteUrl,
|
||||
docsRoutes,
|
||||
expectCoreCapabilitiesVisible,
|
||||
expectMetadataForRoute,
|
||||
expectNoForbiddenPublicClaims,
|
||||
expectNoHorizontalOverflow,
|
||||
expectNoPlaceholderLinks,
|
||||
expectPublicLinksAreIntentional,
|
||||
expectSitemapExcludesRoutes,
|
||||
expectSitemapIncludesRoutes,
|
||||
readSitemapUrls,
|
||||
redirectRouteExpectations,
|
||||
redirectRoutes,
|
||||
renderedRoutes,
|
||||
} from './smoke-helpers';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { globby } from 'globby';
|
||||
|
||||
const routeMetadata = {
|
||||
'/': {
|
||||
title: /Tenantial.*Evidenzbasierte Microsoft-Tenant-Governance/i,
|
||||
description: /evidenzbasierte Governance/i,
|
||||
},
|
||||
'/platform': {
|
||||
title: /Plattform \| Tenantial/i,
|
||||
description: /öffentliches Produktmodell/i,
|
||||
},
|
||||
'/pricing': {
|
||||
title: /Preise \| Tenantial/i,
|
||||
description: /Evaluierungs.*Rollout/i,
|
||||
},
|
||||
'/contact': {
|
||||
title: /Kontakt \| Tenantial/i,
|
||||
description: /Walkthrough.*Rollout/i,
|
||||
},
|
||||
'/trust': {
|
||||
title: /Vertrauen \| Tenantial/i,
|
||||
description: /Trust-Posture.*statische.*Previews/i,
|
||||
},
|
||||
'/legal': {
|
||||
title: /Rechtliches \| Tenantial/i,
|
||||
description: /rechtliche Website-Informationen/i,
|
||||
},
|
||||
'/privacy': {
|
||||
title: /Datenschutz \| Tenantial/i,
|
||||
description: /Website-Datenschutzhinweis/i,
|
||||
},
|
||||
'/terms': {
|
||||
title: /Nutzungsbedingungen \| Tenantial/i,
|
||||
description: /Website-Nutzungsbedingungen/i,
|
||||
},
|
||||
'/imprint': {
|
||||
title: /Impressum \| Tenantial/i,
|
||||
description: /Veröffentlichungsinformationen/i,
|
||||
},
|
||||
'/welcome-to-docs/': {
|
||||
title: /Tenantial Docs/i,
|
||||
description: /Microsoft-Tenant-Governance-Modell/i,
|
||||
},
|
||||
'/guides/intro/': {
|
||||
title: /Einführung in Tenantial/i,
|
||||
description: /Microsoft-Tenant-Governance-Modell/i,
|
||||
},
|
||||
'/guides/getting-started/': {
|
||||
title: /Getting Started/i,
|
||||
description: /Tenantial Walkthrough/i,
|
||||
},
|
||||
'/guides/first-project-checklist/': {
|
||||
title: /Evaluierungscheckliste/i,
|
||||
description: /frühe Tenantial-Evaluierung/i,
|
||||
},
|
||||
'/platform/evidence-review/': {
|
||||
title: /Evidence Review/i,
|
||||
description: /Evidence-Review-Workflows/i,
|
||||
},
|
||||
'/en/': {
|
||||
title: /Tenantial.*Evidence-first Microsoft tenant governance/i,
|
||||
description:
|
||||
/evidence-first governance for Microsoft tenant configuration/i,
|
||||
},
|
||||
'/en/platform': {
|
||||
title: /Platform \| Tenantial/i,
|
||||
description: /public product model for Microsoft tenant/i,
|
||||
},
|
||||
'/en/pricing': {
|
||||
title: /Pricing \| Tenantial/i,
|
||||
description: /evaluation and rollout conversation paths/i,
|
||||
},
|
||||
'/en/contact': {
|
||||
title: /Contact \| Tenantial/i,
|
||||
description: /walkthrough.*scoped rollout conversation/i,
|
||||
},
|
||||
'/en/trust': {
|
||||
title: /Trust \| Tenantial/i,
|
||||
description: /conservative claims.*static preview boundaries/i,
|
||||
},
|
||||
'/en/legal': {
|
||||
title: /Legal \| Tenantial/i,
|
||||
description: /public website legal information/i,
|
||||
},
|
||||
'/en/privacy': {
|
||||
title: /Privacy \| Tenantial/i,
|
||||
description: /public website privacy notice/i,
|
||||
},
|
||||
'/en/terms': {
|
||||
title: /Terms \| Tenantial/i,
|
||||
description: /public website terms/i,
|
||||
},
|
||||
'/en/imprint': {
|
||||
title: /Imprint \| Tenantial/i,
|
||||
description: /public website publication information/i,
|
||||
},
|
||||
'/en/welcome-to-docs/': {
|
||||
title: /Tenantial Docs/i,
|
||||
description: /evidence-first Microsoft tenant governance model/i,
|
||||
},
|
||||
'/en/guides/intro/': {
|
||||
title: /Introduction to Tenantial/i,
|
||||
description: /evidence-first Microsoft tenant governance model/i,
|
||||
},
|
||||
'/en/guides/getting-started/': {
|
||||
title: /Getting Started/i,
|
||||
description: /Tenantial walkthrough/i,
|
||||
},
|
||||
'/en/guides/first-project-checklist/': {
|
||||
title: /Evaluation Checklist/i,
|
||||
description: /conservative checklist for early Tenantial evaluation/i,
|
||||
},
|
||||
'/en/platform/evidence-review/': {
|
||||
title: /Evidence Review/i,
|
||||
description: /Tenantial evidence review workflows/i,
|
||||
},
|
||||
} as const;
|
||||
|
||||
for (const route of renderedRoutes) {
|
||||
test(`renders intentional route ${route}`, async ({ page }) => {
|
||||
await page.goto(route);
|
||||
|
||||
await expect(page.locator('body')).toContainText('Tenantial');
|
||||
await expectNoForbiddenPublicText(page);
|
||||
await expectNoForbiddenPublicClaims(page);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const route of redirectRoutes) {
|
||||
test(`redirects ${route} intentionally`, async ({ page }) => {
|
||||
test('homepage first viewport explains core Tenantial capabilities', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
const heading = page.getByRole('heading', {
|
||||
name: /Evidenzbasierte Governance für Microsoft-Tenant-Konfiguration/i,
|
||||
});
|
||||
await expect(heading).toBeVisible();
|
||||
|
||||
const box = await heading.boundingBox();
|
||||
expect(box?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(420);
|
||||
await expect(
|
||||
page.getByRole('link', { name: /Walkthrough anfragen/i }).first()
|
||||
).toHaveAttribute('href', '/contact');
|
||||
await expect(
|
||||
page.getByRole('link', { name: /Plattform ansehen/i }).first()
|
||||
).toHaveAttribute('href', '/platform');
|
||||
await expectCoreCapabilitiesVisible(page);
|
||||
});
|
||||
|
||||
test('/platform explains the public product model without internal runtime terms', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/platform');
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Plattform-Review-Modell/i })
|
||||
).toBeVisible();
|
||||
await expectCoreCapabilitiesVisible(page);
|
||||
await expect(page.locator('body')).toContainText(/statische Demo-Previews/i);
|
||||
await expect(page.locator('body')).toContainText(
|
||||
/authentifiziert keine Besucher, liest keinen Microsoft Tenant, führt keine Operationen aus und speichert keine Tenant-Exporte/i
|
||||
);
|
||||
|
||||
const text = await page.locator('body').innerText();
|
||||
expect(text).not.toMatch(/\b(Laravel|Filament|Livewire)\b/i);
|
||||
expect(text).not.toMatch(/\bGraph runtime\b/i);
|
||||
});
|
||||
|
||||
for (const route of renderedRoutes) {
|
||||
test(`public links on ${route} resolve intentionally`, async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
await page.goto(route);
|
||||
|
||||
await expect(page).toHaveURL(/\/platform\/?$/);
|
||||
await expect(page.getByRole('heading', { name: /Platform review model/ }))
|
||||
.toBeVisible();
|
||||
await expectNoPlaceholderLinks(page);
|
||||
await expectPublicLinksAreIntentional(page, request, route);
|
||||
});
|
||||
}
|
||||
|
||||
test('built HTML emits no placeholder href values', async () => {
|
||||
const htmlFiles = await globby('dist/**/*.html', {
|
||||
absolute: true,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(htmlFiles.length).toBeGreaterThan(0);
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const html = await readFile(file, 'utf8');
|
||||
expect(html, file).not.toMatch(/href=(["'])#\1/);
|
||||
expect(html, file).not.toMatch(/href=\{["']#["']\}/);
|
||||
}
|
||||
});
|
||||
|
||||
for (const route of [
|
||||
'/trust',
|
||||
'/pricing',
|
||||
'/legal',
|
||||
'/privacy',
|
||||
'/terms',
|
||||
'/imprint',
|
||||
...docsRoutes,
|
||||
] as const) {
|
||||
test(`claim-sensitive route ${route} stays conservative`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(route);
|
||||
|
||||
await expectNoForbiddenPublicClaims(page);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [route, expected] of Object.entries(routeMetadata)) {
|
||||
test(`metadata is route-specific for ${route}`, async ({ page }) => {
|
||||
await page.goto(route);
|
||||
|
||||
await expectMetadataForRoute(page, route, expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const route of redirectRoutes) {
|
||||
test(`redirects ${route} intentionally`, async ({ page, request }) => {
|
||||
const response = await request.get(route, { maxRedirects: 0 });
|
||||
const expected = redirectRouteExpectations[route];
|
||||
|
||||
if (response.status() >= 300 && response.status() < 400) {
|
||||
expect(response.status()).toBe(expected.status);
|
||||
expect(response.headers().location).toBe(expected.target);
|
||||
} else {
|
||||
expect(response.status()).toBe(200);
|
||||
const html = await response.text();
|
||||
|
||||
expect(html).toContain('name="robots" content="noindex"');
|
||||
expect(html).toContain(
|
||||
`rel="canonical" href="${canonicalSiteUrl}${expected.target}"`
|
||||
);
|
||||
expect(html).toContain(`href="${expected.target}"`);
|
||||
}
|
||||
|
||||
await page.goto(route);
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`${expected.target}/?$`));
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: /Plattform-Review-Modell|Platform review model/,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test('robots and sitemap are available', async ({ page }) => {
|
||||
await page.goto('/robots.txt');
|
||||
await expect(page.locator('body')).toContainText('Sitemap:');
|
||||
await expect(page.locator('body')).toContainText(
|
||||
`Sitemap: ${canonicalSiteUrl}/sitemap-index.xml`
|
||||
);
|
||||
|
||||
await page.goto('/sitemap-index.xml');
|
||||
await expect(page.locator('body')).toContainText('sitemap-0.xml');
|
||||
|
||||
await page.goto('/sitemap-0.xml');
|
||||
const sitemap = await page.locator('body').innerText();
|
||||
|
||||
for (const route of redirectRoutes) {
|
||||
expect(sitemap).not.toContain(`https://tenantial.com${route}/`);
|
||||
}
|
||||
});
|
||||
|
||||
test('sitemap exposes canonical routes and excludes redirect aliases', async ({
|
||||
page,
|
||||
}) => {
|
||||
const sitemapUrls = await readSitemapUrls(page);
|
||||
|
||||
expectSitemapIncludesRoutes(sitemapUrls, renderedRoutes);
|
||||
expectSitemapExcludesRoutes(sitemapUrls, redirectRoutes);
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import { expect, type APIRequestContext, type Page } from '@playwright/test';
|
||||
|
||||
export const renderedRoutes = [
|
||||
'/',
|
||||
@ -11,39 +11,142 @@ export const renderedRoutes = [
|
||||
'/terms',
|
||||
'/imprint',
|
||||
'/welcome-to-docs/',
|
||||
'/guides/intro/',
|
||||
'/guides/getting-started/',
|
||||
'/guides/first-project-checklist/',
|
||||
'/platform/evidence-review/',
|
||||
'/en/',
|
||||
'/en/platform',
|
||||
'/en/pricing',
|
||||
'/en/contact',
|
||||
'/en/trust',
|
||||
'/en/legal',
|
||||
'/en/privacy',
|
||||
'/en/terms',
|
||||
'/en/imprint',
|
||||
'/en/welcome-to-docs/',
|
||||
'/en/guides/intro/',
|
||||
'/en/guides/getting-started/',
|
||||
'/en/guides/first-project-checklist/',
|
||||
'/en/platform/evidence-review/',
|
||||
] as const;
|
||||
|
||||
export const redirectRoutes = [
|
||||
'/product',
|
||||
'/products',
|
||||
'/services',
|
||||
'/blog',
|
||||
'/insights',
|
||||
export const docsRoutes = [
|
||||
'/welcome-to-docs/',
|
||||
'/guides/intro/',
|
||||
'/guides/getting-started/',
|
||||
'/guides/first-project-checklist/',
|
||||
'/platform/evidence-review/',
|
||||
'/en/welcome-to-docs/',
|
||||
'/en/guides/intro/',
|
||||
'/en/guides/getting-started/',
|
||||
'/en/guides/first-project-checklist/',
|
||||
'/en/platform/evidence-review/',
|
||||
] as const;
|
||||
|
||||
const forbiddenPublicText = [
|
||||
'ScrewFast',
|
||||
'construction',
|
||||
'hardware',
|
||||
'template',
|
||||
'open-source',
|
||||
'TenantAtlas',
|
||||
'TenantPilot',
|
||||
'TenantCTRL',
|
||||
'trusted by',
|
||||
'SOC 2',
|
||||
'ISO 27001',
|
||||
'99.9%',
|
||||
'Microsoft endorsed',
|
||||
'guaranteed recovery',
|
||||
'guaranteed compliance',
|
||||
export const redirectRouteExpectations = {
|
||||
'/product': { status: 301, target: '/platform' },
|
||||
'/products': { status: 301, target: '/platform' },
|
||||
'/services': { status: 301, target: '/platform' },
|
||||
'/blog': { status: 302, target: '/platform' },
|
||||
'/insights': { status: 302, target: '/platform' },
|
||||
'/en/product': { status: 301, target: '/en/platform' },
|
||||
'/en/products': { status: 301, target: '/en/platform' },
|
||||
'/en/services': { status: 301, target: '/en/platform' },
|
||||
'/en/blog': { status: 302, target: '/en/platform' },
|
||||
'/en/insights': { status: 302, target: '/en/platform' },
|
||||
} as const;
|
||||
|
||||
export const redirectRoutes = Object.keys(redirectRouteExpectations) as Array<
|
||||
keyof typeof redirectRouteExpectations
|
||||
>;
|
||||
|
||||
export const staticRoutes = [
|
||||
'/robots.txt',
|
||||
'/sitemap-index.xml',
|
||||
'/manifest.json',
|
||||
'/favicon.ico',
|
||||
] as const;
|
||||
|
||||
export const canonicalSiteUrl = 'https://tenantial.com';
|
||||
|
||||
const forbiddenPublicPatterns = [
|
||||
{ label: 'ScrewFast residue', pattern: /ScrewFast/i },
|
||||
{ label: 'construction residue', pattern: /\bconstruction\b/i },
|
||||
{ label: 'hardware residue', pattern: /\bhardware\b/i },
|
||||
{ label: 'template residue', pattern: /\btemplate\b/i },
|
||||
{ label: 'open-source residue', pattern: /open-source/i },
|
||||
{ label: 'TenantAtlas residue', pattern: /TenantAtlas/i },
|
||||
{ label: 'TenantPilot residue', pattern: /TenantPilot/i },
|
||||
{ label: 'TenantCTRL residue', pattern: /TenantCTRL/i },
|
||||
{ label: 'fake social proof', pattern: /trusted by/i },
|
||||
{ label: 'SOC 2 claim', pattern: /SOC\s*2/i },
|
||||
{ label: 'ISO 27001 claim', pattern: /ISO\s*27001/i },
|
||||
{ label: 'uptime claim', pattern: /99\.9%|guaranteed uptime/i },
|
||||
{ label: 'Microsoft endorsement', pattern: /Microsoft endorsed/i },
|
||||
{ label: 'Microsoft certification claim', pattern: /Microsoft certified/i },
|
||||
{ label: 'recovery guarantee', pattern: /guaranteed recovery/i },
|
||||
{ label: 'compliance guarantee', pattern: /guaranteed compliance/i },
|
||||
{ label: 'fake checkout CTA', pattern: /\b(buy now|checkout)\b/i },
|
||||
{
|
||||
label: 'fake trial CTA',
|
||||
pattern: /\b(start free trial|create account)\b/i,
|
||||
},
|
||||
{ label: 'fake app access CTA', pattern: /\b(sign in|log in|login)\b/i },
|
||||
];
|
||||
|
||||
const metadataSelectors = [
|
||||
'title',
|
||||
'meta[name="description"]',
|
||||
'meta[property="og:title"]',
|
||||
'meta[property="og:description"]',
|
||||
'meta[property="og:url"]',
|
||||
'meta[property="og:image"]',
|
||||
'meta[property="twitter:url"]',
|
||||
'meta[name="twitter:title"]',
|
||||
'meta[name="twitter:description"]',
|
||||
'meta[name="twitter:image"]',
|
||||
].join(',');
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
if (path === '') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return path === '/' || path.includes('.') || path.endsWith('/')
|
||||
? path
|
||||
: `${path}/`;
|
||||
}
|
||||
|
||||
function normalizeRouteForSitemap(route: string): string {
|
||||
return route === '/' ? '/' : normalizePath(route);
|
||||
}
|
||||
|
||||
function routeToCanonicalPath(route: string): string {
|
||||
return route === '' ? '/' : route;
|
||||
}
|
||||
|
||||
export async function expectNoForbiddenPublicText(page: Page): Promise<void> {
|
||||
const text = await page.locator('body').innerText();
|
||||
|
||||
for (const term of forbiddenPublicText) {
|
||||
expect(text.toLowerCase()).not.toContain(term.toLowerCase());
|
||||
for (const { label, pattern } of forbiddenPublicPatterns) {
|
||||
expect(text, label).not.toMatch(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectNoForbiddenPublicClaims(page: Page): Promise<void> {
|
||||
await expectNoForbiddenPublicText(page);
|
||||
|
||||
const metadataText = await page
|
||||
.locator(metadataSelectors)
|
||||
.evaluateAll(nodes =>
|
||||
nodes
|
||||
.map(node => node.textContent || node.getAttribute('content') || '')
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
for (const { label, pattern } of forbiddenPublicPatterns) {
|
||||
expect(metadataText, `${label} in metadata`).not.toMatch(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,3 +158,164 @@ export async function expectNoHorizontalOverflow(page: Page): Promise<void> {
|
||||
|
||||
expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1);
|
||||
}
|
||||
|
||||
export async function expectCoreCapabilitiesVisible(page: Page): Promise<void> {
|
||||
const text = (await page.locator('body').innerText()).toLowerCase();
|
||||
|
||||
for (const terms of [
|
||||
['backup'],
|
||||
['restore'],
|
||||
['drift detection', 'drift'],
|
||||
['findings'],
|
||||
['evidence'],
|
||||
['auditability'],
|
||||
['exceptions'],
|
||||
['reviews', 'review'],
|
||||
]) {
|
||||
expect(
|
||||
terms.some(term => text.includes(term)),
|
||||
`missing capability term: ${terms.join(' or ')}`
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectNoPlaceholderLinks(page: Page): Promise<void> {
|
||||
const placeholders = await page
|
||||
.locator('a[href="#"], a[href=""], area[href="#"], area[href=""]')
|
||||
.evaluateAll(nodes =>
|
||||
nodes.map(node => ({
|
||||
text: node.textContent?.trim() || '',
|
||||
href: node.getAttribute('href'),
|
||||
}))
|
||||
);
|
||||
|
||||
expect(placeholders).toEqual([]);
|
||||
}
|
||||
|
||||
export async function expectPublicLinksAreIntentional(
|
||||
page: Page,
|
||||
request: APIRequestContext,
|
||||
sourceRoute: string
|
||||
): Promise<void> {
|
||||
const allowedRoutes = new Set<string>(
|
||||
[
|
||||
...renderedRoutes,
|
||||
...redirectRoutes,
|
||||
...staticRoutes,
|
||||
'/sitemap-0.xml',
|
||||
].map(route => normalizePath(route))
|
||||
);
|
||||
|
||||
const hrefs = await page
|
||||
.locator('a[href], area[href]')
|
||||
.evaluateAll(nodes =>
|
||||
nodes
|
||||
.map(node => node.getAttribute('href')?.trim())
|
||||
.filter((href): href is string => Boolean(href))
|
||||
);
|
||||
|
||||
for (const href of hrefs) {
|
||||
expect(href, `placeholder link on ${sourceRoute}`).not.toBe('#');
|
||||
|
||||
if (
|
||||
href.startsWith('mailto:') ||
|
||||
href.startsWith('tel:') ||
|
||||
href.startsWith('javascript:')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = new URL(href, canonicalSiteUrl);
|
||||
|
||||
if (url.origin !== canonicalSiteUrl) {
|
||||
expect(['http:', 'https:']).toContain(url.protocol);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePath(url.pathname);
|
||||
expect(
|
||||
allowedRoutes.has(normalizedPath),
|
||||
`unexpected public link ${href} on ${sourceRoute}`
|
||||
).toBe(true);
|
||||
|
||||
if (url.hash && normalizePath(sourceRoute) === normalizedPath) {
|
||||
const targetId = url.hash.slice(1);
|
||||
await expect(
|
||||
page.locator(`#${targetId}`),
|
||||
`missing anchor target ${url.hash} on ${sourceRoute}`
|
||||
).toHaveCount(1);
|
||||
}
|
||||
|
||||
if (!url.hash || normalizePath(sourceRoute) !== normalizedPath) {
|
||||
const response = await request.get(url.pathname);
|
||||
expect(
|
||||
response.status(),
|
||||
`unexpected status for linked route ${href} on ${sourceRoute}`
|
||||
).toBeLessThan(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectMetadataForRoute(
|
||||
page: Page,
|
||||
route: string,
|
||||
expected: { title: RegExp; description: RegExp }
|
||||
): Promise<void> {
|
||||
await expect(page).toHaveTitle(expected.title);
|
||||
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
|
||||
'content',
|
||||
expected.description
|
||||
);
|
||||
|
||||
const canonicalUrl = `${canonicalSiteUrl}${routeToCanonicalPath(route)}`;
|
||||
|
||||
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
||||
'href',
|
||||
canonicalUrl
|
||||
);
|
||||
await expect(page.locator('meta[property="og:url"]').last()).toHaveAttribute(
|
||||
'content',
|
||||
canonicalUrl
|
||||
);
|
||||
await expect(
|
||||
page
|
||||
.locator('meta[property="twitter:url"], meta[name="twitter:url"]')
|
||||
.last()
|
||||
).toHaveAttribute('content', canonicalUrl);
|
||||
await expect(
|
||||
page.locator('meta[property="og:image"]').last()
|
||||
).toHaveAttribute('content', /\/_astro\/social\.[^/]+\.png$/);
|
||||
await expect(
|
||||
page.locator('meta[name="twitter:image"]').last()
|
||||
).toHaveAttribute('content', /\/_astro\/social\.[^/]+\.png$/);
|
||||
}
|
||||
|
||||
export function extractSitemapUrls(xml: string): string[] {
|
||||
return Array.from(xml.matchAll(/<loc>([^<]+)<\/loc>/g)).map(([, url]) => url);
|
||||
}
|
||||
|
||||
export async function readSitemapUrls(page: Page): Promise<string[]> {
|
||||
await page.goto('/sitemap-0.xml');
|
||||
|
||||
return extractSitemapUrls(await page.content());
|
||||
}
|
||||
|
||||
export function expectSitemapIncludesRoutes(
|
||||
urls: string[],
|
||||
routes: readonly string[]
|
||||
): void {
|
||||
for (const route of routes) {
|
||||
const url = `${canonicalSiteUrl}${normalizeRouteForSitemap(route)}`;
|
||||
expect(urls, `sitemap missing ${url}`).toContain(url);
|
||||
}
|
||||
}
|
||||
|
||||
export function expectSitemapExcludesRoutes(
|
||||
urls: string[],
|
||||
routes: readonly string[]
|
||||
): void {
|
||||
for (const route of routes) {
|
||||
const url = `${canonicalSiteUrl}${normalizeRouteForSitemap(route)}`;
|
||||
expect(urls, `sitemap should exclude ${url}`).not.toContain(url);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
# Public Site Contract: Spec 403 Launch Readiness
|
||||
|
||||
This feature has no REST, GraphQL, Laravel, Filament, Livewire, Microsoft Graph, database, queue, job, policy, RBAC, or product runtime API contract.
|
||||
|
||||
The contract is the public static website behavior that reviewers and smoke tests must verify.
|
||||
|
||||
## Rendered Canonical Routes
|
||||
|
||||
The following German default routes must render intentionally and expose Tenantial-specific page content and metadata:
|
||||
|
||||
- `/`
|
||||
- `/platform`
|
||||
- `/pricing`
|
||||
- `/contact`
|
||||
- `/trust`
|
||||
- `/legal`
|
||||
- `/privacy`
|
||||
- `/terms`
|
||||
- `/imprint`
|
||||
- `/welcome-to-docs/`
|
||||
- `/guides/intro/`
|
||||
- `/guides/getting-started/`
|
||||
- `/guides/first-project-checklist/`
|
||||
- `/platform/evidence-review/`
|
||||
|
||||
The following English route mirrors must render intentionally under `/en/...`:
|
||||
|
||||
- `/en/`
|
||||
- `/en/platform`
|
||||
- `/en/pricing`
|
||||
- `/en/contact`
|
||||
- `/en/trust`
|
||||
- `/en/legal`
|
||||
- `/en/privacy`
|
||||
- `/en/terms`
|
||||
- `/en/imprint`
|
||||
- `/en/welcome-to-docs/`
|
||||
- `/en/guides/intro/`
|
||||
- `/en/guides/getting-started/`
|
||||
- `/en/guides/first-project-checklist/`
|
||||
- `/en/platform/evidence-review/`
|
||||
|
||||
## Redirect-Only Aliases
|
||||
|
||||
The following aliases must redirect intentionally to `/platform` and must not appear as canonical sitemap entries:
|
||||
|
||||
- `/product`
|
||||
- `/products`
|
||||
- `/services`
|
||||
- `/blog`
|
||||
- `/insights`
|
||||
|
||||
The English aliases must redirect intentionally to `/en/platform` and must not appear as canonical sitemap entries:
|
||||
|
||||
- `/en/product`
|
||||
- `/en/products`
|
||||
- `/en/services`
|
||||
- `/en/blog`
|
||||
- `/en/insights`
|
||||
|
||||
Current source behavior may use permanent or temporary redirects, but launch review must document which aliases are retained and why.
|
||||
|
||||
Current planning baseline:
|
||||
|
||||
- `/product`, `/products`, and `/services` use 301 redirects.
|
||||
- `/blog` and `/insights` use 302 redirects.
|
||||
|
||||
## Static Generated Routes
|
||||
|
||||
The following static outputs must be available after build:
|
||||
|
||||
- `/robots.txt`
|
||||
- `/sitemap-index.xml`
|
||||
- generated sitemap child files such as `/sitemap-0.xml`
|
||||
- `/manifest.json`
|
||||
- `/favicon.ico`
|
||||
|
||||
`robots.txt` must point to `/sitemap-index.xml` for the configured public site URL.
|
||||
|
||||
## Metadata Contract
|
||||
|
||||
Each rendered canonical route must provide:
|
||||
|
||||
- Tenantial-specific page title
|
||||
- Tenantial-specific page description
|
||||
- route-appropriate canonical, Open Graph, and Twitter URL values
|
||||
- German default and English alternate `hreflang` links, plus `x-default` pointing at German
|
||||
- locale-appropriate Open Graph locale values
|
||||
- existing social image URL and MIME/type metadata
|
||||
- no ScrewFast, construction/hardware, template, TenantAtlas, TenantPilot, or TenantCTRL residue in public metadata
|
||||
- no unsupported proof claims in title, description, Open Graph, Twitter, schema, or docs metadata
|
||||
|
||||
## CTA Contract
|
||||
|
||||
Every public CTA, navigation item, footer link, docs link, and visible form-like control must satisfy:
|
||||
|
||||
- resolves to an intentional route, anchor, static asset, or legitimate external URL
|
||||
- no public `href="#"` placeholders
|
||||
- demo/contact paths route to `/contact` or an intentional contact section
|
||||
- localized public links remain in the current locale where a localized route exists
|
||||
- no login, account creation, billing, checkout, backend submission, guaranteed provisioning, or app access implication unless implemented
|
||||
- pricing CTAs remain contact/demo-oriented
|
||||
- static form controls must not imply backend submission, guaranteed response, account creation, provisioning, or subscription workflow
|
||||
- footer email/newsletter controls must be removed or reframed if no newsletter workflow exists
|
||||
|
||||
## Trust And Pricing Claim Contract
|
||||
|
||||
Public copy and metadata must avoid unsupported:
|
||||
|
||||
- SOC 2, ISO, or other certification claims
|
||||
- Microsoft endorsement claims
|
||||
- compliance guarantees
|
||||
- recovery guarantees
|
||||
- uptime guarantees
|
||||
- fake customer logos
|
||||
- fake testimonials
|
||||
- fake "trusted by" claims
|
||||
- self-serve billing, checkout, subscription, or entitlement claims
|
||||
- production legal details left as explicit replacement placeholders
|
||||
|
||||
## Product Preview Contract
|
||||
|
||||
Static product previews must:
|
||||
|
||||
- be framed as illustrative, static, or demo content
|
||||
- avoid implying live tenant data
|
||||
- avoid internal Laravel/Filament implementation details
|
||||
- avoid turning visual labels into product runtime taxonomy
|
||||
- communicate meaning through text or labels, not color alone
|
||||
|
||||
## Browser And Accessibility Contract
|
||||
|
||||
Validated public routes must:
|
||||
|
||||
- avoid body-level horizontal overflow on representative desktop and mobile viewports
|
||||
- keep primary navigation, CTAs, footer links, and visible controls keyboard reachable
|
||||
- show visible focus states for interactive elements
|
||||
- remain understandable with reduced motion
|
||||
- keep primary content and links usable if JavaScript fails where reasonably possible for a static marketing site
|
||||
|
||||
## Launch Review Risk Register
|
||||
|
||||
Implementation should explicitly resolve or document these current source risks:
|
||||
|
||||
- Starlight child docs routes are public and indexed but not fully covered by current smoke route lists.
|
||||
- Main-layout `og:url` and `twitter:url` are root-oriented rather than route-specific.
|
||||
- Social image metadata may reference a missing or type-mismatched asset.
|
||||
- `/imprint` contains placeholder production details.
|
||||
- Contact form copy may imply a backend workflow that does not exist.
|
||||
- Footer `Subscribe` wording may imply a newsletter workflow that does not exist.
|
||||
- Source placeholder social links with `#` must not be emitted publicly.
|
||||
|
||||
## Scope Contract
|
||||
|
||||
Implementation and validation must not touch:
|
||||
|
||||
- `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/platform`
|
||||
- Laravel providers, migrations, policies, jobs, queues, database, Filament resources, Livewire components, Blade views, tenant/workspace/RBAC code, Microsoft Graph code, or AuditLog behavior
|
||||
|
||||
Scope proof command:
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && git status --short -- apps/platform
|
||||
```
|
||||
114
specs/403-public-website-launch-readiness/data-model.md
Normal file
114
specs/403-public-website-launch-readiness/data-model.md
Normal file
@ -0,0 +1,114 @@
|
||||
# Data Model: `apps/website` Public Website Launch Readiness
|
||||
|
||||
This feature introduces no database tables, product runtime models, persisted entities, enums, status families, provider contracts, or shared product taxonomy.
|
||||
|
||||
The records below are planning and validation concepts for static website artifacts only.
|
||||
|
||||
## Public Route
|
||||
|
||||
Represents an intentional public URL exposed by the static website.
|
||||
|
||||
**Fields**
|
||||
|
||||
- `path`: public URL path, such as `/`, `/platform`, or `/welcome-to-docs/`
|
||||
- `routeKind`: `rendered`, `redirect`, `static`, or `docs`
|
||||
- `canonicalTarget`: canonical page path when applicable
|
||||
- `sitemapPolicy`: included or excluded from generated sitemap
|
||||
- `navigationPolicy`: primary nav, footer nav, docs nav, hidden, or redirect-only
|
||||
- `metadataRequired`: whether route-specific title and description must be reviewed
|
||||
- `claimReviewRequired`: whether visible copy and metadata require proof-safety review
|
||||
|
||||
**Validation Rules**
|
||||
|
||||
- Rendered canonical routes must return successful HTML.
|
||||
- Redirect-only aliases must resolve intentionally and be excluded from sitemap output.
|
||||
- Static routes such as `/robots.txt` and `/sitemap-index.xml` must be available after build.
|
||||
- Public route copy and metadata must not contain forbidden residue terms listed in Spec 403.
|
||||
|
||||
## CTA Link
|
||||
|
||||
Represents a public call-to-action, navigation item, footer link, docs link, or visible interactive route target.
|
||||
|
||||
**Fields**
|
||||
|
||||
- `label`: visible CTA or link text
|
||||
- `sourceSurface`: page or component where the CTA appears
|
||||
- `href`: route, anchor, or external URL target
|
||||
- `targetType`: rendered route, anchor, static asset, external proof link, or redirect alias
|
||||
- `supportedWorkflow`: contact/demo, product explanation, pricing review, trust/legal review, docs review, or navigation
|
||||
- `claimRisk`: whether the CTA could imply login, billing, checkout, provisioning, account creation, backend submission, or unsupported workflow
|
||||
|
||||
**Validation Rules**
|
||||
|
||||
- Public CTAs must resolve to intentional routes or anchors.
|
||||
- Public CTAs must not use `href="#"` placeholders.
|
||||
- Demo/contact CTAs must lead to `/contact` or an intentional contact section.
|
||||
- CTAs must not imply login, account creation, billing, checkout, backend submission, or automated onboarding unless implemented.
|
||||
|
||||
## Public Claim
|
||||
|
||||
Represents visible copy or metadata that could create a trust, compliance, product, pricing, or proof claim.
|
||||
|
||||
**Fields**
|
||||
|
||||
- `surface`: route, docs page, component, metadata source, or generated HTML surface
|
||||
- `claimText`: reviewed copy or metadata phrase
|
||||
- `claimCategory`: product positioning, trust, pricing, legal, preview framing, social proof, or deployment
|
||||
- `proofStatus`: supplied proof, conservative placeholder, unsupported, or remove
|
||||
- `forbiddenResidueCheck`: whether the phrase contains forbidden public residue terms
|
||||
|
||||
**Validation Rules**
|
||||
|
||||
- No unsupported SOC 2, ISO, Microsoft endorsement, uptime, recovery, compliance, or customer adoption claims.
|
||||
- No fake customer logos, fake testimonials, or fake "trusted by" claims.
|
||||
- Pricing copy must avoid self-serve subscription, billing, checkout, and entitlement claims unless workflow exists.
|
||||
- Metadata must stay Tenantial-specific and route-appropriate.
|
||||
|
||||
## Static Product Preview
|
||||
|
||||
Represents product-like static/demo content shown on public pages.
|
||||
|
||||
**Fields**
|
||||
|
||||
- `surface`: route/component where preview appears
|
||||
- `previewName`: human-readable preview name
|
||||
- `framingCopy`: text that identifies the preview as static, illustrative, or demo
|
||||
- `capabilityShown`: backup, restore, drift detection, findings, evidence, auditability, exceptions, reviews, or related product positioning
|
||||
- `dataProvenance`: static/demo only
|
||||
- `colorMeaningSupport`: text/icon/label support so meaning does not rely on color alone
|
||||
|
||||
**Validation Rules**
|
||||
|
||||
- Preview content must not imply live tenant data.
|
||||
- Preview status-like values must not become shared product taxonomy.
|
||||
- Preview meaning must not rely on color alone.
|
||||
- Preview wording must avoid internal Laravel/Filament implementation details.
|
||||
|
||||
## Launch Readiness Note
|
||||
|
||||
Represents the implementation handoff artifact required by FR-034.
|
||||
|
||||
**Fields**
|
||||
|
||||
- `routeExposure`: canonical routes, docs routes, static routes, redirect aliases, and hidden routes
|
||||
- `ctaAssumptions`: supported public CTA paths and intentionally absent workflows
|
||||
- `trustPricingPosture`: conservative claim posture and unsupported claims avoided
|
||||
- `sitemapBehavior`: sitemap inclusion/exclusion and robots entrypoint
|
||||
- `deploymentExpectations`: build, smoke, static output, and deploy notes
|
||||
- `platformScopeCheck`: recorded confirmation that `apps/platform` remains untouched
|
||||
|
||||
**Validation Rules**
|
||||
|
||||
- Must be stored inside the feature's website-local implementation or release handoff path.
|
||||
- Must not introduce product runtime truth.
|
||||
- Must document the exact validation commands reviewers should run.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A `Public Route` may contain many `CTA Link` and `Public Claim` review points.
|
||||
- A `Static Product Preview` belongs to one public route or docs route.
|
||||
- The `Launch Readiness Note` summarizes all reviewed public routes, CTAs, claims, sitemap behavior, and deployment expectations.
|
||||
|
||||
## State Transitions
|
||||
|
||||
No product state transitions are introduced. Review progress may be tracked in tasks or checklists during implementation, but it must not become a product runtime status family.
|
||||
127
specs/403-public-website-launch-readiness/launch-readiness.md
Normal file
127
specs/403-public-website-launch-readiness/launch-readiness.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Launch Readiness Handoff: Spec 403
|
||||
|
||||
## Scope
|
||||
|
||||
Runtime changes are limited to `apps/website`. Planning and handoff artifacts live in `specs/403-public-website-launch-readiness`.
|
||||
|
||||
`apps/platform` remains out of scope and was checked with:
|
||||
|
||||
```bash
|
||||
git status --short -- apps/platform
|
||||
```
|
||||
|
||||
The command printed no output.
|
||||
|
||||
## Route Exposure
|
||||
|
||||
Canonical rendered German default routes:
|
||||
|
||||
- `/`
|
||||
- `/platform`
|
||||
- `/pricing`
|
||||
- `/contact`
|
||||
- `/trust`
|
||||
- `/legal`
|
||||
- `/privacy`
|
||||
- `/terms`
|
||||
- `/imprint`
|
||||
- `/welcome-to-docs/`
|
||||
- `/guides/intro/`
|
||||
- `/guides/getting-started/`
|
||||
- `/guides/first-project-checklist/`
|
||||
- `/platform/evidence-review/`
|
||||
|
||||
Canonical rendered English routes:
|
||||
|
||||
- `/en/`
|
||||
- `/en/platform`
|
||||
- `/en/pricing`
|
||||
- `/en/contact`
|
||||
- `/en/trust`
|
||||
- `/en/legal`
|
||||
- `/en/privacy`
|
||||
- `/en/terms`
|
||||
- `/en/imprint`
|
||||
- `/en/welcome-to-docs/`
|
||||
- `/en/guides/intro/`
|
||||
- `/en/guides/getting-started/`
|
||||
- `/en/guides/first-project-checklist/`
|
||||
- `/en/platform/evidence-review/`
|
||||
|
||||
Redirect-only aliases:
|
||||
|
||||
- `/product` -> `/platform`
|
||||
- `/products` -> `/platform`
|
||||
- `/services` -> `/platform`
|
||||
- `/blog` -> `/platform`
|
||||
- `/insights` -> `/platform`
|
||||
- `/en/product` -> `/en/platform`
|
||||
- `/en/products` -> `/en/platform`
|
||||
- `/en/services` -> `/en/platform`
|
||||
- `/en/blog` -> `/en/platform`
|
||||
- `/en/insights` -> `/en/platform`
|
||||
|
||||
Static Astro output renders redirect aliases as noindex redirect HTML pages. Smoke coverage verifies the noindex marker, canonical target, and visible redirect link.
|
||||
|
||||
Top navigation exposes `/welcome-to-docs/` as `Docs`, matching the public docs route instead of keeping documentation discoverable only through footer or contact-page links.
|
||||
|
||||
German remains the unprefixed default locale. English is available as an additional locale under `/en/...`; the language picker switches between the current German route and its English counterpart where available.
|
||||
|
||||
## CTA Assumptions
|
||||
|
||||
Supported public CTA paths are contact/demo and product-evaluation paths only.
|
||||
|
||||
- Homepage CTAs route to `/contact` and `/platform`.
|
||||
- Platform CTAs route to `/contact` and `/trust`.
|
||||
- Pricing CTAs route to `/contact`.
|
||||
- Docs links route to intentional localized docs pages.
|
||||
- Footer contact CTA routes to `/contact`.
|
||||
|
||||
The public website does not claim login, account creation, self-service payment, automated provisioning, live tenant connection, or backend form submission.
|
||||
|
||||
## Trust And Pricing Posture
|
||||
|
||||
Public copy remains conservative:
|
||||
|
||||
- no customer-logo proof
|
||||
- no testimonials or third-party endorsement claims
|
||||
- no external assurance or certification claims
|
||||
- no uptime, recovery, or compliance guarantees
|
||||
- no fixed package access or instant payment claims
|
||||
- no live tenant data in static previews
|
||||
|
||||
Static product previews are framed as static/demo/illustrative content and do not imply live Microsoft tenant access from the public website.
|
||||
|
||||
## Sitemap And Robots
|
||||
|
||||
`robots.txt` points to:
|
||||
|
||||
```text
|
||||
https://tenantial.com/sitemap-index.xml
|
||||
```
|
||||
|
||||
Generated sitemap coverage includes canonical rendered German and English routes and excludes redirect-only aliases. Route-specific metadata checks cover title, description, canonical URL, alternate language links, Open Graph URL, Open Graph locale, Twitter URL, and generated PNG social image metadata.
|
||||
|
||||
## Deployment Expectations
|
||||
|
||||
Reviewer validation commands:
|
||||
|
||||
```bash
|
||||
corepack pnpm build:website
|
||||
WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke
|
||||
git diff --check
|
||||
git status --short -- apps/platform
|
||||
! rg "href=\"#\"|href=\\{\"#\"\\}|href=\\{'#'\\}" apps/website/src
|
||||
```
|
||||
|
||||
The build produces static output in `apps/website/dist`. Deploy the website artifact only; no Laravel, Filament, database, queue, Sail, Microsoft Graph, or `apps/platform` deployment step is required for this feature.
|
||||
|
||||
## Review Evidence
|
||||
|
||||
- Website build passed with existing Astro hints only.
|
||||
- Public routes smoke suite passed: 230 passed.
|
||||
- Interaction smoke suite passed: 10 passed, 4 intentionally skipped.
|
||||
- Integrated browser review opened `http://127.0.0.1:4321/` with German default navigation and the language menu showing `Deutsch` and `English`.
|
||||
- `git diff --check` passed.
|
||||
- Source placeholder href scan passed.
|
||||
- `apps/platform` scope check passed.
|
||||
220
specs/403-public-website-launch-readiness/plan.md
Normal file
220
specs/403-public-website-launch-readiness/plan.md
Normal file
@ -0,0 +1,220 @@
|
||||
# Implementation Plan: `apps/website` Public Website Launch Readiness
|
||||
|
||||
**Branch**: `feat/403-public-website-launch-readiness` | **Date**: 2026-05-21 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/spec.md`
|
||||
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Make the existing Spec 402 ScrewFast-derived Tenantial public website launch-ready without rebuilding the foundation or touching `apps/platform`. The implementation will stay inside `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website` and focus on German-default plus English-prefixed public copy, CTA targets, route exposure, metadata, sitemap/robots behavior, conservative trust/pricing wording, static preview framing, and browser/static validation.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0
|
||||
**Primary Dependencies**: Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwright
|
||||
**Storage**: N/A - static website content and generated build output only; no database or product persistence
|
||||
**Testing**: Astro check/build plus Playwright smoke tests under `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke`
|
||||
**Validation Lanes**: website build, public smoke, manual browser review, whitespace check, `apps/platform` scope check
|
||||
**Target Platform**: Static Astro public website deployed from `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website`, with public site URL `https://tenantial.com`
|
||||
**Project Type**: Web - standalone Astro public website inside monorepo
|
||||
**Performance Goals**: No body-level horizontal overflow on validated desktop/mobile routes; static pages remain usable when JavaScript fails where reasonably possible
|
||||
**Constraints**: Runtime/source changes are scoped to `apps/website`; Spec Kit artifacts may live under `specs/403-public-website-launch-readiness`; preserve Spec 402 ScrewFast-derived substrate; no backend contact/demo workflow; no authentication, billing, Microsoft Graph, Laravel, Filament, Livewire, database, queue, job, provider, policy, RBAC, or `apps/platform` changes
|
||||
**Scale/Scope**: Public launch-readiness pass for canonical German default routes, English `/en/...` route mirrors, redirect aliases, docs routes, static metadata assets, navigation, footer CTAs, pricing/trust/legal/contact content, and Playwright smoke coverage
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: no admin/operator-facing product surface change; public website launch-readiness workflow only
|
||||
- **Native vs custom classification summary**: ScrewFast-derived Astro website; no Filament/Blade/admin surface
|
||||
- **Shared-family relevance**: none for Laravel/Filament shared interaction families
|
||||
- **State layers in scope**: localized static page content, public route metadata, navigation/footer link data, Starlight docs configuration, Playwright smoke expectations
|
||||
- **Audience modes in scope**: public visitor, buyer/reviewer, site owner/reviewer; no authenticated operator/MSP/support-platform modes
|
||||
- **Decision/diagnostic/raw hierarchy plan**: public copy must be decision-first for visitors; diagnostics are limited to launch-readiness notes and tests, not visible runtime UI
|
||||
- **Raw/support gating plan**: N/A - no raw/support product evidence surface
|
||||
- **One-primary-action / duplicate-truth control**: each public page should keep a clear demo/contact path and avoid competing fake login, billing, checkout, or account CTAs
|
||||
- **Handling modes by drift class or surface**: public route/copy/CTA drift is review-mandatory inside this feature; platform/admin drift is a hard stop
|
||||
- **Repository-signal treatment**: `apps/platform` changes are a hard stop; generated website build artifacts are reviewed only if they become tracked changes
|
||||
- **Special surface test profiles**: N/A - not a Filament surface
|
||||
- **Required tests or manual smoke**: public smoke plus manual browser review across light/dark/mobile/desktop and keyboard reachability
|
||||
- **Exception path and spread control**: none
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: no
|
||||
- **Systems touched**: `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website` public website only
|
||||
- **Shared abstractions reused**: existing Spec 402 ScrewFast-derived Astro structure, page components, Starlight docs, sitemap integration, and Playwright smoke helper patterns
|
||||
- **New abstraction introduced? why?**: none
|
||||
- **Why the existing abstraction was sufficient or insufficient**: The website already has route, layout, navigation, metadata, docs, and smoke-test structure. Launch readiness can be implemented by tightening those local artifacts rather than adding runtime abstractions.
|
||||
- **Bounded deviation / spread control**: none
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no
|
||||
- **Central contract reused**: N/A
|
||||
- **Delegated UX behaviors**: N/A
|
||||
- **Surface-owned behavior kept local**: N/A
|
||||
- **Queued DB-notification policy**: N/A
|
||||
- **Terminal notification path**: N/A
|
||||
- **Exception path**: none
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Provider-owned seams**: N/A
|
||||
- **Platform-core seams**: N/A
|
||||
- **Neutral platform terms / contracts preserved**: Public product language may refer to Microsoft tenant configuration as positioning only; no product runtime seam changes.
|
||||
- **Retained provider-specific semantics and why**: Microsoft tenant wording remains because Tenantial's public positioning is currently Microsoft tenant governance.
|
||||
- **Bounded extraction or follow-up path**: none
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first: PASS - no inventory, snapshot, or backup runtime behavior is changed.
|
||||
- Read/write separation: PASS - no product write/change behavior is introduced.
|
||||
- Graph contract path: PASS - no Microsoft Graph calls or contract registry changes.
|
||||
- Deterministic capabilities: PASS - no capability resolver or capability derivation changes.
|
||||
- RBAC-UX: PASS - no `/admin`, `/system`, tenant context, workspace context, capability, policy, or authorization behavior changes.
|
||||
- Workspace isolation: PASS - no workspace data or routes.
|
||||
- RBAC-UX destructive-like actions: PASS - no destructive actions.
|
||||
- RBAC-UX global search: PASS - no Filament resources or global search changes.
|
||||
- Tenant isolation: PASS - no tenant data, tenant routes, or cross-tenant views.
|
||||
- Run observability: PASS - no long-running, remote, queued, scheduled, or security-relevant DB action.
|
||||
- OperationRun start UX: PASS - no OperationRun behavior.
|
||||
- Ops-UX 3-surface feedback: PASS - no OperationRun notifications.
|
||||
- Ops-UX lifecycle: PASS - no OperationRun status/outcome transitions.
|
||||
- Ops-UX summary counts: PASS - no summary counts.
|
||||
- Ops-UX guards: PASS - no Ops-UX guard changes.
|
||||
- Ops-UX system runs: PASS - no system runs.
|
||||
- Automation: PASS - no queued/scheduled operations.
|
||||
- Data minimization: PASS - public copy and static preview content only; no secrets or tokens.
|
||||
- Test governance (TEST-GOV-001): PASS - Browser/static website classification, explicit website smoke lane, no hidden Laravel/Filament/provider/database setup.
|
||||
- Proportionality (PROP-001): PASS - local website hardening, no new product runtime structure.
|
||||
- No premature abstraction (ABSTR-001): PASS - no new factories, registries, resolvers, strategies, interfaces, or frameworks.
|
||||
- Persisted truth (PERSIST-001): PASS - no persisted product truth.
|
||||
- Behavioral state (STATE-001): PASS - no status/state/reason family.
|
||||
- UI semantics (UI-SEM-001): PASS - static presentation remains local and does not become product taxonomy.
|
||||
- Shared pattern first (XCUT-001): PASS - no shared operator interaction family touched.
|
||||
- Provider boundary (PROV-001): PASS - public Microsoft wording only; no shared provider/platform seam changes.
|
||||
- V1 explicitness / few layers (V1-EXP-001, LAYER-001): PASS - direct website-local edits only.
|
||||
- Spec discipline / bloat check (SPEC-DISC-001, BLOAT-001): PASS - no new enum, DTO, presenter, persisted entity, interface, registry, resolver, taxonomy, or cross-domain framework.
|
||||
- Badge semantics (BADGE-001): PASS - no product badge semantics; preview labels must not become shared status truth.
|
||||
- Filament-native UI (UI-FIL-001): PASS - no Filament UI.
|
||||
- UI/UX surface taxonomy: PASS - no operator-facing surface.
|
||||
- Decision-first operating model: PASS - public website copy is visitor-decision oriented; no operator surface.
|
||||
- Audience-aware disclosure: PASS - no raw/support product detail surface.
|
||||
- UI/UX inspect model: PASS - no list/detail operator surface.
|
||||
- UI/UX action hierarchy: PASS - no Filament actions.
|
||||
- UI/UX scope, truth, and naming: PASS - no admin scope labels; public copy must avoid implementation-first wording.
|
||||
- UI/UX placeholder ban: PASS - no Filament action groups.
|
||||
- UI naming: PASS - no operator-facing action labels, run titles, notifications, or audit prose.
|
||||
- Operator surfaces: PASS - no `/admin` surface.
|
||||
- Filament UI Action Surface Contract: PASS - no Filament Resource/RelationManager/Page.
|
||||
- Filament UI UX-001: PASS - no Filament screen.
|
||||
- Action-surface discipline: PASS - no operator action surface.
|
||||
- UI review workflow: PASS - plan carries N/A decisions forward and keeps `apps/platform` as hard stop.
|
||||
|
||||
**Initial Gate Result**: PASS - no constitution violations or unresolved clarifications.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Browser/static website
|
||||
- **Affected validation lanes**: website build, public smoke, manual browser review, whitespace/scope checks
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The feature changes public static pages, route/metadata exposure, CTAs, and browser rendering. Laravel/Pest/Filament lanes would not prove the changed behavior.
|
||||
- **Narrowest proving command(s)**: `cd /Users/ahmeddarrazi/Documents/projects/wt-website && corepack pnpm build:website`; `cd /Users/ahmeddarrazi/Documents/projects/wt-website && WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`; `cd /Users/ahmeddarrazi/Documents/projects/wt-website && git diff --check`; `cd /Users/ahmeddarrazi/Documents/projects/wt-website && git status --short -- apps/platform`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: none - no database, provider, workspace, membership, session, queue, Sail, Laravel, Filament, or Livewire setup
|
||||
- **Expensive defaults or shared helper growth introduced?**: no
|
||||
- **Heavy-family additions, promotions, or visibility changes**: Browser review remains explicit in website smoke/manual review; no heavy-governance lane
|
||||
- **Surface-class relief / special coverage rule**: N/A - public website
|
||||
- **Closing validation and reviewer handoff**: Reviewers should rely on website build, Playwright smoke, manual light/dark/mobile/desktop checks, no horizontal overflow, no dead CTAs/placeholders, sitemap/robots inspection, and `apps/platform` untouched status.
|
||||
- **Budget / baseline / trend follow-up**: none expected
|
||||
- **Review-stop questions**: lane fit, browser breadth, hidden platform cost, CTA honesty, unsupported claim posture, route exposure
|
||||
- **Escalation path**: document-in-feature
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **Why no dedicated follow-up spec is needed**: The browser/static review cost is local to this launch-readiness pass unless repeated website release gates become a recurring process problem.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── public-site-contract.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
├── playwright.config.ts
|
||||
├── process-html.mjs
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── content/
|
||||
│ ├── data_files/
|
||||
│ ├── images/
|
||||
│ ├── layouts/
|
||||
│ ├── pages/
|
||||
│ └── utils/
|
||||
└── tests/
|
||||
└── smoke/
|
||||
```
|
||||
|
||||
**Structure Decision**: Use the existing `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website` Astro app from Spec 402. Do not create new base source folders and do not touch `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/platform`.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| None | N/A | N/A |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Public launch review lacks a single bounded plan for route exposure, copy clarity, CTA honesty, metadata, claim safety, and browser checks after the foundation rebuild.
|
||||
- **Existing structure is insufficient because**: Spec 402 established the Astro/ScrewFast substrate but did not complete a release-readiness pass.
|
||||
- **Narrowest correct implementation**: Website-local copy, route, metadata, CTA, smoke, and launch-note work inside `apps/website`.
|
||||
- **Ownership cost created**: Maintenance of website-local public copy, smoke expectations, and launch-readiness note.
|
||||
- **Alternative intentionally rejected**: Rebuilding the foundation, adding backend contact/account workflows, or introducing product runtime abstractions.
|
||||
- **Release truth**: Current-release public website readiness.
|
||||
|
||||
## Phase 0 Research
|
||||
|
||||
Research output is captured in `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/research.md`.
|
||||
|
||||
**Resolved clarifications**:
|
||||
|
||||
- The feature uses the existing Astro 6 website and does not need a new framework decision.
|
||||
- Public routes and redirect behavior are defined by `apps/website/src/pages`, `apps/website/astro.config.mjs`, and current Playwright smoke helpers.
|
||||
- Starlight child docs routes are publicly emitted and must be included in launch review, not only the docs root.
|
||||
- Current launch risks to resolve include route-specific social metadata, social image metadata consistency, `/imprint` placeholder details, static contact form wording, footer subscription wording, source `href="#"` placeholders, and the German-default/English-prefixed language route model.
|
||||
- German is the default locale at unprefixed public routes; English is available under `/en/...`.
|
||||
- There are no API endpoints or data entities to contract; route, CTA, metadata, and launch-check contracts are sufficient.
|
||||
- Validation stays in website build and Playwright smoke lanes.
|
||||
|
||||
## Phase 1 Design
|
||||
|
||||
Design output is captured in:
|
||||
|
||||
- `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/data-model.md`
|
||||
- `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/contracts/public-site-contract.md`
|
||||
- `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/quickstart.md`
|
||||
|
||||
No REST, GraphQL, database, Laravel, Filament, Livewire, Microsoft Graph, queue, job, policy, RBAC, or product runtime contracts are introduced.
|
||||
|
||||
## Post-Design Constitution Check
|
||||
|
||||
**Post-Design Gate Result**: PASS
|
||||
|
||||
- The Phase 1 design remains website-local.
|
||||
- All clarification markers are resolved.
|
||||
- No product persistence, abstractions, state families, provider/platform seams, OperationRun behavior, RBAC behavior, Filament behavior, or Graph calls are introduced.
|
||||
- Browser/static validation remains explicit and bounded to `apps/website`.
|
||||
- `apps/platform` remains out of scope and must be verified by `git status --short -- apps/platform`.
|
||||
119
specs/403-public-website-launch-readiness/quickstart.md
Normal file
119
specs/403-public-website-launch-readiness/quickstart.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Quickstart: Spec 403 Public Website Launch Readiness
|
||||
|
||||
## Scope
|
||||
|
||||
Work only in:
|
||||
|
||||
```bash
|
||||
/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website
|
||||
/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness
|
||||
```
|
||||
|
||||
Do not touch:
|
||||
|
||||
```bash
|
||||
/Users/ahmeddarrazi/Documents/projects/wt-website/apps/platform
|
||||
```
|
||||
|
||||
## Install Dependencies If Needed
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && corepack pnpm install
|
||||
```
|
||||
|
||||
## Run The Website Locally
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && WEBSITE_PORT=4321 corepack pnpm dev:website
|
||||
```
|
||||
|
||||
Open:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:4321
|
||||
```
|
||||
|
||||
## Build Validation
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && corepack pnpm build:website
|
||||
```
|
||||
|
||||
## Smoke Validation
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke
|
||||
```
|
||||
|
||||
## Static Git Validation
|
||||
|
||||
```bash
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && git diff --check
|
||||
cd /Users/ahmeddarrazi/Documents/projects/wt-website && git status --short -- apps/platform
|
||||
```
|
||||
|
||||
The `apps/platform` command must print no output.
|
||||
|
||||
## Manual Browser Review
|
||||
|
||||
Review these routes in desktop and mobile widths, light and dark modes:
|
||||
|
||||
- `/`
|
||||
- `/platform`
|
||||
- `/pricing`
|
||||
- `/contact`
|
||||
- `/trust`
|
||||
- `/legal`
|
||||
- `/privacy`
|
||||
- `/terms`
|
||||
- `/imprint`
|
||||
- `/welcome-to-docs/`
|
||||
- `/guides/intro/`
|
||||
- `/guides/getting-started/`
|
||||
- `/guides/first-project-checklist/`
|
||||
- `/platform/evidence-review/`
|
||||
|
||||
Check:
|
||||
|
||||
- homepage explains Tenantial within the first viewport
|
||||
- `/platform` explains the public product model, not the internal Laravel/Filament app
|
||||
- all CTAs resolve to intentional routes or anchors
|
||||
- no `href="#"` placeholders are visible or emitted
|
||||
- contact form and footer copy do not imply backend submission or newsletter subscription workflows that do not exist
|
||||
- `/imprint` contains no production replacement placeholder
|
||||
- pricing remains contact/demo oriented
|
||||
- trust and legal pages avoid unsupported certification, endorsement, uptime, recovery, compliance, or customer adoption claims
|
||||
- static product previews are framed as illustrative/static/demo content
|
||||
- keyboard users can reach navigation, CTAs, footer links, and visible controls
|
||||
- visible focus states are present
|
||||
- no body-level horizontal overflow occurs
|
||||
- reduced-motion mode leaves content understandable
|
||||
|
||||
## Sitemap And Robots Review
|
||||
|
||||
After build, inspect:
|
||||
|
||||
```text
|
||||
/robots.txt
|
||||
/sitemap-index.xml
|
||||
/sitemap-0.xml
|
||||
```
|
||||
|
||||
Confirm:
|
||||
|
||||
- `robots.txt` points to `/sitemap-index.xml`
|
||||
- canonical rendered routes are included
|
||||
- redirect-only aliases are excluded
|
||||
- metadata is Tenantial-specific and route-appropriate
|
||||
- Open Graph/Twitter URLs and social image metadata are route-appropriate and point to existing assets
|
||||
|
||||
## Launch Readiness Handoff
|
||||
|
||||
Implementation must add or update a launch-readiness note documenting:
|
||||
|
||||
- route exposure
|
||||
- CTA assumptions
|
||||
- trust/pricing claim posture
|
||||
- sitemap behavior
|
||||
- deployment expectations
|
||||
- `apps/platform` untouched confirmation
|
||||
72
specs/403-public-website-launch-readiness/research.md
Normal file
72
specs/403-public-website-launch-readiness/research.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Phase 0 Research: `apps/website` Public Website Launch Readiness
|
||||
|
||||
## Decision: Keep the Spec 402 Astro/ScrewFast-derived foundation
|
||||
|
||||
**Rationale**: The active website is a standalone Astro app at `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website` with Starlight docs, sitemap generation, Tailwind v4, Preline, Lenis, and Playwright smoke tests already in place. Spec 403 is a launch-readiness pass, not another rebuild.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Rebuild the website foundation again: rejected by scope.
|
||||
- Introduce a new design system or app shell: rejected because Spec 402 already selected the substrate.
|
||||
- Move launch checks into the Laravel platform: rejected because `apps/platform` is out of scope.
|
||||
|
||||
## Decision: Treat route, CTA, metadata, and sitemap behavior as the public contract
|
||||
|
||||
**Rationale**: This feature has no API endpoints or product runtime entities. Public launch behavior is expressed through static pages, redirect aliases, generated metadata, navigation/footer links, docs exposure, `robots.txt`, and sitemap output.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Generate OpenAPI or GraphQL contracts: rejected because the public website exposes no API for this feature.
|
||||
- Model route readiness as product runtime state: rejected because it would create unnecessary taxonomy/persistence.
|
||||
|
||||
## Decision: Use conservative public copy and proof-safe claim checks
|
||||
|
||||
**Rationale**: The spec assumes no verified customer logos, testimonials, certifications, Microsoft endorsements, uptime guarantees, compliance guarantees, recovery guarantees, self-serve billing, or account provisioning workflow. Copy and metadata must therefore stay contact/demo-oriented and avoid unsupported proof claims.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Use aspirational enterprise proof language: rejected because it creates public trust risk.
|
||||
- Keep placeholder social proof from the substrate: rejected unless hidden, removed, or rewritten as neutral content.
|
||||
|
||||
## Decision: Keep static product previews illustrative
|
||||
|
||||
**Rationale**: Product-like preview content can help visitors understand Tenantial, but it must not imply live Microsoft tenant data, a running Graph integration on the website, or shared product status taxonomy.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Present previews as live tenant output: rejected because the public website has no authenticated tenant runtime.
|
||||
- Remove all previews: rejected because static/demo previews are useful and allowed when clearly framed.
|
||||
|
||||
## Decision: Validate with website build, Playwright smoke, manual browser review, and scope checks
|
||||
|
||||
**Rationale**: The current package scripts support `corepack pnpm build:website` and `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`. Playwright is already configured for desktop and mobile projects and starts the Astro preview server. These lanes prove the public static website without pulling in Laravel runtime cost.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Run Laravel/Pest/Filament tests: rejected because this feature does not touch `apps/platform`.
|
||||
- Rely only on static text review: rejected because launch readiness requires browser/mobile/light/dark/overflow/CTA validation.
|
||||
|
||||
## Decision: Use a website-local launch-readiness note as the implementation handoff artifact
|
||||
|
||||
**Rationale**: FR-034 requires a launch-readiness note documenting route exposure, CTA assumptions, trust/pricing claim posture, sitemap behavior, and deployment expectations. This is a website release artifact, not product persistence.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Store launch readiness in database tables: rejected as out of scope and unnecessary.
|
||||
- Skip the note and rely on PR comments: rejected because reviewers need a durable handoff artifact.
|
||||
|
||||
## Current Source Observations
|
||||
|
||||
- Rendered smoke routes currently include `/`, `/platform`, `/pricing`, `/contact`, `/trust`, `/legal`, `/privacy`, `/terms`, `/imprint`, and `/welcome-to-docs/`.
|
||||
- Public Starlight docs routes also include `/guides/intro/`, `/guides/getting-started/`, `/guides/first-project-checklist/`, and `/platform/evidence-review/`; current smoke coverage only lists the docs root.
|
||||
- Redirect smoke routes currently include `/product`, `/products`, `/services`, `/blog`, and `/insights`, all resolving to `/platform`.
|
||||
- `/product`, `/products`, and `/services` currently use 301 redirects; `/blog` and `/insights` currently use 302 redirects. The launch note should confirm this split is intentional.
|
||||
- `astro.config.mjs` filters redirect-only paths out of the generated sitemap.
|
||||
- `robots.txt` points to the generated sitemap index through `import.meta.env.SITE`.
|
||||
- `apps/website/src/utils/navigation.ts` still contains `socialLinks` with `#` placeholder targets; implementation should remove, hide, or replace public placeholders if they are emitted.
|
||||
- Existing smoke helpers already check forbidden public terms and horizontal overflow; Spec 403 may need broader CTA, metadata, docs, sitemap, and keyboard coverage.
|
||||
- Main-layout canonical URLs are page-specific, but current social metadata uses the site root for `og:url` and `twitter:url` on non-doc pages; launch implementation should make route metadata consistent.
|
||||
- Current social image metadata has launch-review risk: main-layout image type is hardcoded as PNG while generated social output may be WebP, and Starlight docs metadata references `https://tenantial.com/social.webp` without an obvious matching public file.
|
||||
- `/imprint` contains placeholder production details that must be replaced or made explicitly conservative before launch.
|
||||
- The contact page includes a static form and `Prepare Email` submit button without a backend workflow; copy/behavior must not imply a guaranteed submission workflow.
|
||||
- The footer includes an email field and `Subscribe` link to `/contact`; this should be reframed or removed to avoid implying a newsletter workflow.
|
||||
421
specs/403-public-website-launch-readiness/spec.md
Normal file
421
specs/403-public-website-launch-readiness/spec.md
Normal file
@ -0,0 +1,421 @@
|
||||
# Feature Specification: `apps/website` Public Website Launch Readiness
|
||||
|
||||
**Feature Branch**: `feat/403-public-website-launch-readiness`
|
||||
**Created**: 2026-05-20
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Make the current Tenantial public website release-ready after the Spec 402 ScrewFast-derived rebuild, scoped strictly to `apps/website`."
|
||||
|
||||
## Spec Candidate Check
|
||||
|
||||
- **Problem**: The public website now has the correct foundation, but it still needs a focused launch-readiness pass for message clarity, CTA integrity, route exposure, SEO metadata, trust/pricing claim safety, and browser review.
|
||||
- **Today's failure**: A technically passing static build could still publish unclear positioning, unsupported claims, dead or misleading CTAs, route residue, or metadata that is not ready for public release.
|
||||
- **User-visible improvement**: Visitors can quickly understand Tenantial in German by default or English under `/en`, follow honest contact/demo paths, review conservative trust and pricing language, and browse only intentional public routes.
|
||||
- **Smallest enterprise-capable version**: Keep the Spec 402 ScrewFast-derived foundation, change only website-local copy, metadata, route exposure, CTA behavior, public assets, smoke checks, and launch-readiness notes.
|
||||
- **Explicit non-goals**: No rebuild, no new design system, no backend contact workflow, no authentication, no billing, no Microsoft Graph calls, no Laravel/Filament/Livewire/admin behavior, and no `apps/platform` changes.
|
||||
- **Permanent complexity imported**: Website-local launch copy, route/metadata hygiene, static preview framing, browser/static validation expectations, and a launch-readiness note. No new product runtime abstractions or persistence.
|
||||
- **Why now**: Spec 402 intentionally solved the foundation; launch readiness is the next release gate before public exposure.
|
||||
- **Why not local**: This is local to `apps/website`; the spec explicitly rejects broader platform/runtime changes.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: None. This is not a new foundation, framework, status taxonomy, persistence layer, or abstraction.
|
||||
- **Score**: Nutzen 2 | Dringlichkeit 2 | Scope 2 | Komplexitaet 2 | Produktnaehe 2 | Wiederverwendung 1 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields
|
||||
|
||||
- **Scope**: N/A - public website only, strictly `apps/website`
|
||||
- **Primary Routes**: `/`, `/platform`, `/pricing`, `/trust`, `/legal`, `/privacy`, `/terms`, `/imprint`, `/contact`, docs routes, `/en/*` localized route mirrors, redirect aliases, `/robots.txt`, `/sitemap-index.xml`
|
||||
- **Data Ownership**: No workspace-owned or tenant-owned product records impacted; no persistence changes.
|
||||
- **RBAC**: N/A - no authenticated/admin/platform runtime behavior, no tenant/workspace membership behavior, no authorization changes.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: N/A - no canonical admin view or tenant context.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: N/A - no tenant data or authenticated runtime.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse
|
||||
|
||||
- **Cross-cutting feature?**: no
|
||||
- **Interaction class(es)**: N/A - public website navigation and CTAs only; no shared operator interaction family.
|
||||
- **Systems touched**: `apps/website` static/public routes, metadata, navigation, footer, docs/legal/trust/pricing/contact surfaces, smoke checks.
|
||||
- **Existing pattern(s) to extend**: Spec 402 ScrewFast-derived website structure.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: N/A - no Laravel/Filament shared operator contract.
|
||||
- **Why the existing shared path is sufficient or insufficient**: The website-local ScrewFast-derived structure is sufficient; this spec only hardens launch readiness.
|
||||
- **Allowed deviation and why**: none
|
||||
- **Consistency impact**: Public copy, CTAs, metadata, route exposure, sitemap, and browser review must stay aligned across the website.
|
||||
- **Review focus**: Verify no `apps/platform` dependency or admin/runtime shared pattern is introduced.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no
|
||||
- **Shared OperationRun UX contract/layer reused**: N/A
|
||||
- **Delegated start/completion UX behaviors**: N/A
|
||||
- **Local surface-owned behavior that remains**: N/A
|
||||
- **Queued DB-notification policy**: N/A
|
||||
- **Terminal notification path**: N/A
|
||||
- **Exception required?**: none
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Boundary classification**: N/A
|
||||
- **Seams affected**: N/A
|
||||
- **Neutral platform terms preserved or introduced**: Public copy may mention Microsoft tenant configuration as product positioning only.
|
||||
- **Provider-specific semantics retained and why**: Bounded public positioning for Microsoft tenant configuration; no runtime contracts, models, Graph calls, or platform-core taxonomy changes.
|
||||
- **Why this does not deepen provider coupling accidentally**: The feature changes only static public website artifacts and does not alter product runtime boundaries.
|
||||
- **Follow-up path**: none
|
||||
|
||||
## UI / Surface Guardrail Impact
|
||||
|
||||
`N/A - no admin/operator-facing product surface change. This spec changes public marketing website surfaces only.`
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Public website launch-readiness pass | no | ScrewFast-derived Astro website | none | static pages, route metadata, navigation, footer | no | Public website only; no Laravel/Filament/Livewire surface |
|
||||
|
||||
## Decision-First Surface Role
|
||||
|
||||
N/A - no admin/operator-facing product surface is added or changed.
|
||||
|
||||
## Audience-Aware Disclosure
|
||||
|
||||
N/A - no authenticated detail/status surface is added or changed. Static product previews must remain clearly illustrative/static/demo content.
|
||||
|
||||
## UI/UX Surface Classification
|
||||
|
||||
N/A - no Filament/operator list, detail, queue, audit, config, or report surface is added or changed.
|
||||
|
||||
## Operator Surface Contract
|
||||
|
||||
N/A - no admin/operator-facing product page is added or materially refactored.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no product runtime persistence. A website-local launch-readiness note may be added as a release handoff artifact.
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: Public launch review currently lacks a bounded, reviewable checklist for messaging, CTAs, claims, metadata, routes, and responsive behavior after the website foundation rebuild.
|
||||
- **Existing structure is insufficient because**: Spec 402 established the substrate; it did not exhaustively certify public release quality or claim posture.
|
||||
- **Narrowest correct implementation**: Website-local edits plus static/browser validation, no platform/runtime changes.
|
||||
- **Ownership cost**: Maintaining website copy, metadata, smoke coverage, and the launch-readiness note.
|
||||
- **Alternative intentionally rejected**: Rebuilding the website foundation again or introducing backend workflows for contact/demo/account behavior.
|
||||
- **Release truth**: Current-release launch readiness for the public website.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment for the public website. It introduces no product runtime compatibility, migration, legacy alias, or data-shape requirement.
|
||||
|
||||
## Testing / Lane / Runtime Impact
|
||||
|
||||
- **Test purpose / classification**: Browser/static website
|
||||
- **Validation lane(s)**: website build, public smoke, browser review, whitespace/scope checks
|
||||
- **Why this classification and these lanes are sufficient**: The feature changes static public website behavior, metadata, route exposure, CTAs, and responsive rendering rather than Laravel runtime behavior.
|
||||
- **New or expanded test families**: May update website smoke checks if route/CTA/sitemap validation gaps are found.
|
||||
- **Fixture / helper cost impact**: none expected; no provider, workspace, membership, database, session, Filament, Livewire, or Laravel setup.
|
||||
- **Heavy-family visibility / justification**: Browser review is explicit and limited to public website pages.
|
||||
- **Special surface test profile**: N/A - public website static/browser validation.
|
||||
- **Standard-native relief or required special coverage**: N/A - no Filament surface.
|
||||
- **Reviewer handoff**: Confirm lane fit, no hidden platform runtime cost, and exact validation commands listed in this spec.
|
||||
- **Budget / baseline / trend impact**: none expected.
|
||||
- **Escalation needed**: document-in-feature
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **Planned validation commands**:
|
||||
- `corepack pnpm build:website`
|
||||
- `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
- `git diff --check`
|
||||
- `git status --short -- apps/platform`
|
||||
|
||||
## Status
|
||||
|
||||
Draft
|
||||
|
||||
## Scope
|
||||
|
||||
Runtime/source implementation for this spec is strictly local to `apps/website`.
|
||||
|
||||
Spec Kit artifacts for planning, tasks, analysis, and launch handoff may live under `specs/403-public-website-launch-readiness/`.
|
||||
|
||||
It builds on Spec 402, where `apps/website` was replaced with a Tenantial-branded public website derived from the pinned ScrewFast foundation.
|
||||
|
||||
This spec does not rebuild the website foundation again. It does not replace the ScrewFast substrate. It focuses on launch-readiness of public copy, navigation, CTA flows, metadata, route exposure, trust wording, and browser review.
|
||||
|
||||
## Important Scope Clarification
|
||||
|
||||
In this feature, the word "platform" refers only to the public website route `/platform` inside `apps/website`.
|
||||
|
||||
It does not refer to the Laravel/Filament application in `apps/platform`.
|
||||
|
||||
`apps/platform` is completely out of scope:
|
||||
|
||||
- do not modify it
|
||||
- do not import from it
|
||||
- do not run platform-specific migrations, tests, providers, resources, policies, Livewire, Filament, Blade, jobs, queues, database, tenant/workspace, RBAC, or Microsoft Graph code
|
||||
- only verify through `git diff` or `git status` that it remains untouched
|
||||
|
||||
Planning artifacts and the launch-readiness handoff note are allowed under `specs/403-public-website-launch-readiness/`; they must not introduce platform runtime behavior.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the current Tenantial public website release-ready after the Spec 402 ScrewFast-derived rebuild.
|
||||
|
||||
The website should clearly communicate Tenantial's public product positioning, expose only intentional routes, avoid unsupported public claims, provide coherent CTAs, and pass manual/browser/static launch checks.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This spec does not:
|
||||
|
||||
- recreate `apps/website`
|
||||
- replace the ScrewFast-derived foundation
|
||||
- redesign the website from scratch
|
||||
- remove unused vendored ScrewFast substrate files unless they are publicly emitted or create release risk
|
||||
- add backend contact/demo workflow
|
||||
- add authentication, login, customer portal, billing, subscription, or app access behavior
|
||||
- introduce Microsoft Graph calls
|
||||
- introduce platform/admin runtime behavior
|
||||
- touch `apps/platform`
|
||||
- change Filament, Livewire, Laravel, Blade, providers, policies, migrations, jobs, queues, database, tenant isolation, workspace behavior, RBAC, or AuditLog behavior
|
||||
|
||||
## Problem
|
||||
|
||||
Spec 402 established the correct website substrate and public route model.
|
||||
|
||||
However, a foundation replacement is not the same as launch readiness. Before release, the public website needs a focused pass over:
|
||||
|
||||
1. message clarity
|
||||
2. CTA consistency
|
||||
3. SEO metadata
|
||||
4. route exposure
|
||||
5. trust and pricing safety
|
||||
6. docs/legal/contact completeness
|
||||
7. mobile and keyboard usability
|
||||
8. public claim discipline
|
||||
9. deployment/reviewer handoff
|
||||
|
||||
Without this pass, the website may technically build and smoke-test while still being unclear, under-polished, incorrectly localized, or risky from a public-claims perspective.
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - Understand Tenantial Quickly (Priority: P1)
|
||||
|
||||
A first-time visitor opens the homepage and understands within one minute that Tenantial is an evidence-first governance product for Microsoft tenant configuration, with emphasis on backup, restore, drift detection, findings, evidence, auditability, exceptions, and reviews.
|
||||
|
||||
**Why this priority**: The homepage is the main conversion and positioning surface. If the message is unclear, the website is not launch-ready even if the build passes.
|
||||
|
||||
**Independent Test**: Open `/` in desktop and mobile widths and verify that the hero, section sequence, CTAs, and FAQ communicate the product category and core value without requiring platform knowledge.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a visitor opens `/`, **When** the hero loads, **Then** Tenantial's product category and primary value proposition are visible without scrolling.
|
||||
2. **Given** a visitor scans the homepage, **When** they move through sections, **Then** they can identify at least three core capabilities.
|
||||
3. **Given** a visitor reaches the final CTA, **When** they decide what to do next, **Then** the CTA path is clear and does not imply unsupported login, billing, or backend workflow behavior.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Evaluate The Public Product Route (Priority: P1)
|
||||
|
||||
A buyer opens `/platform` and understands Tenantial's public product model without confusing it with the internal Laravel/Filament app.
|
||||
|
||||
**Why this priority**: The `/platform` page is the primary product explanation route. It must explain the product clearly while staying within public website scope.
|
||||
|
||||
**Independent Test**: Open `/platform` and verify that the page explains the public product model, not internal implementation details.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a visitor opens `/platform`, **When** the page loads, **Then** the page explains backup, restore, drift detection, findings, evidence, audit trail, exceptions, and reviews.
|
||||
2. **Given** the page uses product-like previews, **When** they are reviewed, **Then** they are clearly static/demo previews and do not imply live tenant data.
|
||||
3. **Given** the page references Microsoft tenants, **When** copy is reviewed, **Then** the language remains public positioning and does not imply a runtime Microsoft Graph integration on the website.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Follow A Safe CTA Path (Priority: P2)
|
||||
|
||||
A visitor who wants to evaluate Tenantial can follow a clear contact/demo path without encountering fake forms, dead links, fake account access, or unsupported self-serve purchase claims.
|
||||
|
||||
**Why this priority**: CTA integrity is essential for trust. Public CTAs must match what the website can actually support.
|
||||
|
||||
**Independent Test**: Click primary and secondary CTAs from homepage, `/platform`, `/pricing`, `/trust`, docs, and footer. Verify that each CTA leads to an intentional route or anchor.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a visitor clicks a demo/contact CTA, **When** the link resolves, **Then** it leads to `/contact` or an intentional page section.
|
||||
2. **Given** a visitor sees pricing, **When** they review package language, **Then** the page avoids unsupported billing, checkout, plan entitlement, or self-serve subscription claims.
|
||||
3. **Given** no backend form workflow exists, **When** a contact surface renders, **Then** it does not imply a guaranteed workflow, automated provisioning, or account creation.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Trust The Claims (Priority: P2)
|
||||
|
||||
A security-conscious buyer reads trust, legal, pricing, and product pages and sees conservative, proof-safe wording.
|
||||
|
||||
**Why this priority**: Tenantial is an enterprise governance product. Unsupported claims about compliance, certifications, Microsoft endorsement, uptime, recovery guarantees, or customer adoption would be risky.
|
||||
|
||||
**Independent Test**: Review visible copy and metadata on `/`, `/platform`, `/pricing`, `/trust`, `/legal`, `/privacy`, `/terms`, `/imprint`, and docs routes.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** no verified certifications are supplied, **When** trust pages render, **Then** they do not claim SOC 2, ISO, Microsoft endorsement, guaranteed compliance, guaranteed uptime, or guaranteed recovery.
|
||||
2. **Given** no customer proof is supplied, **When** social-proof areas render, **Then** they do not show fake customer logos, fake testimonials, or "trusted by" claims.
|
||||
3. **Given** static product previews are shown, **When** reviewed, **Then** they are framed as illustrative/static/demo content rather than live tenant output.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - Review Launch Metadata And Route Exposure (Priority: P3)
|
||||
|
||||
A site owner can review the public website output and confirm routes, metadata, sitemap, robots, and redirects are intentional.
|
||||
|
||||
**Why this priority**: Launch readiness includes not only visual pages, but also search engine exposure, route hygiene, and public metadata.
|
||||
|
||||
**Independent Test**: Run static route and metadata checks against the built website output.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the website builds, **When** sitemap output is inspected, **Then** only canonical renderable routes are included.
|
||||
2. **Given** redirect aliases exist, **When** sitemap output is inspected, **Then** redirect-only routes are excluded.
|
||||
3. **Given** `robots.txt` is opened, **When** reviewed, **Then** it points to the intended sitemap index.
|
||||
4. **Given** public pages are opened, **When** metadata is reviewed, **Then** titles and descriptions are Tenantial-specific and route-appropriate.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- If a route is retained from ScrewFast but not ready for Tenantial content, it must be hidden from navigation, redirected intentionally, or given restrained placeholder content.
|
||||
- If docs pages are exposed, they must not imply product behavior that does not exist yet.
|
||||
- If pricing is not final, pricing must remain conservative and contact/demo-oriented.
|
||||
- If trust/legal wording is not legally reviewed, pages must use restrained placeholder language.
|
||||
- If a CTA cannot be backed by a real workflow, it must link to `/contact` or be removed.
|
||||
- If product previews include status-like values, they must not become shared product taxonomy or imply runtime truth.
|
||||
- If visual assets are retained from the vendored ScrewFast source but unused, they are not release blockers unless they are imported, emitted, linked, or visible.
|
||||
- If localized or editorial routes are not ready, they must not appear in primary navigation.
|
||||
- If localization is enabled, German must remain the unprefixed default and English must use `/en/...` route prefixes.
|
||||
- If reduced-motion is enabled, content must remain understandable without animation.
|
||||
- If JavaScript fails, primary content and links must remain usable where reasonably possible for a static marketing site.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 402 is already implemented and validated.
|
||||
- `apps/website` is now ScrewFast-derived and Tenantial-branded.
|
||||
- `apps/platform` is untouched and remains out of scope.
|
||||
- No verified customer logos, testimonials, certifications, Microsoft endorsements, uptime guarantees, compliance guarantees, or recovery guarantees have been supplied.
|
||||
- No self-serve billing or account provisioning workflow is available from the public website.
|
||||
- The public domain is `tenantial.com`.
|
||||
- The generated sitemap entrypoint is `/sitemap-index.xml`.
|
||||
- German is the default public website locale at unprefixed routes.
|
||||
- English is an additional public locale under `/en/...`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Scope And Architecture
|
||||
|
||||
- **FR-001**: Runtime/source implementation MUST remain strictly scoped to `apps/website`; Spec Kit artifacts and launch handoff notes MAY live under `specs/403-public-website-launch-readiness/`.
|
||||
- **FR-002**: The implementation MUST NOT touch `apps/platform`.
|
||||
- **FR-003**: The implementation MUST NOT replace or rebuild the ScrewFast-derived foundation again.
|
||||
- **FR-004**: Changes SHOULD focus on copy, metadata, route exposure, CTA behavior, public assets, and launch documentation.
|
||||
- **FR-005**: Unused vendored ScrewFast substrate files MAY remain if they are not publicly emitted or referenced.
|
||||
|
||||
### Public Positioning
|
||||
|
||||
- **FR-006**: The homepage MUST clearly position Tenantial as evidence-first governance for Microsoft tenant configuration.
|
||||
- **FR-007**: The homepage MUST communicate backup, restore, drift detection, findings, evidence, auditability, exceptions, and reviews either directly or through linked product sections.
|
||||
- **FR-008**: The `/platform` route MUST explain the public product model without referencing internal Laravel/Filament implementation details.
|
||||
- **FR-009**: Public copy MUST avoid implementation-first labels where user-facing product language is clearer.
|
||||
|
||||
### CTA And Navigation
|
||||
|
||||
- **FR-010**: All public CTAs MUST resolve to intentional routes or anchors.
|
||||
- **FR-011**: Public CTAs MUST NOT imply login, account creation, billing, checkout, backend submission, or automated onboarding unless such workflow exists.
|
||||
- **FR-012**: Primary navigation MUST expose only intentional, release-ready public routes.
|
||||
- **FR-013**: Footer navigation MUST expose legal, trust, contact, and core product routes intentionally.
|
||||
- **FR-014**: Redirect aliases MUST remain intentional. Aliases that render HTML MUST be noindex; aliases implemented as pure redirects MUST be documented by redirect status/target and excluded from sitemap output.
|
||||
- **FR-014a**: Language switching MUST keep German as the unprefixed default locale and English as prefixed `/en/...` routes.
|
||||
- **FR-014b**: Navigation, footer links, CTAs, metadata, and docs links MUST resolve to the current locale where a localized route exists.
|
||||
|
||||
### Trust, Pricing, And Claims
|
||||
|
||||
- **FR-015**: Trust copy MUST avoid unsupported SOC 2, ISO, Microsoft endorsement, uptime, recovery, compliance, or customer adoption claims.
|
||||
- **FR-016**: Pricing copy MUST avoid unsupported plan entitlement, self-serve subscription, billing, checkout, or purchase claims.
|
||||
- **FR-017**: Static product previews MUST be framed as illustrative/static/demo content and MUST NOT imply live tenant data.
|
||||
- **FR-018**: Public pages MUST NOT publish fake customer logos, fake testimonials, or fake "trusted by" claims.
|
||||
|
||||
### Metadata And SEO
|
||||
|
||||
- **FR-019**: Each canonical public route MUST have Tenantial-specific title and description metadata.
|
||||
- **FR-020**: The generated sitemap MUST include only intentional canonical public routes.
|
||||
- **FR-021**: Redirect-only aliases MUST be excluded from sitemap output.
|
||||
- **FR-022**: `robots.txt` MUST point to `/sitemap-index.xml`.
|
||||
- **FR-023**: Public metadata MUST NOT contain ScrewFast, construction/hardware, template, TenantAtlas, TenantPilot, or TenantCTRL residue.
|
||||
- **FR-023a**: Public metadata MUST expose correct locale signals for German default pages, English `/en/...` pages, and alternate language URLs.
|
||||
|
||||
### Accessibility And Responsive Review
|
||||
|
||||
- **FR-024**: Public pages MUST remain usable on representative mobile and desktop viewport sizes.
|
||||
- **FR-025**: Body-level horizontal overflow MUST NOT occur on validated public routes.
|
||||
- **FR-026**: Keyboard users MUST be able to reach primary navigation, CTAs, footer links, and visible form controls.
|
||||
- **FR-027**: Focus-visible states MUST be visible for interactive elements.
|
||||
- **FR-028**: Reduced-motion preferences MUST be respected for retained animations.
|
||||
- **FR-029**: Status or preview meaning MUST NOT rely on color alone.
|
||||
|
||||
### Validation
|
||||
|
||||
- **FR-030**: Build validation MUST pass with `corepack pnpm build:website`.
|
||||
- **FR-031**: Public smoke validation MUST pass with `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`.
|
||||
- **FR-032**: Whitespace/conflict validation MUST pass with `git diff --check`.
|
||||
- **FR-033**: Scope validation MUST confirm `git status --short -- apps/platform` is empty.
|
||||
- **FR-034**: A launch-readiness note MUST document route exposure, CTA assumptions, trust/pricing claim posture, sitemap behavior, and deployment expectations.
|
||||
|
||||
## Constitution Alignment
|
||||
|
||||
- This feature introduces no Microsoft Graph calls.
|
||||
- This feature introduces no product write/change behavior.
|
||||
- This feature introduces no queued or scheduled work.
|
||||
- This feature introduces no OperationRun behavior.
|
||||
- This feature introduces no AuditLog behavior.
|
||||
- This feature introduces no tenant/workspace isolation behavior.
|
||||
- This feature introduces no RBAC behavior.
|
||||
- This feature introduces no Laravel/Filament/Livewire/admin behavior.
|
||||
- This feature introduces no database or persisted product truth.
|
||||
- This feature introduces no shared product status taxonomy.
|
||||
- This feature introduces no provider/platform seam.
|
||||
- This feature is limited to public website artifacts and public website validation.
|
||||
- Livewire v4.0+ compliance: N/A for implementation because no Livewire code is in scope; the platform remains on Livewire v4 per application info.
|
||||
- Provider registration location: N/A for implementation because no Laravel providers are added or changed; Laravel 11+/12 provider registration remains outside scope.
|
||||
- Globally searchable resources: N/A because no Filament resources are added or changed.
|
||||
- Destructive actions: none.
|
||||
- Asset strategy: website-local Astro/ScrewFast assets only; no Filament assets are registered, so `filament:assets` is not part of this feature's deploy path.
|
||||
- Testing plan: website build, public smoke validation, browser review, `git diff --check`, and `apps/platform` untouched scope check.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-001**: 100% of canonical public routes render successfully during smoke validation.
|
||||
- **SC-002**: 100% of redirect-only aliases redirect intentionally and are excluded from sitemap output.
|
||||
- **SC-003**: 0 public `href="#"` placeholders remain in source or emitted HTML.
|
||||
- **SC-004**: 0 forbidden public residue terms appear in visible copy or metadata across validated public routes.
|
||||
- **SC-005**: 100% of public CTAs resolve to intentional routes or anchors.
|
||||
- **SC-006**: 100% of trust, pricing, and preview claims are conservative or backed by supplied proof.
|
||||
- **SC-007**: 100% of validated mobile and desktop review sizes show no body-level horizontal overflow.
|
||||
- **SC-008**: Primary navigation, CTAs, footer links, and visible controls are reachable by keyboard.
|
||||
- **SC-009**: `corepack pnpm build:website` passes.
|
||||
- **SC-010**: `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke` passes.
|
||||
- **SC-011**: `git diff --check` passes.
|
||||
- **SC-012**: `git status --short -- apps/platform` returns empty.
|
||||
|
||||
## Reviewer Notes
|
||||
|
||||
Reviewers should focus on launch quality, not on replacing the website foundation again.
|
||||
|
||||
The main review questions are:
|
||||
|
||||
1. Does the homepage explain Tenantial clearly?
|
||||
2. Does `/platform` explain the public product model without touching or implying `apps/platform` implementation details?
|
||||
3. Are CTAs honest and usable?
|
||||
4. Are trust and pricing claims conservative?
|
||||
5. Are sitemap, robots, metadata, and route exposure intentional?
|
||||
6. Is the website still ScrewFast-derived rather than a new custom design system?
|
||||
7. Is `apps/platform` untouched?
|
||||
|
||||
## Suggested Validation Commands
|
||||
|
||||
```bash
|
||||
corepack pnpm build:website
|
||||
WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke
|
||||
git diff --check
|
||||
git status --short -- apps/platform
|
||||
```
|
||||
255
specs/403-public-website-launch-readiness/tasks.md
Normal file
255
specs/403-public-website-launch-readiness/tasks.md
Normal file
@ -0,0 +1,255 @@
|
||||
# Tasks: `apps/website` Public Website Launch Readiness
|
||||
|
||||
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/`
|
||||
**Prerequisites**: [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/plan.md), [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/spec.md), [research.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/research.md), [data-model.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/data-model.md), [public-site-contract.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/contracts/public-site-contract.md), [quickstart.md](/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/quickstart.md)
|
||||
|
||||
**Tests**: Browser/static website validation is required by Spec 403. Use Playwright smoke tests and static source/build checks only; do not add Pest, Laravel, Filament, Livewire, database, Sail, Microsoft Graph, queue, job, policy, RBAC, or `apps/platform` test coverage.
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
**Purpose**: Establish the website-only scope and current launch-readiness baseline before editing.
|
||||
|
||||
- [X] T001 Inspect current git scope and confirm `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/platform` is untouched with `git status --short -- apps/platform` from `/Users/ahmeddarrazi/Documents/projects/wt-website`
|
||||
- [X] T002 [P] Review current public route sources in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages`
|
||||
- [X] T003 [P] Review current navigation and footer link data in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/utils/navigation.ts`
|
||||
- [X] T004 [P] Review current metadata components in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/Meta.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/ui/starlight/Head.astro`
|
||||
- [X] T005 [P] Review current Playwright smoke coverage in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts`, `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`, and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational
|
||||
|
||||
**Purpose**: Add shared launch-readiness validation scaffolding required before user-story work.
|
||||
|
||||
**Critical**: Complete this phase before story-specific implementation.
|
||||
|
||||
- [X] T006 Update canonical rendered route and docs route lists in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts` to include `/`, `/platform`, `/pricing`, `/contact`, `/trust`, `/legal`, `/privacy`, `/terms`, `/imprint`, `/welcome-to-docs/`, `/guides/intro/`, `/guides/getting-started/`, `/guides/first-project-checklist/`, and `/platform/evidence-review/`
|
||||
- [X] T007 Update redirect alias expectations in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts` for `/product`, `/products`, `/services`, `/blog`, and `/insights`
|
||||
- [X] T008 Add shared forbidden metadata and public claim assertion helpers in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts`
|
||||
- [X] T009 Add shared public link and anchor validation helpers in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts`
|
||||
- [X] T010 Add shared sitemap parsing helpers in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts`
|
||||
- [X] T011 Confirm no foundational task touches `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/platform` by running `git status --short -- apps/platform` from `/Users/ahmeddarrazi/Documents/projects/wt-website`
|
||||
|
||||
**Checkpoint**: Public route, link, metadata, and sitemap smoke helpers are ready for user-story implementation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Understand Tenantial Quickly (Priority: P1) MVP
|
||||
|
||||
**Goal**: A first-time visitor understands from `/` that Tenantial is evidence-first governance for Microsoft tenant configuration, including backup, restore, drift detection, findings, evidence, auditability, exceptions, and reviews.
|
||||
|
||||
**Independent Test**: Open `/` in desktop and mobile widths and verify that the hero, section sequence, CTAs, and FAQ communicate the product category and core value without requiring platform knowledge.
|
||||
|
||||
### Tests For User Story 1
|
||||
|
||||
- [X] T012 [P] [US1] Add homepage smoke assertions for first-viewport positioning and core capability terms in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T013 [P] [US1] Add homepage mobile overflow and heading visibility assertions in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts`
|
||||
|
||||
### Implementation For User Story 1
|
||||
|
||||
- [X] T014 [US1] Refine homepage hero copy and primary CTA labels in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/index.astro`
|
||||
- [X] T015 [US1] Refine homepage capability section copy for backup, restore, drift detection, findings, evidence, auditability, exceptions, and reviews in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/index.astro`
|
||||
- [X] T016 [P] [US1] Refine homepage FAQ copy to remove implementation-first wording in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/data_files/faqs.json`
|
||||
- [X] T017 [US1] Verify homepage metadata title and description are Tenantial-specific in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/index.astro`
|
||||
- [X] T018 [US1] Run fresh-build US1 validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website` before `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
|
||||
**Checkpoint**: User Story 1 is independently shippable as the MVP.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Evaluate The Public Product Route (Priority: P1)
|
||||
|
||||
**Goal**: A buyer can open `/platform` and understand Tenantial's public product model without confusing it with the internal Laravel/Filament app.
|
||||
|
||||
**Independent Test**: Open `/platform` and verify that the page explains backup, restore, drift detection, findings, evidence, audit trail, exceptions, and reviews, and that previews are static/demo content.
|
||||
|
||||
### Tests For User Story 2
|
||||
|
||||
- [X] T019 [P] [US2] Add `/platform` product model smoke assertions in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T020 [US2] Add static/demo preview framing assertions for `/platform` in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
|
||||
### Implementation For User Story 2
|
||||
|
||||
- [X] T021 [US2] Refine `/platform` product model copy for backup, restore, drift detection, findings, evidence, audit trail, exceptions, and reviews in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T022 [US2] Add or tighten static/demo preview framing text in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T023 [US2] Remove internal Laravel, Filament, Livewire, provider, Graph runtime, or admin implementation wording from `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T024 [P] [US2] Review static product preview component copy for color-independent meaning in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/ui/blocks/ReviewComponent.astro`
|
||||
- [X] T025 [US2] Verify `/platform` metadata title and description are public-product focused in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T026 [US2] Run fresh-build US2 validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website` before `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
|
||||
**Checkpoint**: User Story 2 is independently shippable once `/platform` communicates the public product model safely.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Follow A Safe CTA Path (Priority: P2)
|
||||
|
||||
**Goal**: A visitor can follow clear demo/contact paths without fake forms, dead links, fake account access, unsupported billing, or unsupported self-serve purchase claims.
|
||||
|
||||
**Independent Test**: Click primary and secondary CTAs from homepage, `/platform`, `/pricing`, `/trust`, docs, and footer; each CTA resolves to an intentional route or anchor.
|
||||
|
||||
### Tests For User Story 3
|
||||
|
||||
- [X] T027 [P] [US3] Add public CTA crawl assertions for rendered routes in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T028 [US3] Add emitted `href="#"` placeholder assertions for rendered HTML in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T029 [P] [US3] Add keyboard reachability smoke assertions for primary navigation, CTAs, footer links, and visible form controls in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts`
|
||||
|
||||
### Implementation For User Story 3
|
||||
|
||||
- [X] T030 [US3] Remove or replace placeholder social link values in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/utils/navigation.ts`
|
||||
- [X] T031 [US3] Reframe footer email and `Subscribe` CTA so it does not imply a newsletter workflow in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/sections/navbar&footer/FooterSection.astro`
|
||||
- [X] T032 [US3] Reframe static contact form copy and controls so they do not imply backend submission or guaranteed provisioning in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/contact.astro`
|
||||
- [X] T033 [P] [US3] Verify homepage CTA targets route only to `/contact` or `/platform` in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/index.astro`
|
||||
- [X] T034 [P] [US3] Verify platform CTA targets route only to intentional public routes or anchors in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T035 [P] [US3] Verify pricing CTAs remain contact/demo-oriented in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/pricing.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/data_files/pricing.json`
|
||||
- [X] T036 [P] [US3] Verify trust and docs CTA targets are intentional in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/trust.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/content/docs`
|
||||
- [X] T037 [US3] Run fresh-build US3 validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website` before `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
|
||||
**Checkpoint**: User Story 3 is independently shippable once CTAs are honest and resolvable.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - Trust The Claims (Priority: P2)
|
||||
|
||||
**Goal**: A security-conscious buyer sees conservative, proof-safe wording across trust, legal, pricing, product, and docs pages.
|
||||
|
||||
**Independent Test**: Review visible copy and metadata on `/`, `/platform`, `/pricing`, `/trust`, `/legal`, `/privacy`, `/terms`, `/imprint`, and docs routes; no unsupported proof claims or fake social proof appear.
|
||||
|
||||
### Tests For User Story 4
|
||||
|
||||
- [X] T038 [P] [US4] Expand forbidden public claim assertions for visible copy and metadata in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/smoke-helpers.ts`
|
||||
- [X] T039 [P] [US4] Add trust, pricing, legal, and docs claim-safety smoke assertions in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
|
||||
### Implementation For User Story 4
|
||||
|
||||
- [X] T040 [US4] Rewrite trust page copy to avoid unsupported certification, endorsement, uptime, recovery, compliance, and customer adoption claims in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/trust.astro`
|
||||
- [X] T041 [P] [US4] Rewrite pricing page and pricing data copy to avoid self-serve subscription, billing, checkout, and entitlement claims in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/pricing.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/data_files/pricing.json`
|
||||
- [X] T042 [P] [US4] Replace production placeholder wording on `/imprint` with conservative placeholder-safe public language in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/imprint.astro`
|
||||
- [X] T043 [P] [US4] Review legal, privacy, and terms pages for unsupported claims in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/legal.astro`, `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/privacy.astro`, and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/terms.astro`
|
||||
- [X] T044 [P] [US4] Review docs copy for unsupported runtime, compliance, recovery, or live-tenant claims in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/content/docs`
|
||||
- [X] T045 [P] [US4] Remove or neutralize any fake testimonial, customer logo, or social proof imports in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/sections/testimonials` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/sections/landing/ClientsSection.astro`
|
||||
- [X] T046 [US4] Run fresh-build US4 validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website` before `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
|
||||
**Checkpoint**: User Story 4 is independently shippable once public claims are conservative or backed by supplied proof.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: User Story 5 - Review Launch Metadata And Route Exposure (Priority: P3)
|
||||
|
||||
**Goal**: A site owner can confirm route exposure, metadata, sitemap, robots, and redirects are intentional after build.
|
||||
|
||||
**Independent Test**: Run static route and metadata checks against the built website output.
|
||||
|
||||
### Tests For User Story 5
|
||||
|
||||
- [X] T047 [US5] Add route-specific title, description, canonical, Open Graph, and Twitter metadata assertions in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T048 [US5] Add sitemap allowlist assertions for all intentional canonical routes and exclusion assertions for redirect-only aliases in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T049 [US5] Add exact `/sitemap-index.xml` robots assertion in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
- [X] T050 [US5] Add redirect status and noindex-if-rendered smoke assertions for `/product`, `/products`, `/services`, `/blog`, and `/insights` in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/public-routes.spec.ts`
|
||||
|
||||
### Implementation For User Story 5
|
||||
|
||||
- [X] T051 [US5] Fix route-specific social URL metadata in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/Meta.astro`
|
||||
- [X] T052 [US5] Fix social image URL and MIME/type metadata consistency in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/Meta.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/ui/starlight/Head.astro`
|
||||
- [X] T053 [P] [US5] Confirm sitemap redirect filtering still covers all redirect aliases in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/astro.config.mjs`
|
||||
- [X] T054 [P] [US5] Confirm `robots.txt` points to `/sitemap-index.xml` for `https://tenantial.com` in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/robots.txt.ts`
|
||||
- [X] T055 [P] [US5] Confirm redirect-only pages use documented redirect status/target behavior or noindex metadata if they render HTML in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/product.astro`, `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/products/index.astro`, `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/services.astro`, `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/blog/index.astro`, and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/insights/index.astro`
|
||||
- [X] T056 [US5] Run build validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website`
|
||||
- [X] T057 [US5] Run public smoke validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
|
||||
**Checkpoint**: User Story 5 is independently shippable once route exposure and metadata output are intentional.
|
||||
|
||||
---
|
||||
|
||||
## Final Phase: Polish And Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Complete launch handoff, final browser review, and scope validation.
|
||||
|
||||
- [X] T058 [P] Create launch-readiness note documenting route exposure, CTA assumptions, trust/pricing claim posture, sitemap behavior, deployment expectations, and `apps/platform` untouched confirmation in `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/launch-readiness.md`
|
||||
- [X] T059 [P] Perform manual desktop browser review for light and dark modes on public routes from `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/quickstart.md`
|
||||
- [X] T060 [P] Perform manual mobile browser review for light and dark modes on public routes from `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness/quickstart.md`
|
||||
- [X] T061 [P] Verify keyboard focus reachability and visible focus states across primary navigation, CTAs, footer links, and visible controls using `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts`
|
||||
- [X] T062 [P] Verify reduced-motion readability and color-independent preview meaning on rendered pages in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages`
|
||||
- [X] T063 Run final build validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `corepack pnpm build:website`
|
||||
- [X] T064 Run final public smoke validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `WEBSITE_PORT=4321 corepack pnpm --filter @tenantatlas/website test:smoke`
|
||||
- [X] T065 Run final whitespace validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `git diff --check`
|
||||
- [X] T066 Run final scope validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `git status --short -- apps/platform`
|
||||
- [X] T067 Run source-wide placeholder validation from `/Users/ahmeddarrazi/Documents/projects/wt-website` with `! rg "href=\"#\"|href=\\{\"#\"\\}|href=\\{'#'\\}" apps/website/src`
|
||||
- [X] T068 Add no-JavaScript primary content and link usability smoke coverage in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts`
|
||||
- [X] T069 Review homepage and platform copy for trust/pricing/preview claim safety in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/index.astro` and `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/platform.astro`
|
||||
- [X] T070 Add German-default and English-prefixed localization helpers, copy, pages, docs, redirects, and route metadata in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src`
|
||||
- [X] T071 Add localized language picker behavior to primary navigation and verify `Docs` remains visible in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/sections/navbar&footer/Navbar.astro`
|
||||
- [X] T072 Expand Playwright smoke coverage for German default routes, English `/en/...` routes, localized redirects, metadata, sitemap, and language switching in `/Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke`
|
||||
- [X] T073 Update Spec 403 artifacts to document German as the default locale and English as the additional `/en/...` locale in `/Users/ahmeddarrazi/Documents/projects/wt-website/specs/403-public-website-launch-readiness`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Phase Dependencies**
|
||||
|
||||
Setup before Foundational. Foundational before any user story. User Stories 1 and 2 are both P1 and can proceed after Foundational. User Stories 3 and 4 can proceed after the relevant P1 page and CTA surfaces exist. User Story 5 should run after route, copy, CTA, and trust/pricing decisions settle.
|
||||
|
||||
**User Story Dependencies**
|
||||
|
||||
- **US1**: Depends on Phase 2 only.
|
||||
- **US2**: Depends on Phase 2 only.
|
||||
- **US3**: Depends on Phase 2 and benefits from US1/US2 CTA copy decisions.
|
||||
- **US4**: Depends on Phase 2 and can run parallel with US3 after P1 copy direction is stable.
|
||||
- **US5**: Depends on Phase 2 and final route/metadata decisions from US1-US4.
|
||||
|
||||
**MVP Scope**
|
||||
|
||||
Complete Phase 1, Phase 2, and Phase 3 (US1) for the smallest independently reviewable increment.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
**After Phase 2**
|
||||
|
||||
```text
|
||||
US1 independent files:
|
||||
- T016 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/data_files/faqs.json
|
||||
- T013 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/tests/smoke/interaction.spec.ts
|
||||
|
||||
US2 independent file:
|
||||
- T024 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/ui/blocks/ReviewComponent.astro
|
||||
```
|
||||
|
||||
**CTA And Claim Safety**
|
||||
|
||||
```text
|
||||
US3 CTA work:
|
||||
- T031 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/components/sections/navbar&footer/FooterSection.astro
|
||||
- T032 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/contact.astro
|
||||
- T035 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/pricing.astro and /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/data_files/pricing.json
|
||||
|
||||
US4 claim work:
|
||||
- T040 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/trust.astro
|
||||
- T042 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/imprint.astro
|
||||
- T044 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/content/docs
|
||||
```
|
||||
|
||||
**Metadata And Route Exposure**
|
||||
|
||||
```text
|
||||
US5 output work:
|
||||
- T053 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/astro.config.mjs
|
||||
- T054 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/robots.txt.ts
|
||||
- T055 in /Users/ahmeddarrazi/Documents/projects/wt-website/apps/website/src/pages/product.astro and related redirect page files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
1. Finish the setup and foundational smoke-helper tasks first.
|
||||
2. Ship the MVP with US1 homepage positioning and smoke assertions.
|
||||
3. Add US2 `/platform` product clarity before broad CTA review so link labels and product terms are stable.
|
||||
4. Add US3 CTA integrity and US4 claim safety in parallel where files do not overlap.
|
||||
5. Finish with US5 metadata, route exposure, sitemap, robots, and final build/smoke validation.
|
||||
6. Complete the launch-readiness note and final `apps/platform` untouched proof before review.
|
||||
|
||||
## Test Governance Notes
|
||||
|
||||
The declared test purpose is Browser/static website. The affected validation lanes are website build, Playwright public smoke, manual browser review, whitespace validation, and `apps/platform` scope validation. No Pest, Laravel, Filament, Livewire, database, provider, workspace, membership, session, queue, Sail, or heavy-governance setup is introduced.
|
||||
Loading…
Reference in New Issue
Block a user