diff --git a/public/_redirects b/public/_redirects index d97739fbdb..eb257e3de4 100644 --- a/public/_redirects +++ b/public/_redirects @@ -101,3 +101,8 @@ /monorepo-ci/buildkite /integrations/buildkite#monorepo-ci 301 /monorepo-ci/buildkite/ /integrations/buildkite#monorepo-ci 301 /monorepo-ci/buildkite.md /integrations/buildkite.md 301 + +# `/developers` is the path people and crawlers guess for a developer portal. +# Ours is the API reference. +/developers /api 301 +/developers/ /api 301 diff --git a/public/robots.txt b/public/robots.txt index 723f48f1e1..259589adbe 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -7,3 +7,5 @@ Disallow: /support/premium/ Disallow: /support/premium/* Disallow: /merge-queue/migrate-partitions-to-scopes Allow: / + +Sitemap: https://docs.mergify.com/sitemap-index.xml 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/components/HeadCommon.astro b/src/components/HeadCommon.astro index 93531cb759..c1d848e218 100644 --- a/src/components/HeadCommon.astro +++ b/src/components/HeadCommon.astro @@ -28,6 +28,11 @@ const { activePageGroupIds = [] } = Astro.props as Props; + + + 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. { + // Read as a Buffer, not a string: decoding to UTF-16 and re-encoding would + // make "the same bytes" a claim about a round trip rather than a fact. + const spec = await readFile(path.join(process.cwd(), 'public', 'api-schemas.json')); + + return new Response(spec, { + headers: { + // Plain `application/json` rather than the `application/vnd.oai.openapi+json` + // media type: every generic JSON client understands it, and clients that do + // care about OpenAPI read the `openapi` field in the body anyway. + 'Content-Type': 'application/json; charset=utf-8', + }, + }); +}; 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; }