Integrations Directory
The integrations section is two routes over one collection. /integrations/ is a searchable, filterable directory with a featured row at the top; /integrations/<id>/ is a detail page assembled almost entirely from the entry’s frontmatter. Nine sample integrations ship — Slack, Salesforce, Zapier, Notion, Linear, GitHub, Stripe, Discord and Mailchimp.
It is the theme’s second content-driven route pair, and it deliberately mirrors the blog’s shape. If you have read Blog & RSS, most of this will look familiar.
Directory order lives in one place
src/js/integrationUtils.ts owns the sort, and both routes read through it:
export async function getIntegrations() {
const all = await getCollection("integrations");
return all.sort((a, b) => b.data.rating - a.data.rating || a.data.name.localeCompare(b.data.name));
}
Rating descending, then name alphabetically as a tie-break. That is the design’s “Most Popular” ordering, and because getStaticPaths reads the same helper, the index order and the generated paths cannot drift.
The design’s “Sort by: Most Popular” dropdown was dropped, since the grid is most-popular-sorted and a control with one option is not a control. So was “Load More Integrations” — nine cards never need pagination.
The index
IntegrationIndex receives the sorted entries and derives everything else:
- The featured row is
items.filter((i) => i.data.featured)— the wide cards at the top.featureddefaults tofalsein the schema. - The category pills are
[...new Set(items.map((i) => i.data.category))], so adding a newcategorystring to any entry grows the filter bar with nothing to register. This is the same derivation the blog index uses for its categories. - The grid is every entry, featured ones included, so each filter pill always has members.
Search and category filtering are one small bundled script and they compose with AND — typing “slack” with the Communication pill active narrows to both. The script also binds ⌘K / Ctrl-K to focus the search box, through the shared _hotkey.ts owner rather than its own keydown listener, so two components can never both claim the chord.
With JavaScript off, every card is visible, the pills are inert and the search box does nothing. That is the degradation contract the whole theme follows: the content is always there, the enhancement is what is missing.
The detail page
src/pages/integrations/[id].astro renders the entry and composes three sections. It passes the entry’s image as the page’s social image and builds a breadcrumb trail that feeds both the visible navigation and the BreadcrumbList JSON-LD — one array, two consumers, so the markup and the structured data agree by construction.
IntegrationHero renders the logo chip, the <Name> + <site name> title, the description, an install CTA and a meta row of Category / Developer / Rating — the last formatted as 4.9/5.0 from the numeric field. It also renders the breadcrumbs.
IntegrationHighlight renders highlight.title and highlight.lead from frontmatter, and the entry’s markdown body becomes the section’s paragraphs. This is the only place in the theme where a collection entry’s body is a component’s content rather than the whole page.
IntegrationSetup renders the requirements array as a checklist inside the house violet panel, with a documentation pointer. The schema requires at least one requirement, so an empty checklist is a build error rather than an empty box.
Two links on the detail page are placeholders: “Install Integration” and “View Documentation” both point at #, because no install flow or docs route exists in a theme. Point them somewhere real, or remove them.
The logo convention
Each entry needs a brand glyph at src/assets/logos/<id>.svg, keyed by the collection entry id — src/data/integrations/slack.md needs src/assets/logos/slack.svg.
This is enforced outside the Zod schema, in the helper module. integrationUtils.ts globs the directory eagerly and throws a named error when a glyph is missing:
No logo for integration "linear" — add src/assets/logos/linear.svg
That is a build failure whose message contains the fix, which is why it is a throw rather than a fallback glyph. The SVGs are imported raw and inlined with set:html, the same treatment the footer wordmark gets — no image request, no icon registry entry, and they can be styled with CSS if you want them to.
The eager glob is a deliberate choice for a small set. If you grow to hundreds of integrations, that is the ceiling to revisit.
Adding an integration
Create src/data/integrations/<slug>.md and a matching src/assets/logos/<slug>.svg:
---
name: Slack
tagline: Send automated alerts and updates directly to your team's channels.
description: Bring your team's communication into your workflow engine.
category: Communication
developer: Olsa Labs
rating: 4.9
featured: true
image: ../../assets/images/integrations/dashboard.jpg
highlight:
title: Real-time transparency
lead: Eliminate context switching.
requirements:
- Slack Workspace Admin permissions
- An active Olsa Growth or Team account
---
The markdown body becomes the highlight section's paragraphs.
developer defaults to "Olsa Labs" if you omit it, featured defaults to false, and rating is bounded 0–5. The image is the detail page screenshot and doubles as the page’s og:image; the nine sample entries all share one dashboard render, which is a placeholder you will want to replace per integration.
The card blurb is tagline, not description — keep it to one line. The hero paragraph and the meta description are description.
Repurposing the section
The directory shape is generic: a searchable, filterable, rating-sorted grid of entries with detail pages. It works as-is for a product catalog, a plugin marketplace, a partner list or a template gallery. What you change is the schema in src/content.config.ts, the field names the three sections read, and the sort in integrationUtils.ts. The search-plus-filter script, the derived pills and the breadcrumb wiring are all field-agnostic.