From b9a20ed0a2808f51a49e772f10b94954e0bd5f1a Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 28 Aug 2026 15:24:47 +0200 Subject: [PATCH 1/2] fix(seo): restore the homepage OpenGraph image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getOgImageUrl` strips the leading and trailing slashes off the pathname to build the image filename. For the homepage that pathname is `/`, so stripping left an empty string and the lookup missed — every docs page had an OpenGraph image and the homepage shipped `` with no content. The homepage's collection id is `index`, which is what `getStaticPaths` names its image, so fall back to that when the slug comes out empty. Covered by a regression test that fails against the old expression. The generated-image set comes from the content collection and needs the Astro build pipeline, so the test stubs it and exercises the derivation, which is the half that was wrong. Change-Id: Ifb9a23ea2caa20d28489a4f21363d85ed5e3342c --- src/util/getOgImageUrl.test.ts | 30 ++++++++++++++++++++++++++++++ src/util/getOgImageUrl.ts | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/util/getOgImageUrl.test.ts diff --git a/src/util/getOgImageUrl.test.ts b/src/util/getOgImageUrl.test.ts new file mode 100644 index 0000000000..a189aace58 --- /dev/null +++ b/src/util/getOgImageUrl.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest'; +import { getOgImageUrl } from './getOgImageUrl'; + +// `getOgImageUrl` looks a derived filename up in the set of images +// astro-og-canvas actually generated, which comes from the content collection +// and so needs the Astro build pipeline. Stub that set and test the derivation +// — the half where the homepage bug was. +vi.mock('../pages/open-graph/[...path]', () => ({ + getStaticPaths: async () => [ + { params: { path: 'index.png' } }, + { params: { path: 'merge-queue.png' } }, + ], +})); + +describe('getOgImageUrl', () => { + it('resolves the homepage to the index image', () => { + // Regression: stripping the slashes off `/` left an empty slug, so the + // homepage was the one page that shipped an empty `og:image`. + expect(getOgImageUrl('/')).toBe('/open-graph/index.png'); + }); + + it('resolves a normal page, with or without a trailing slash', () => { + expect(getOgImageUrl('/merge-queue')).toBe('/open-graph/merge-queue.png'); + expect(getOgImageUrl('/merge-queue/')).toBe('/open-graph/merge-queue.png'); + }); + + it('returns undefined when no image was generated', () => { + expect(getOgImageUrl('/not-a-page')).toBeUndefined(); + }); +}); diff --git a/src/util/getOgImageUrl.ts b/src/util/getOgImageUrl.ts index b4987ea872..ed43b9046a 100644 --- a/src/util/getOgImageUrl.ts +++ b/src/util/getOgImageUrl.ts @@ -20,6 +20,10 @@ const paths = new Set(routes.map(({ params }) => params.path)); * @returns Path to the OpenGraph image if found. Otherwise, `undefined`. */ export function getOgImageUrl(path: string): string | undefined { - const imagePath = path.replace(/^\//, '').replace(/\/$/, '') + '.png'; + // The homepage's collection id is `index`, so stripping its slashes leaves an + // empty string and the lookup misses — which is why the homepage shipped with + // an empty `og:image` while every other page had one. + const slug = path.replace(/^\//, '').replace(/\/$/, '') || 'index'; + const imagePath = slug + '.png'; if (paths.has(imagePath)) return '/open-graph/' + imagePath; } From 0ac1500183e0a137ac2d99488f40e96f5eeb0f81 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 28 Aug 2026 15:24:54 +0200 Subject: [PATCH 2/2] fix(a11y): stop docset grids skipping a heading level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docset cards rendered their title as `h4`. Almost every grid sits directly under an `##`, so the outline jumped h2 to h4 — including on the homepage, whose whole body is the "Products" grid. Screen readers and anything parsing the document outline read that as a missing level. Default the card heading to `h3` and make it a prop, because one grid does belong at h4: the "Components" grid in `ci-insights.mdx` is nested under an `### Components`, where h3 would make the cards siblings of their own section heading instead of children. Change-Id: Ie01f4c2b03f1f2b7f798ae89b056135a5b00800e --- src/components/DocsetGrid/Docset.astro | 15 ++++++++++++--- src/content/docs/ci-insights.mdx | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/DocsetGrid/Docset.astro b/src/components/DocsetGrid/Docset.astro index aedf5868f4..76af8d5c2d 100644 --- a/src/components/DocsetGrid/Docset.astro +++ b/src/components/DocsetGrid/Docset.astro @@ -9,15 +9,23 @@ interface Props { icon?: string; /** Render a product icon in the neutral text color instead of its brand color. */ neutral?: boolean; + /** + * Heading level for the card title. Grids almost always sit directly under an + * `##`, so `h3` is the default; pass 4 for the handful nested under an `###`. + * Skipping a level breaks the document outline that assistive tech and + * agents read the page structure from. + */ + headingLevel?: 3 | 4; } -const { title, path, icon, neutral } = Astro.props; +const { title, path, icon, neutral, headingLevel = 3 } = Astro.props; +const Heading = `h${headingLevel}` as 'h3' | 'h4'; const productIconName = parseProductIcon(icon); const productKey = neutral ? null : (productIconName ?? (path === '/workflow' ? 'workflow' : null)); --- -

+ {productIconName && (
@@ -29,7 +37,7 @@ const productKey = neutral ? null : (productIconName ?? (path === '/workflow' ?
)} {title} -

+
@@ -69,6 +77,7 @@ const productKey = neutral ? null : (productIconName ?? (path === '/workflow' ? border-color: var(--color-rose-700); } + h3, h4 { display: flex; align-items: center; diff --git a/src/content/docs/ci-insights.mdx b/src/content/docs/ci-insights.mdx index 3973d5d08e..36da559ed3 100644 --- a/src/content/docs/ci-insights.mdx +++ b/src/content/docs/ci-insights.mdx @@ -22,6 +22,7 @@ GitHub and covers basic configuration steps.