Skip to content
AstroCraft Docs
On this theme

Integrations Directory

The integrations directory is three routes over one collection: /integrations/ draws the whole directory, /integrations/<slug>/ draws a detail page, and /integrations/category/<slug>/ filters it. Twenty-four entries ship in nine categories.

Like /customers/, this route was promised long before it existed — the Product mega menu, the footer, the home page’s integrations orbit and a home differentiator all point at it. Four surfaces advertised the URL; the page is what stops them 404ing.

No pagination, on purpose

Unlike the blog and the customer stories, the directory has no pager. A collection of posts grows without bound; a directory of integrations is a fixed set a reader wants to scan in one go, and the design draws all 24 in a single grid.

The filter chips are the navigation instead. They run in the directory’s own reading order rather than alphabetically, because the chips and the grid below them are the same list under two presentations. Each chip carries a live count from categoryCounts — derived, never authored, because a hand-written “4” beside a chip is a number that goes stale the first time an entry lands.

export const CATEGORY_LABELS = [
  "Communication",
  "Identity & SSO",
  "Ticketing & work",
  "CRM & procurement",
  "Finance & spend",
  "Docs & evidence",
  "Engineering",
  "Cloud & security",
  "Automation",
] as const;

Nine chips, plus “All”.

The four invariants

assertDirectory runs once, on the index route, and throws. It exists because Zod validates each entry alone, and everything that goes wrong in a directory is a relationship between entries — each of which is valid frontmatter, a valid build, and silent in its own way:

A duplicate order. Two cards claim one slot, so the grid’s reading order becomes whichever way the sort happened to break the tie — a different page each time the loader changes its mind. The order is authored rather than derived, precisely because the design’s own sequence (Jira, Linear, Asana, Zendesk) is not a rule a comparator could recover.

The wrong number of featured. The “most connected” row is built for three across. A fourth wraps alone onto a second row; a second leaves a hole. Neither is a type error. FEATURED_COUNT is exported from integrationData rather than inlined because two files depend on the same three — the index renders them, and integrationData.test.ts asserts exactly that many entries carry the flag.

A category with no entries. getStaticPaths builds a filter page for every label in the taxonomy, so an unused label ships an empty listing that the chip row links to.

An entry naming a category outside the set. Caught by z.enum() at the schema, before the directory is even read.

Each throw names the entries involved rather than just the rule:

integrations: "slack" and "jira" both claim order 3 — the directory's reading
order is authored, so it has to be unique.

A detail page

/integrations/<slug>/ composes seven sections, and every one of them is structured data rather than prose — which is why this is the collection with an empty body on every entry.

Section Drawn from
DetailHero name, description, tags, and the category from category
ApiPanel the spec rows, with a Category row prepended from category
StatStrip stats — optional, four figures, CountUp-tweened
Capabilities the three capabilities.cards, each with a validated icon
SlackThread only when showcase: "slack-thread" — a hand-built product mock
Permissions the reads / writes / never scope lists
SetupSteps the shared four steps, or the entry’s own setup override

Two of those are worth expanding.

The masthead draws the category from category, and tags deliberately omits it. So the pill beside the H1 and the breadcrumb cannot disagree about which listing the entry belongs to. The spec panel does the same with its Category row.

never in the permissions block is authored rather than derived. It is a promise about what is not touched, which no complement of the other two lists can express.

The placeholder templates

The security card, the setup steps, the related-integrations heading and three separate counts are one string each in integrationData, rendered across up to 24 pages. They carry {name} and {count} placeholders, filled by fill in @js/integrations:

fill("Revocable in {name}", { name: "Slack" })   // "Revocable in Slack"

The design writes “24” in three places and “Slack” in five. Hand-maintaining those copies is how a directory ends up claiming a size it no longer has, so the count is read from the live collection and substituted.

Unknown placeholders are left alone rather than replaced with undefined — a template that says {plan} should render as a visible mistake, not silently swallow the word.

Logos

logo is a file stem under src/assets/logos/, and the mark is inlined rather than served as an <img>. The five home-page wall logos are drawn monochrome, so their paths carry fill="currentColor" and follow the theme into dark mode — which only works if the SVG is part of the document. The orbit marks keep their owners’ brand colours (the one documented exception to the tokens-only rule) and are inlined through the same door, so there is one mechanism rather than two.

logoSvg throws if the stem has no file, because a missing logo must fail the build rather than render a silent gap; integrationData.test.ts catches the same mistake earlier, on pnpm test.

One detail of that helper is worth knowing before you add your own exports. Figma names the ids in an SVG export by position, so all of these files declare clip0_0_1, and most declare a clip1_0_1 and a gradient id too. Inlined into one document those become duplicate ids, and every url(#clip0_0_1) on the page resolves to whichever logo rendered first — so most marks get clipped by a stranger’s clipPath. logoSvg namespaces every internal id to its file, which means a re-export from Figma cannot reintroduce the bug.

Adding an integration

  1. Create src/data/integrations/<slug>/index.md with an empty body.
  2. Fill the required frontmatter. capabilities needs exactly three cards, permissions needs at least one entry in each of its three lists, and spec needs at least one row.
  3. Drop the vendor’s SVG into src/assets/logos/ and point logo at its stem.
  4. Give it an order no other entry uses.
  5. Pick a category from the nine. If you need a tenth, add it to CATEGORY_LABELS and give it at least one entry, or assertDirectory will fail on the empty listing.

The icon names in capabilities.cards[].icon are validated against the live registry — Icons explains why that check lives in the schema rather than at the render site, and how to find a name that exists.

On the shipped demo entries: they carry twenty-four real companies’ trademarked marks as sample content. Drop any you do not actually integrate with before you ship, and read THIRD-PARTY.md — none of those logos is the template licence’s to grant.

NEXT STEPPages & Routing