diff --git a/website/README.md b/website/README.md index 0c74b242f..48592d510 100644 --- a/website/README.md +++ b/website/README.md @@ -57,13 +57,28 @@ Sidebar order is defined in `astro.config.mjs`, not by directory order. The landing page places its Kubernetes infrastructure section before the final Get started call to action in [Home.astro](src/components/Home.astro), styled with Kubernetes blue -(`#326CE5`). Its unmodified white logo is vendored from the +(`#326CE5`) accents on a light background in both site themes. Its +unmodified full-color logo is vendored from the [CNCF Kubernetes artwork](https://github.com/cncf/artwork/tree/main/projects/kubernetes) -in [public/kubernetes-stacked-white.svg](public/kubernetes-stacked-white.svg). +in [public/kubernetes-stacked-color.svg](public/kubernetes-stacked-color.svg). Keep the official artwork intact rather than recreating or recoloring it. The Kubernetes section's Host on Kubernetes link opens the [Kubernetes self-hosting overview](src/content/docs/self-hosting/overview.md). +The infrastructure description and autoscaling tile both name KEDA. +The feature row starts with queue-driven scaling; its headline +opens the [Autoscaling with KEDA guide](src/content/docs/self-hosting/scaling.md). +The compact full-color [KEDA](https://keda.sh/) wordmark links to the +project site and sits directly on the light section background, without a +separate badge, keeping Kubernetes as the primary brand. +The three borderless feature tiles share an equal-height graphic area +above aligned headlines and descriptions. The KEDA wordmark, container +icon, and GitOps icon are vertically centered in that area; the tiles +stack on narrow screens. +The unmodified SVG is vendored from the +[CNCF KEDA artwork](https://github.com/cncf/artwork/tree/main/projects/keda) +in [public/keda-horizontal-color.svg](public/keda-horizontal-color.svg), +under the [CNCF artwork trademark policy](https://github.com/cncf/artwork/blob/main/LICENSE.md). Mermaid diagrams in Markdown and MDX are rendered client-side by `astro-mermaid`, which must remain before Starlight in the `integrations` @@ -170,7 +185,9 @@ table of contents, code examples, and previous/next navigation. Green and red communicate criterion outcomes, always with text. - **Navigation:** a landing header with direct documentation and demo links, plus task-oriented guide groups: run experiments, design a - benchmark, and connect tools. Existing document URLs are preserved. + benchmark, and connect tools. The footer's Learn group links to the + [Kubernetes deployment guide](src/content/docs/self-hosting/deploy-helm.md). + Existing document URLs are preserved. - **Onboarding:** a concrete example before terminology, followed by separate Portal, CLI, and API entry points. Access requirements are explicit rather than promising instant access to a hosted service. diff --git a/website/public/keda-horizontal-color.svg b/website/public/keda-horizontal-color.svg new file mode 100644 index 000000000..821cdd531 --- /dev/null +++ b/website/public/keda-horizontal-color.svg @@ -0,0 +1 @@ +keda-word diff --git a/website/public/kubernetes-stacked-color.svg b/website/public/kubernetes-stacked-color.svg new file mode 100644 index 000000000..27b5580f5 --- /dev/null +++ b/website/public/kubernetes-stacked-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/src/components/Home.astro b/website/src/components/Home.astro index 29d743204..878a765e9 100644 --- a/website/src/components/Home.astro +++ b/website/src/components/Home.astro @@ -144,18 +144,47 @@ const paths = [

Your experiments deserve serious infrastructure. Scope runs on Kubernetes, bringing containerized agent workers, queue-driven - scaling, and GitOps deployments to your benchmarking stack. + scaling with KEDA, and GitOps deployments to your benchmarking stack.

Host on Kubernetes
- Kubernetes + Kubernetes Open infrastructure. Repeatable experiments.
diff --git a/website/src/components/SiteFooter.astro b/website/src/components/SiteFooter.astro index d0d91e6f0..81e27fce4 100644 --- a/website/src/components/SiteFooter.astro +++ b/website/src/components/SiteFooter.astro @@ -29,6 +29,7 @@ const groups: { title: string; links: FooterLink[] }[] = [ { label: 'What is Scope?', href: 'introduction/what-is-scope/' }, { label: 'Concepts', href: 'introduction/concepts/' }, { label: 'Choose a coding agent', href: 'guides/choosing-a-coding-agent/' }, + { label: 'Kubernetes deployment', href: 'self-hosting/deploy-helm/' }, { label: 'REST API', href: 'reference/rest-api/' }, ], }, diff --git a/website/src/styles/landing.css b/website/src/styles/landing.css index c13bffb58..c9ae5343e 100644 --- a/website/src/styles/landing.css +++ b/website/src/styles/landing.css @@ -109,21 +109,26 @@ scope-showreel { display: block; max-width: 60rem; margin: 3rem auto 0; } .scope-strip-divider { width: 1px; height: 1.1rem; background: var(--scope-border); } .scope-section { margin-top: clamp(3.5rem, 7vw, 6rem); } .scope-demo-section { margin-top: 3.5rem; } -.scope-kubernetes { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 2.5rem; padding: clamp(1.5rem, 5vw, 4rem); border: 1px solid #326ce5; border-radius: 1rem; color: #fff; background: radial-gradient(ellipse at 100% 0, #326ce5 0, transparent 65%), #102b64; box-shadow: 0 24px 70px -36px #326ce580; } -.scope-kubernetes .scope-eyebrow { color: #c5dcff; } -.scope-kubernetes h2 { margin-top: 1rem; font-size: clamp(2.5rem, 5vw, 4.5rem); font-weight: 650; line-height: 1.08; letter-spacing: -0.05em; } -.scope-kubernetes h2 span { color: #fff; } -.scope-home .scope-kubernetes-description { max-width: 49ch; margin-top: 1.5rem; color: #d9e6ff; font-size: 1rem; } -.scope-kubernetes-link { margin-top: 1.75rem; background: #fff; color: #143572; } -.scope-kubernetes-link:hover { background: #e5eeff; } -.scope-kubernetes :focus-visible { outline-color: #fff; } +.scope-kubernetes { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 2.5rem; padding: clamp(1.5rem, 5vw, 4rem); border: 1px solid #cbdcf3; border-radius: 1rem; color: #16365f; background: #f7faff; box-shadow: 0 24px 70px -36px #326ce530; } +.scope-kubernetes .scope-eyebrow { color: #315ca3; } +.scope-kubernetes h2 { margin-top: 1rem; color: #16365f; font-size: clamp(2.5rem, 5vw, 4.5rem); font-weight: 650; line-height: 1.08; letter-spacing: -0.05em; } +.scope-kubernetes h2 span { color: #326ce5; } +.scope-home .scope-kubernetes-description { max-width: 49ch; margin-top: 1.5rem; color: #405675; font-size: 1rem; } +.scope-kubernetes-link { margin-top: 1.75rem; background: #326ce5; color: #fff; } +.scope-kubernetes-link:hover { background: #255bbf; } +.scope-kubernetes :focus-visible { outline-color: #2456ad; } .scope-kubernetes-brand { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 1.5rem; text-align: center; } .scope-kubernetes-brand img { display: block; width: min(100%, 240px); height: auto; } -.scope-kubernetes-brand > span { max-width: 25ch; color: #d9e6ff; font: 0.75rem/1.6 var(--sl-font-mono); } -.scope-home .scope-kubernetes-features { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem; list-style: none; margin: 0; padding: 1.75rem 0 0; border-top: 1px solid #ffffff40; } -.scope-kubernetes-features li { display: flex; flex-direction: column; gap: 0.4rem; } -.scope-kubernetes-features strong { font-size: 0.9rem; font-weight: 600; } -.scope-kubernetes-features span { color: #d9e6ff; font-size: 0.8rem; } +.scope-kubernetes-brand > span { max-width: 25ch; color: #405675; font: 0.75rem/1.6 var(--sl-font-mono); } +.scope-home .scope-kubernetes-features { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem; list-style: none; margin: 0; padding: 1.75rem 0 0; border-top: 1px solid #cbdcf3; } +.scope-kubernetes-features li { display: flex; flex-direction: column; gap: 0.5rem; } +.scope-feature-graphic { display: flex; align-items: center; height: 3rem; color: #326ce5; } +.scope-kubernetes-features strong { color: #16365f; font-size: 0.9rem; font-weight: 600; } +.scope-kubernetes-features strong a { color: inherit; text-decoration: underline; text-underline-offset: 0.15em; } +.scope-kubernetes-features strong a:hover { color: #2456ad; } +.scope-kubernetes-features span { color: #405675; font-size: 0.8rem; } +.scope-keda-brand { display: inline-flex; align-items: center; min-height: 3rem; border-radius: 0.2rem; } +.scope-keda-brand img { display: block; width: 6rem; height: auto; } @media (max-width: 48rem) { .scope-kubernetes { grid-template-columns: minmax(0, 1fr); } .scope-kubernetes-brand img { width: 160px; } diff --git a/website/tests/accessibility.spec.ts b/website/tests/accessibility.spec.ts index 88792da1d..8f0ac67c3 100644 --- a/website/tests/accessibility.spec.ts +++ b/website/tests/accessibility.spec.ts @@ -17,6 +17,25 @@ async function expectAccessible(page: Page) { test('landing page and interactive example are accessible', async ({ page }) => { await page.goto('/scope/'); + const kubernetesSection = page.locator('.scope-kubernetes'); + await expect(kubernetesSection).toHaveCSS('background-color', 'rgb(247, 250, 255)'); + await expect(kubernetesSection.locator('.scope-kubernetes-description')) + .toContainText('queue-driven scaling with KEDA'); + const kubernetesLogo = kubernetesSection.getByRole('img', { name: 'Kubernetes', exact: true }); + await expect(kubernetesLogo).toBeVisible(); + await expect(kubernetesLogo).toHaveAttribute('src', '/scope/kubernetes-stacked-color.svg'); + const scalingFeature = kubernetesSection.getByRole('listitem').first(); + const scalingDocsLink = scalingFeature.getByRole('link', { name: 'Queue-driven scaling', exact: true }); + await expect(scalingDocsLink).toHaveAttribute('href', '/scope/self-hosting/scaling/'); + const kedaLink = scalingFeature.getByRole('link', { name: 'KEDA (opens in a new tab)', exact: true }); + await expect(kedaLink).toBeVisible(); + await expect(kedaLink).toHaveAttribute('href', 'https://keda.sh/'); + await expect(kedaLink).toHaveAttribute('target', '_blank'); + await expect(kedaLink).toHaveAttribute('rel', 'noopener noreferrer'); + await expect(kedaLink).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + const kedaLogo = kedaLink.getByRole('img', { name: 'KEDA', exact: true }); + await expect(kedaLogo).toBeVisible(); + await expect(kedaLogo).toHaveAttribute('src', '/scope/keda-horizontal-color.svg'); await expect(page.locator('scope-flow [data-play]')).toBeEnabled(); await expectAccessible(page); @@ -27,6 +46,55 @@ test('landing page and interactive example are accessible', async ({ page }) => await page.locator('[data-step="3"]').click(); await page.getByText('Inspect per-criterion outcomes', { exact: true }).click(); await expectAccessible(page); + + await scalingDocsLink.click(); + await expect(page).toHaveURL(/\/scope\/self-hosting\/scaling\/$/); + await expect(page.getByRole('heading', { name: 'Autoscaling with KEDA', level: 1, exact: true })) + .toBeVisible(); +}); + +test('infrastructure feature tiles align their graphics and copy', async ({ page }) => { + await page.goto('/scope/'); + const features = page.locator('.scope-kubernetes-features'); + await expect(features.locator('.scope-feature-graphic')).toHaveCount(3); + await expect(features.locator('.scope-feature-graphic > svg[aria-hidden="true"]')).toHaveCount(2); + await expect(features.locator(':scope > li > span')).toHaveText([ + 'KEDA scales workers with demand.', + 'Coding agents run in containers.', + 'Flux keeps deployments in sync.', + ]); + + const layout = await features.evaluate((grid) => { + const topSpread = (selector: string) => { + const positions = Array.from(grid.children, (item) => { + const element = item.querySelector(selector); + if (!element) throw new Error(`Feature tile is missing ${selector}`); + return element.getBoundingClientRect().top; + }); + return Math.max(...positions) - Math.min(...positions); + }; + return { + columns: getComputedStyle(grid).gridTemplateColumns.split(' ').length, + graphics: topSpread('.scope-feature-graphic'), + headings: topSpread('strong'), + descriptions: topSpread(':scope > span'), + graphicCenterOffsets: Array.from(grid.querySelectorAll('.scope-feature-graphic'), (graphic) => { + const visual = graphic.querySelector('img, svg'); + if (!visual) throw new Error('Feature tile is missing its graphic'); + const frame = graphic.getBoundingClientRect(); + const image = visual.getBoundingClientRect(); + return Math.abs(frame.top + frame.height / 2 - image.top - image.height / 2); + }), + }; + }); + for (const offset of layout.graphicCenterOffsets) { + expect(offset).toBeLessThanOrEqual(1); + } + if (layout.columns === 3) { + expect(layout.graphics).toBeLessThanOrEqual(1); + expect(layout.headings).toBeLessThanOrEqual(1); + expect(layout.descriptions).toBeLessThanOrEqual(1); + } }); for (const path of ['/', '/introduction/what-is-scope/', '/community/articles-and-talks/', '/reference/api/']) { @@ -40,6 +108,10 @@ for (const path of ['/', '/introduction/what-is-scope/', '/community/articles-an await page.goto(`/scope${path}`); const footer = page.getByRole('contentinfo'); await expect(footer).toHaveCount(1); + const deploymentLink = footer.getByRole('list', { name: /^learn$/i }) + .getByRole('link', { name: 'Kubernetes deployment', exact: true }); + await expect(deploymentLink).toBeVisible(); + await expect(deploymentLink).toHaveAttribute('href', '/scope/self-hosting/deploy-helm/'); for (const [name, href] of [ ['Your Privacy Choices', 'https://aka.ms/yourcaliforniaprivacychoices'], ['Consumer Health Privacy', 'https://go.microsoft.com/fwlink/?linkid=2259814'],