diff --git a/Agents.md b/Agents.md index d3f69e6b..4adac4fa 100644 --- a/Agents.md +++ b/Agents.md @@ -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) diff --git a/apps/website/astro.config.mjs b/apps/website/astro.config.mjs index d038fca8..7bf81bd2 100644 --- a/apps/website/astro.config.mjs +++ b/apps/website/astro.config.mjs @@ -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(), ], diff --git a/apps/website/src/components/BrandLogo.astro b/apps/website/src/components/BrandLogo.astro index cf1ed9f1..a90fbdae 100644 --- a/apps/website/src/components/BrandLogo.astro +++ b/apps/website/src/components/BrandLogo.astro @@ -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 ?? ''}`; +--- + - T - Tenantial + + + diff --git a/apps/website/src/components/Meta.astro b/apps/website/src/components/Meta.astro index 5c078eae..31204278 100644 --- a/apps/website/src/components/Meta.astro +++ b/apps/website/src/components/Meta.astro @@ -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({ { - 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 ; }) } + {/* Facebook Meta Tags */} - - + + @@ -117,8 +123,8 @@ const appleTouchIcon = await getImage({ {/* Twitter Meta Tags */} - - + + diff --git a/apps/website/src/components/pages/ContactPage.astro b/apps/website/src/components/pages/ContactPage.astro new file mode 100644 index 00000000..092835f8 --- /dev/null +++ b/apps/website/src/components/pages/ContactPage.astro @@ -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); +--- + + + + diff --git a/apps/website/src/components/pages/HomePage.astro b/apps/website/src/components/pages/HomePage.astro new file mode 100644 index 00000000..98677c06 --- /dev/null +++ b/apps/website/src/components/pages/HomePage.astro @@ -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], +})); +--- + + + + + + + + + + + + diff --git a/apps/website/src/components/pages/LegalPage.astro b/apps/website/src/components/pages/LegalPage.astro new file mode 100644 index 00000000..4798d5f3 --- /dev/null +++ b/apps/website/src/components/pages/LegalPage.astro @@ -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; +--- + + + + + + + + + + + diff --git a/apps/website/src/components/pages/PlatformPage.astro b/apps/website/src/components/pages/PlatformPage.astro new file mode 100644 index 00000000..609fd9cb --- /dev/null +++ b/apps/website/src/components/pages/PlatformPage.astro @@ -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); +--- + + + + + + + + + + + + diff --git a/apps/website/src/components/pages/PricingPage.astro b/apps/website/src/components/pages/PricingPage.astro new file mode 100644 index 00000000..c02292f7 --- /dev/null +++ b/apps/website/src/components/pages/PricingPage.astro @@ -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; +--- + + + + + diff --git a/apps/website/src/components/pages/TextPage.astro b/apps/website/src/components/pages/TextPage.astro new file mode 100644 index 00000000..a4614357 --- /dev/null +++ b/apps/website/src/components/pages/TextPage.astro @@ -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]; +--- + + + + diff --git a/apps/website/src/components/pages/TrustPage.astro b/apps/website/src/components/pages/TrustPage.astro new file mode 100644 index 00000000..90b604a8 --- /dev/null +++ b/apps/website/src/components/pages/TrustPage.astro @@ -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; +--- + + + + + diff --git a/apps/website/src/components/sections/landing/ClientsSection.astro b/apps/website/src/components/sections/landing/ClientsSection.astro index 33b0a3e4..1d9d2924 100644 --- a/apps/website/src/components/sections/landing/ClientsSection.astro +++ b/apps/website/src/components/sections/landing/ClientsSection.astro @@ -14,6 +14,10 @@ interface Props { subTitle?: string; partners: Partner[]; } + +const visiblePartners = partners.filter( + partner => partner.href && partner.icon +); --- {/* Clients Group SVGs */} { - partners.map(partner => ( - + visiblePartners.map(partner => ( + )) diff --git a/apps/website/src/components/sections/landing/HeroSection.astro b/apps/website/src/components/sections/landing/HeroSection.astro index 5ea47f48..27359661 100644 --- a/apps/website/src/components/sections/landing/HeroSection.astro +++ b/apps/website/src/components/sections/landing/HeroSection.astro @@ -15,7 +15,6 @@ const { secondaryBtnURL, withReview, avatars, - starCount, rating, reviews, src, @@ -32,7 +31,6 @@ interface Props { secondaryBtnURL?: string; withReview?: boolean; avatars?: Array; - starCount?: number; rating?: string; reviews?: string; src?: any; @@ -81,12 +79,7 @@ interface Props { } { withReview ? ( - + ) : ( '' ) diff --git a/apps/website/src/components/sections/misc/Authentication.astro b/apps/website/src/components/sections/misc/Authentication.astro index 537d9af6..6be8c104 100644 --- a/apps/website/src/components/sections/misc/Authentication.astro +++ b/apps/website/src/components/sections/misc/Authentication.astro @@ -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 = ` + + + `; + +const copy = siteCopy[locale].auth; --- - + + + {copy.walkthrough} + diff --git a/apps/website/src/components/sections/misc/ContactSection.astro b/apps/website/src/components/sections/misc/ContactSection.astro index 1d648a96..7dd532b6 100644 --- a/apps/website/src/components/sections/misc/ContactSection.astro +++ b/apps/website/src/components/sections/misc/ContactSection.astro @@ -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 = - {title} + {copy.title} - {subTitle} + {copy.subtitle} @@ -36,7 +38,7 @@ const formSubTitle: string = - {formTitle} + {copy.formTitle} { /* Form for user input with various input fields.--> @@ -47,31 +49,31 @@ const formSubTitle: string = - - + + - + - {formSubTitle} + {copy.formSubtitle} @@ -82,35 +84,35 @@ const formSubTitle: string = } ; +}; ---
- {subTitle} + {copy.subtitle}
- {formSubTitle} + {copy.formSubtitle}