## Summary - add the localized evaluation-readiness route pair at `/evaluierung` and `/en/evaluation` with a shared page component - wire homepage, platform, trust, review-pack, use-case, footer, and locale-switcher discovery paths into the new evaluation surface - add smoke coverage plus full Spec Kit artifacts for the evaluation, procurement, and rollout readiness feature ## Validation - `corepack pnpm --filter @tenantatlas/website build` - `WEBSITE_PORT=4322 corepack pnpm --filter @tenantatlas/website test tests/smoke/public-routes.spec.ts` - `WEBSITE_PORT=4323 corepack pnpm --filter @tenantatlas/website test tests/smoke/interaction.spec.ts` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #408
531 lines
18 KiB
Plaintext
531 lines
18 KiB
Plaintext
---
|
|
import MainLayout from '@/layouts/MainLayout.astro';
|
|
import HeroSection from '@components/sections/landing/HeroSection.astro';
|
|
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
|
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
|
import Icon from '@components/ui/icons/Icon.astro';
|
|
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
|
|
import heroImage 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].evaluation;
|
|
const siteDescription = siteCopy[locale].site.description;
|
|
const canonicalPath = localizedPath(copy.routePath, 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],
|
|
}}
|
|
>
|
|
<HeroSection
|
|
title={copy.heroTitle}
|
|
subTitle={copy.heroSubtitle}
|
|
primaryBtn={copy.primaryCta.label}
|
|
primaryBtnURL={localizeHref(copy.primaryCta.href, locale)}
|
|
secondaryBtn={copy.secondaryCta.label}
|
|
secondaryBtnURL={localizeHref(copy.secondaryCta.href, locale)}
|
|
withReview={false}
|
|
supportingLine={copy.supportingLine}
|
|
src={heroImage}
|
|
alt={copy.heroTitle}
|
|
/>
|
|
|
|
{/* Evaluation path */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionRoute" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.pathTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.pathIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.evaluationSteps.map((step: any, index: number) => (
|
|
<li class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
|
|
{String(index + 1).padStart(2, '0')}
|
|
</span>
|
|
<h3 class="mt-4 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{step.title}
|
|
</h3>
|
|
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
|
|
{step.content}
|
|
</p>
|
|
</li>
|
|
))
|
|
}
|
|
</ol>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Preparation */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionPrep" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.preparationTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.preparationIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.preparationCards.map((card: any) => (
|
|
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
|
|
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{card.title}
|
|
</h3>
|
|
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
|
|
{card.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Pilot scenarios */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionPilot" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.pilotTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.pilotIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.pilotScenarios.map((scenario: any) => (
|
|
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{scenario.title}
|
|
</h3>
|
|
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
|
|
{scenario.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Stakeholders */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionStakeholders" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.stakeholderTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.stakeholderIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.stakeholderCards.map((card: any) => (
|
|
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{card.title}
|
|
</h3>
|
|
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
|
|
{card.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Security & procurement */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/20 text-yellow-600 ring-1 ring-yellow-400/40 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionSecurity" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.securityTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.securityIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.securityChecklist.map((item: any) => (
|
|
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
|
|
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{item.title}
|
|
</h3>
|
|
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
|
|
{item.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
<div
|
|
class="mt-8 border-t border-neutral-300 pt-6 dark:border-neutral-700"
|
|
>
|
|
<h3
|
|
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
|
|
>
|
|
{copy.securityHandoffTitle}
|
|
</h3>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 dark:text-neutral-300"
|
|
>
|
|
{copy.securityHandoffText}
|
|
</p>
|
|
<div class="mt-5">
|
|
<PrimaryCTA
|
|
title={copy.securityHandoffCta.label}
|
|
url={localizeHref(copy.securityHandoffCta.href, locale)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Microsoft 365 access principles */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionAccess" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.accessTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.accessIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-6 md:grid-cols-2">
|
|
{
|
|
copy.accessPrinciples.map((principle: any) => (
|
|
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{principle.title}
|
|
</h3>
|
|
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
|
|
{principle.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Non-requirements */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionLimits" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.nonRequirementTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.nonRequirementIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{
|
|
copy.nonRequirementCards.map((card: any) => (
|
|
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{card.title}
|
|
</h3>
|
|
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
|
|
{card.content}
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Example timeline */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionTimeline" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.timelineTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.timelineIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
{
|
|
copy.timelineEntries.map((entry: any, index: number) => (
|
|
<li class="rounded-3xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
|
|
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
|
|
{String(index + 1).padStart(2, '0')}
|
|
</span>
|
|
<h3 class="mt-4 text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
|
{entry.title}
|
|
</h3>
|
|
<p class="mt-2 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
|
|
{entry.content}
|
|
</p>
|
|
</li>
|
|
))
|
|
}
|
|
</ol>
|
|
</section>
|
|
|
|
|
|
{/* Buyer FAQ */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
|
|
>
|
|
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
|
|
<span
|
|
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
|
|
>
|
|
<Icon name="sectionFaq" />
|
|
</span>
|
|
<div>
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.faqTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.faqIntro}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 grid gap-x-12 md:grid-cols-2">
|
|
{
|
|
[0, 1].map((column: number) => (
|
|
<div class="hs-accordion-group divide-y divide-neutral-300 dark:divide-neutral-700">
|
|
{copy.faqItems
|
|
.filter((_item: any, index: number) => index % 2 === column)
|
|
.map((item: any, index: number) => {
|
|
const itemIndex = index * 2 + column;
|
|
return (
|
|
<AccordionItem
|
|
id={`evaluation-faq-heading-${itemIndex}`}
|
|
collapseId={`evaluation-faq-collapse-${itemIndex}`}
|
|
question={item.question}
|
|
answer={item.answer}
|
|
first={index === 0}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
{/* Final CTA */}
|
|
<section
|
|
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
|
|
>
|
|
<div
|
|
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
|
|
>
|
|
<div class="max-w-(--breakpoint-md)">
|
|
<h2
|
|
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
|
|
>
|
|
{copy.finalCtaTitle}
|
|
</h2>
|
|
<p
|
|
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
|
|
>
|
|
{copy.finalCtaSubtitle}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mt-6 flex flex-wrap gap-4">
|
|
{
|
|
copy.finalCtas.map((cta: any, index: number) =>
|
|
index === 0 ? (
|
|
<PrimaryCTA
|
|
title={cta.label}
|
|
url={localizeHref(cta.href, locale)}
|
|
/>
|
|
) : (
|
|
<SecondaryCTA
|
|
title={cta.label}
|
|
url={localizeHref(cta.href, locale)}
|
|
/>
|
|
)
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</MainLayout>
|