Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 12 additions & 3 deletions src/components/DocsetGrid/Docset.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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));
---

<a href={path} class="container" data-product={productKey}>
<h4>
<Heading>
{productIconName && (
<div class="docset-icon" data-product={productKey}>
<ProductIcon name={productIconName} />
Expand All @@ -29,7 +37,7 @@ const productKey = neutral ? null : (productIconName ?? (path === '/workflow' ?
</div>
)}
<span>{title}</span>
</h4>
</Heading>
<div class="description">
<slot />
</div>
Expand Down Expand Up @@ -69,6 +77,7 @@ const productKey = neutral ? null : (productIconName ?? (path === '/workflow' ?
border-color: var(--color-rose-700);
}

h3,
h4 {
display: flex;
align-items: center;
Expand Down
5 changes: 5 additions & 0 deletions src/components/HeadCommon.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const { activePageGroupIds = [] } = Astro.props as Props;
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico" />
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- Machine-readable entry points, for agents and API tooling that crawl the docs
rather than the dashboard. `service-desc` is the IANA relation for "the
description of this site's API" (RFC 8631). -->
<link rel="service-desc" type="application/json" href="/openapi.json" />
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />

<!-- Include plausible scripts -->
<link rel="preconnect" href="https://plausible.io" />
Expand Down
3 changes: 3 additions & 0 deletions src/content/docs/ci-insights.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@ GitHub and covers basic configuration steps.

<DocsetGrid>
<Docset
headingLevel={4}
title="Self-hosted runners"
path="/ci-insights/runners"
icon="lucide:server"
>
Monitor your self-hosted runners' capacity, performance, cost, and reliability.
</Docset>
<Docset
headingLevel={4}
title="Jobs"
path="/ci-insights/jobs"
icon="lucide:list-checks"
>
Monitor job health, duration, cost, and flaky behavior.
</Docset>
<Docset
headingLevel={4}
title="Auto-Retry"
path="/ci-insights/auto-retry"
icon="lucide:rotate-cw"
Expand Down
30 changes: 30 additions & 0 deletions src/pages/openapi.json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import type { APIRoute } from 'astro';

/**
* Serves the Mergify OpenAPI description at the conventional discovery path.
*
* The spec itself is synced from the engine repository into
* `public/api-schemas.json`, which is where the API Reference pages read it
* from. That filename is ours alone, so agents crawling the docs never find
* it. Tooling — and the "is this site agent-readable" scanners — look for
* `/openapi.json`, so publish the same bytes there too.
*
* This route deliberately does not transform the document: two spellings of
* the same spec that can disagree would be worse than one obscure path.
*/
export const GET: APIRoute = async () => {
// 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',
},
});
};
30 changes: 30 additions & 0 deletions src/util/getOgImageUrl.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
6 changes: 5 additions & 1 deletion src/util/getOgImageUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading