Skip to content
Merged
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
31 changes: 27 additions & 4 deletions src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
buildProviderFacets,
} from "../data/catalog.js";
import { loadAllModels } from "../data/load.js";
import { buildLlmsFullTxt, buildLlmsTxt } from "../data/llms.js";
import {
DIST_API_DIR,
DIST_ASSETS_DIR,
DIST_DIR,
MODELS_DIR,
} from "../data/paths.js";
import { modelId } from "../schema/model.js";
import { modelId, type Model } from "../schema/model.js";
import { buildModelJsonSchema } from "../schema/generate.js";
import { bundleClientScript, compileStyles, copyStaticAssets } from "./assets.js";
import { renderIndex } from "./render.js";
Expand All @@ -30,15 +31,36 @@ async function writeJson(file: string, payload: unknown): Promise<void> {
await fs.writeFile(file, JSON.stringify(payload, null, 2) + "\n", "utf8");
}

async function writeLlmsFiles(models: Model[]): Promise<void> {
await fs.writeFile(path.join(DIST_DIR, "llms.txt"), buildLlmsTxt(SITE_URL, models), "utf8");
await fs.writeFile(
path.join(DIST_DIR, "llms-full.txt"),
buildLlmsFullTxt(SITE_URL, models),
"utf8",
);
}

async function writeRobotsAndSitemap(): Promise<void> {
const robots = `User-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`;
const robots = `# AI agents welcome. Machine-readable overview: ${SITE_URL}/llms.txt\nUser-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`;
await fs.writeFile(path.join(DIST_DIR, "robots.txt"), robots, "utf8");

const today = new Date().toISOString().slice(0, 10);
const urls = [
{ loc: `${SITE_URL}/`, priority: "1.0" },
{ loc: `${SITE_URL}/llms.txt`, priority: "0.8" },
{ loc: `${SITE_URL}/llms-full.txt`, priority: "0.7" },
{ loc: `${SITE_URL}/api/v1/models.json`, priority: "0.5" },
{ loc: `${SITE_URL}/api/v1/schema.json`, priority: "0.5" },
];
const body = urls
.map(
({ loc, priority }) =>
` <url><loc>${loc}</loc><lastmod>${today}</lastmod><priority>${priority}</priority></url>`,
)
.join("\n");
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>${SITE_URL}/</loc><lastmod>${today}</lastmod><priority>1.0</priority></url>
<url><loc>${SITE_URL}/api/v1/models.json</loc><lastmod>${today}</lastmod><priority>0.5</priority></url>
${body}
</urlset>
`;
await fs.writeFile(path.join(DIST_DIR, "sitemap.xml"), sitemap, "utf8");
Expand Down Expand Up @@ -99,6 +121,7 @@ export async function build(): Promise<{ models: number }> {
console.log("Bundling client + styles...");
await Promise.all([bundleClientScript(), compileStyles(), copyStaticAssets()]);

await writeLlmsFiles(models);
await writeRobotsAndSitemap();

const elapsed = ((Date.now() - startedAt) / 1000).toFixed(2);
Expand Down
37 changes: 34 additions & 3 deletions src/build/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
providerLabel,
} from "../data/display.js";
import { groupParams } from "../data/group.js";
import { usageGuideMarkdown } from "../data/llms.js";
import { logoFor } from "../data/logos.js";
import { VIEWS_DIR } from "../data/paths.js";
import { modelId, type Catalog, type Model } from "../schema/model.js";
Expand All @@ -30,8 +31,37 @@ export interface RenderOptions {
}

function buildStructuredData(models: Model[]): string {
const data = {
"@context": "https://schema.org",
const website = {
"@type": "WebSite",
"@id": `${SITE_URL}/#website`,
url: `${SITE_URL}/`,
name: "modelparameters.dev",
description: SITE_DESCRIPTION,
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${SITE_URL}/?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
};
const dataset = {
"@type": "Dataset",
"@id": `${SITE_URL}/#dataset`,
name: "modelparameters.dev catalog",
description: SITE_DESCRIPTION,
url: `${SITE_URL}/`,
license: "https://opensource.org/licenses/MIT",
isAccessibleForFree: true,
creator: { "@type": "Organization", name: "modelparameters.dev", url: `${SITE_URL}/` },
distribution: {
"@type": "DataDownload",
encodingFormat: "application/json",
contentUrl: `${SITE_URL}/api/v1/models.json`,
},
};
const itemList = {
"@type": "ItemList",
name: "modelparameters.dev catalog",
description: SITE_DESCRIPTION,
Expand All @@ -48,7 +78,7 @@ function buildStructuredData(models: Model[]): string {
},
})),
};
return JSON.stringify(data);
return JSON.stringify({ "@context": "https://schema.org", "@graph": [website, dataset, itemList] });
}

export async function renderIndex(opts: RenderOptions): Promise<string> {
Expand Down Expand Up @@ -80,6 +110,7 @@ export async function renderIndex(opts: RenderOptions): Promise<string> {
canonicalUrl: SITE_URL,
initialThemeClass: opts.initialThemeClass ?? "",
structuredData: buildStructuredData(opts.catalog.models),
usageGuide: usageGuideMarkdown(SITE_URL),
body,
});

Expand Down
63 changes: 63 additions & 0 deletions src/client/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { setupWebMCP } from "./webmcp.js";

type AuthFilter = "all" | "api_key" | "subscription";

interface FilterState {
Expand Down Expand Up @@ -43,6 +45,57 @@ function setupHowToUseModal(): void {
});
}

async function copyText(text: string): Promise<boolean> {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
/* fall through to the execCommand path below */
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
let copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
}
document.body.removeChild(textarea);
return copied;
}

function setupCopyHowToUse(): void {
const button = document.querySelector<HTMLButtonElement>("[data-copy-how-to-use]");
const source = document.getElementById("how-to-use-md");
if (!button || !source) return;

const label = button.querySelector<HTMLElement>("[data-copy-label]");
const idleIcon = button.querySelector<HTMLElement>("[data-copy-idle]");
const doneIcon = button.querySelector<HTMLElement>("[data-copy-done]");
const idleText = label?.textContent ?? "Copy";
let resetTimer = 0;

button.addEventListener("click", async () => {
const copied = await copyText((source.textContent ?? "").trim());
if (label) label.textContent = copied ? "Copied" : "Press ⌘C";
idleIcon?.classList.toggle("hidden", copied);
doneIcon?.classList.toggle("hidden", !copied);
window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
if (label) label.textContent = idleText;
idleIcon?.classList.remove("hidden");
doneIcon?.classList.add("hidden");
}, 2000);
});
}

function setupThemeToggle(): void {
const toggle = document.querySelector<HTMLButtonElement>("[data-theme-toggle]");
if (!toggle) return;
Expand Down Expand Up @@ -100,6 +153,14 @@ function setupSearch(): void {
state.query = input.value.trim().toLowerCase();
applyFilters();
});

// Deep link: /?q=opus pre-fills the search (backs the schema.org SearchAction).
const initial = new URLSearchParams(window.location.search).get("q");
if (initial) {
input.value = initial;
state.query = initial.trim().toLowerCase();
applyFilters();
}
}

function setupAuthFilters(): void {
Expand Down Expand Up @@ -136,8 +197,10 @@ function setupToggleChips(selector: string, datasetKey: string, bucket: Set<stri
document.addEventListener("DOMContentLoaded", () => {
setupThemeToggle();
setupHowToUseModal();
setupCopyHowToUse();
setupSearch();
setupAuthFilters();
setupToggleChips("[data-provider]", "provider", state.providers);
setupToggleChips("[data-capability]", "capability", state.capabilities);
setupWebMCP();
});
Loading
Loading