Skip to content

Commit 3a78590

Browse files
committed
SEO: server-render the listing, add sitemap, route SSR through the API layer
- Homepage now server-renders the first page of domains (real <a> links) from a shared catalog query layer (src/lib/catalog.ts) — was an empty client-only shell. Infinite scroll + search remain as progressive enhancement over the same data. - Expose that query layer as a real endpoint: /api/domains.json. SSR and the client both load through it (one data path, a usable public API). - @astrojs/sitemap -> sitemap-index covering all 5,045 pages; robots.txt points to it so crawlers reach every page without depending on JS. - Narrow the worker's /api routing so static /api/* (domains.json) serves.
1 parent 431f515 commit 3a78590

8 files changed

Lines changed: 166 additions & 60 deletions

File tree

‎astro.config.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { defineConfig } from "astro/config";
22
import react from "@astrojs/react";
3+
import sitemap from "@astrojs/sitemap";
34

45
export default defineConfig({
56
site: "https://integrations.sh",
6-
integrations: [react()],
7+
integrations: [
8+
react(),
9+
// Enumerate every static page (homepage, /d/*, /<kind>/<slug>/*) so crawlers
10+
// don't depend on the client-rendered listing. Exclude JSON API routes.
11+
sitemap({ filter: (page) => !page.includes("/api/") }),
12+
],
713
build: {
814
format: "directory",
915
},

‎bun.lock‎

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@astrojs/react": "^5.0.4",
21+
"@astrojs/sitemap": "^3.7.3",
2122
"@executor-js/config": "^0.0.2",
2223
"@executor-js/plugin-graphql": "^0.0.2",
2324
"@executor-js/plugin-mcp": "^0.0.2",

‎public/robots.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://integrations.sh/sitemap-index.xml

‎src/lib/catalog.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Catalog query layer — the single data-loading path for the registry listing.
3+
*
4+
* Both the public JSON endpoint (`/api/domains.json`) and the server-rendered
5+
* pages call these functions, so SSR goes through the same "API" everyone else
6+
* uses (no ad-hoc grouping in templates). Built over the enriched index.
7+
*/
8+
import { index } from "./data.ts";
9+
import { faviconUrl } from "./favicon.ts";
10+
import type { Kind } from "./types.ts";
11+
12+
export interface DomainSummary {
13+
domain: string;
14+
icon: string | null;
15+
total: number;
16+
formats: Partial<Record<Kind, number>>;
17+
popularity: number;
18+
description: string;
19+
}
20+
21+
const KIND_ORDER: Kind[] = ["mcp", "openapi", "graphql", "cli"];
22+
23+
const DOMAINS: DomainSummary[] = (() => {
24+
const map = new Map<string, DomainSummary>();
25+
for (const r of index) {
26+
const d = r.domain || r.slug;
27+
if (!d) continue;
28+
let g = map.get(d);
29+
if (!g) {
30+
g = { domain: d, icon: faviconUrl(d), total: 0, formats: {}, popularity: 0, description: "" };
31+
map.set(d, g);
32+
}
33+
g.total++;
34+
g.formats[r.kind] = (g.formats[r.kind] ?? 0) + 1;
35+
g.popularity = Math.max(g.popularity, r.popularity ?? 0);
36+
if (!g.description && r.description) g.description = r.description.replace(/\s+/g, " ").slice(0, 110);
37+
}
38+
return [...map.values()].sort(
39+
(a, b) => b.popularity - a.popularity || b.total - a.total || a.domain.localeCompare(b.domain),
40+
);
41+
})();
42+
43+
/** Formats present on a domain, in canonical order. */
44+
export const formatsOf = (d: DomainSummary): Kind[] => KIND_ORDER.filter((k) => d.formats[k]);
45+
46+
/** All domains, popularity-sorted. The full list the JSON endpoint serves. */
47+
export function allDomains(): DomainSummary[] {
48+
return DOMAINS;
49+
}
50+
51+
/** A page of domains — what each server-rendered listing renders. */
52+
export function listDomains(opts?: { offset?: number; limit?: number }): DomainSummary[] {
53+
const offset = opts?.offset ?? 0;
54+
return opts?.limit != null ? DOMAINS.slice(offset, offset + opts.limit) : DOMAINS.slice(offset);
55+
}
56+
57+
export function domainCount(): number {
58+
return DOMAINS.length;
59+
}

‎src/pages/api/domains.json.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { APIRoute } from "astro";
2+
import { allDomains } from "~/lib/catalog.ts";
3+
4+
// GET /api/domains.json — the registry's domains (popularity-sorted), the same
5+
// data the homepage and /browse pages render server-side. Prerendered at build.
6+
export const GET: APIRoute = () =>
7+
new Response(JSON.stringify(allDomains()), {
8+
headers: {
9+
"content-type": "application/json; charset=utf-8",
10+
"access-control-allow-origin": "*",
11+
"cache-control": "public, max-age=3600",
12+
},
13+
});

0 commit comments

Comments
 (0)