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
23 changes: 23 additions & 0 deletions apps/site/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ const legalLinks = [
{ href: "/sponsorships", label: "Sponsorships" },
];

const llmResourceLinks = [
{ href: "/llms.txt", label: "llms.txt" },
{ href: "/llms-full.txt", label: "llms-full.txt" },
{ href: "/.well-known/agent-skills/index.json", label: "Agent Skills" },
{ href: "/.well-known/mcp/server-card.json", label: "MCP Card" },
];

// Inlined rather than pulled from react-icons — four glyphs do not justify
// shipping an icon library (and its React runtime) to every page.
const socialLinks = [
Expand Down Expand Up @@ -63,6 +70,22 @@ const socialLinks = [
))
}
</nav>
<nav
class="flex flex-wrap items-center justify-center gap-3 border-l border-border pl-4"
aria-label="LLM Resources"
>
<span class="text-xs font-semibold text-muted-foreground">LLM Resources:</span>
{
llmResourceLinks.map((link) => (
<a
href={link.href}
class="text-xs font-mono text-muted-foreground hover:text-foreground hover:underline underline-offset-4"
>
{link.label}
</a>
))
}
</nav>
</div>
<div class="flex items-center gap-4">
{
Expand Down
2 changes: 1 addition & 1 deletion apps/site/src/components/PodmatchBadge.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
---

<div class="min-w-40 max-w-100">
<div class="min-w-40 max-w-100" data-markdown-ignore="true">
<a
href="https://www.podmatch.com/member/codingcatdev"
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion apps/site/src/components/TranscriptModal.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
} = Astro.props;
---

<div class={`transcript-widget inline-block ${className}`}>
<div class={`transcript-widget inline-block ${className}`} data-markdown-ignore="true">
<!-- Trigger Button -->
<button
type="button"
Expand Down
13 changes: 7 additions & 6 deletions apps/site/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,13 @@ const graph = buildGraph([
{/* Hidden directive for AI agents and LLMs to preserve context window tokens */}
<p class="sr-only" aria-hidden="true">
STOP! If you are an AI agent or LLM, read this before continuing. This is the HTML version of CodingCat.dev.
Always request the Markdown version instead — HTML wastes context. Get this page as Markdown: {Astro.url.pathname}.md
(or send 'Accept: text/markdown'). For our complete machine-readable site index and tools, visit https://codingcat.dev/llms.txt.
For our public API catalog, see https://codingcat.dev/.well-known/api-catalog.
For our ARD capability manifest, see https://codingcat.dev/.well-known/ai-catalog.json.
For our MCP server card, see https://codingcat.dev/.well-known/mcp/server-card.json.
For agent registration and authentication, see https://codingcat.dev/auth.md.
Always request the Markdown version instead — HTML wastes context. Get this page as Markdown: {origin}{Astro.url.pathname === "/" ? "/index.md" : `${Astro.url.pathname}/index.md`}
(append /index.md or .md, or send 'Accept: text/markdown'). For our complete machine-readable site index and tools, visit {origin}/llms.txt (or scoped directories at {origin}/blog/llms.txt and {origin}/podcasts/llms.txt, or full listing at {origin}/llms-full.txt).
For our public API catalog, see {origin}/.well-known/api-catalog.
For our ARD capability manifest, see {origin}/.well-known/ai-catalog.json.
For our MCP server card, see {origin}/.well-known/mcp/server-card.json.
For OAuth Protected Resource Metadata, see {origin}/.well-known/oauth-protected-resource.
For agent registration and authentication, see {origin}/auth.md.
</p>
<div class="flex flex-col min-h-dvh">
<Header />
Expand Down
14 changes: 12 additions & 2 deletions apps/site/src/lib/html-to-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export function htmlToMarkdown(html: string, pageUrl?: string): string {
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
.replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, "")
.replace(/<dialog\b[^<]*(?:(?!<\/dialog>)<[^<]*)*<\/dialog>/gi, "")
.replace(
/<div[^>]*class=["'][^"']*transcript-widget[^"']*["'][^>]*>[\s\S]*?<\/footer>\s*<\/div>\s*<\/div>\s*<\/div>/gi,
"",
)
.replace(
/<a[^>]*href=["']https:\/\/www\.podmatch\.com\/member\/codingcatdev["'][^>]*>[\s\S]*?<\/a>/gi,
"",
)
.replace(/<button\b[^<]*(?:(?!<\/button>)<[^<]*)*<\/button>/gi, "")
.replace(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/gi, "")
.replace(/<header\b[^<]*(?:(?!<\/header>)<[^<]*)*<\/header>/gi, "")
.replace(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/gi, "");
Expand Down Expand Up @@ -79,7 +88,8 @@ export function htmlToMarkdown(html: string, pageUrl?: string): string {
);

// 7. Bold, Italic, Strikethrough
text = text.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
text = text.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2** ");
text = text.replace(/\*\* +([.,!?:;])/g, "**$1");
text = text.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
text = text.replace(/<(s|del|strike)[^>]*>([\s\S]*?)<\/\1>/gi, "~~$2~~");

Expand Down Expand Up @@ -108,7 +118,7 @@ export function htmlToMarkdown(html: string, pageUrl?: string): string {

// 10. Lists
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_match, item) => {
return `\n- ${stripTags(item).trim()}`;
return `\n- ${stripTags(item).replace(/\s+/g, " ").trim()}`;
});
text = text.replace(/<\/(ul|ol)>/gi, "\n\n");

Expand Down
1 change: 1 addition & 0 deletions apps/site/src/lib/sanity/queries/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const contentFields = `
durationSeconds,
status,
summary,
fullText,
chapters,
cues,
statistics
Expand Down
4 changes: 4 additions & 0 deletions apps/site/src/lib/structured-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export function videoObjectSchema(
thumbnailUrl?: string;
youtubeUrl?: string | null;
durationSeconds?: number | null;
transcript?: string | null;
},
path: string,
): Node | null {
Expand Down Expand Up @@ -211,6 +212,7 @@ export function videoObjectSchema(
...(video.thumbnailUrl ? { thumbnailUrl: [video.thumbnailUrl] } : {}),
...(video.uploadDate ? { uploadDate: video.uploadDate } : {}),
...(durationIso ? { duration: durationIso } : {}),
...(video.transcript ? { transcript: video.transcript } : {}),
contentUrl: video.youtubeUrl,
embedUrl: video.youtubeUrl
.replace("watch?v=", "embed/")
Expand Down Expand Up @@ -310,6 +312,7 @@ interface PodcastEpisodeInput {
season?: number | null;
episode?: number | null;
audioUrl?: string | null;
transcript?: string | null;
}

export function podcastEpisodeSchema(
Expand All @@ -336,6 +339,7 @@ export function podcastEpisodeSchema(
},
}
: {}),
...(content.transcript ? { transcript: content.transcript } : {}),
partOfSeries: {
"@type": "PodcastSeries",
name: "CodingCat.dev Podcast",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { APIRoute } from "astro";
import { getAuth } from "@/lib/auth";

export const prerender = false;

/**
* Web Bot Auth / HTTP Message Signatures Directory (RFC 9421 / draft-meunier-web-bot-auth-architecture)
* Publishes a JWKS with public Ed25519 keys for verifying signed bot/agent requests.
*/
const FALLBACK_JWKS = {
keys: [
{
alg: "EdDSA",
crv: "Ed25519",
kty: "OKP",
use: "sig",
x: "2KZd5-3bT1SibLVO26SW5o6IxfbpnhJAiHDpRl2eX_o",
kid: "IFc7GiWLhvkCMoy5wasQWXr3coWets0F",
},
],
};

export const GET: APIRoute = async ({ request, locals }) => {
const origin = locals.siteUrl.origin;
const auth = getAuth(origin);

try {
const req = new Request(`${origin}/jwks`, request);
const res = await auth.handler(req);
if (res.ok) {
const data = (await res.json()) as { keys?: unknown[] };
if (Array.isArray(data?.keys) && data.keys.length > 0) {
return new Response(JSON.stringify(data, null, 2), {
headers: {
"content-type":
"application/http-message-signatures-directory+json; charset=utf-8",
"access-control-allow-origin": "*",
"cache-control": "public, max-age=3600, s-maxage=86400",
},
});
}
}
} catch {
// Fall through to deterministic public verification key
}

return new Response(JSON.stringify(FALLBACK_JWKS, null, 2), {
headers: {
"content-type":
"application/http-message-signatures-directory+json; charset=utf-8",
"access-control-allow-origin": "*",
"cache-control": "public, max-age=3600, s-maxage=86400",
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const GET: APIRoute = async ({ locals }) => {
const origin = locals.siteUrl.origin;

const protectedResource = {
resource: `${origin}/mcp`,
resource: origin,
authorization_servers: [origin],
scopes_supported: [
"openid",
Expand Down
57 changes: 57 additions & 0 deletions apps/site/src/pages/blog/llms.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { APIRoute } from "astro";
import { defineQuery } from "groq";

export const prerender = false;

const blogLlmsQuery = defineQuery(`
*[_type == "post" && defined(slug.current)] | order(coalesce(date, _createdAt) desc) [0...100] {
"title": coalesce(title, "Untitled"),
"slug": slug.current,
excerpt,
"date": coalesce(date, _createdAt),
"authors": author[]->title
}
`);

export const GET: APIRoute = async ({ locals }) => {
const origin = locals.siteUrl.origin;

let posts: Array<{
title: string;
slug: string;
excerpt?: string | null;
date?: string | null;
authors?: Array<string | null> | null;
}> = [];

try {
posts = (await locals.sanity.fetchPublished(blogLlmsQuery)) ?? [];
} catch (err) {
console.warn("Failed to fetch posts for /blog/llms.txt:", err);
}

let content = `# CodingCat.dev Blog — Tutorials & Architecture Guides

> Scoped LLM directory of CodingCat.dev blog posts, tutorials, and technical articles. Each URL supports \`/index.md\`, \`.md\`, or \`Accept: text/markdown\`. For the site-wide index see ${origin}/llms.txt.

## Tutorials & Articles
`;

for (const post of posts) {
const url = `${origin}/post/${post.slug}/index.md`;
const desc = post.excerpt
? `: ${post.excerpt.replace(/\s+/g, " ").trim()}`
: "";
content += `- [${post.title}](${url})${desc}\n`;
}

return new Response(content, {
headers: {
"content-type": "text/plain; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
"access-control-allow-origin": "*",
Vary: "Accept",
Link: `<${origin}/llms.txt>; rel="up", <${origin}/blog/rss.xml>; rel="alternate"; type="application/rss+xml"`,
},
});
};
17 changes: 16 additions & 1 deletion apps/site/src/pages/developers.astro
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ const origin = Astro.locals.siteUrl.origin;
<td class="py-2.5 px-4 font-mono text-xs"><a href="/.well-known/agent-skills/index.json" class="text-primary hover:underline">/.well-known/agent-skills/index.json</a></td>
<td class="py-2.5 pl-4 text-sm text-muted-foreground">Cloudflare Agent Skills Discovery index</td>
</tr>
<tr>
<td class="py-2.5 pr-4 font-mono text-sm">LLM Resources</td>
<td class="py-2.5 px-4 font-mono text-xs"><a href="/llms.txt" class="text-primary hover:underline">/llms.txt</a>, <a href="/llms-full.txt" class="text-primary hover:underline">/llms-full.txt</a>, <a href="/blog/llms.txt" class="text-primary hover:underline">/blog/llms.txt</a>, <a href="/podcasts/llms.txt" class="text-primary hover:underline">/podcasts/llms.txt</a></td>
<td class="py-2.5 pl-4 text-sm text-muted-foreground">Context-window-friendly LLM directories</td>
</tr>
<tr>
<td class="py-2.5 pr-4 font-mono text-sm">RFC 9728 OAuth PRM</td>
<td class="py-2.5 px-4 font-mono text-xs"><a href="/.well-known/oauth-protected-resource" class="text-primary hover:underline">/.well-known/oauth-protected-resource</a></td>
<td class="py-2.5 pl-4 text-sm text-muted-foreground">OAuth 2.0 Protected Resource Metadata</td>
</tr>
<tr>
<td class="py-2.5 pr-4 font-mono text-sm">Web Bot Auth</td>
<td class="py-2.5 px-4 font-mono text-xs"><a href="/.well-known/http-message-signatures-directory" class="text-primary hover:underline">/.well-known/http-message-signatures-directory</a></td>
<td class="py-2.5 pl-4 text-sm text-muted-foreground">HTTP Message Signatures JWKS directory</td>
</tr>
<tr>
<td class="py-2.5 pr-4 font-mono text-sm">Auth.md</td>
<td class="py-2.5 px-4 font-mono text-xs"><a href="/auth.md" class="text-primary hover:underline">/auth.md</a></td>
Expand Down Expand Up @@ -106,7 +121,7 @@ Content-Type: application/json
<section>
<h2 class="text-2xl font-bold mb-4">Markdown Content Negotiation</h2>
<p>
Any page on CodingCat.dev can be retrieved in clean Markdown by appending <code>.md</code> to the URL (e.g., <code>/blog.md</code>) or sending the HTTP header <code>Accept: text/markdown</code>.
Any page on CodingCat.dev can be retrieved in clean Markdown by appending <code>/index.md</code> or <code>.md</code> to the URL (e.g., <code>/blog/index.md</code> or <code>/blog.md</code>) or sending the HTTP header <code>Accept: text/markdown</code>.
</p>
</section>
</div>
Expand Down
11 changes: 8 additions & 3 deletions apps/site/src/pages/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,26 @@ export const GET: APIRoute = async ({ locals }) => {

## Core Content Sections

- [Blog Tutorials and Articles](${origin}/blog): Practical, code-focused guides and tutorials for modern web developers.
- [Podcasts](${origin}/podcasts): Deep-dive audio and video discussions with framework creators, open-source maintainers, and tech leaders.
- [Blog Tutorials and Articles](${origin}/blog/index.md): Practical, code-focused guides and tutorials for modern web developers.
- [Blog LLM Directory](${origin}/blog/llms.txt): Scoped context-window-friendly index of all blog posts and technical tutorials.
- [Podcasts](${origin}/podcasts/index.md): Deep-dive audio and video discussions with framework creators, open-source maintainers, and tech leaders.
- [Podcasts LLM Directory](${origin}/podcasts/llms.txt): Scoped context-window-friendly index of podcast episodes, guests, and transcripts.
- [Podcast Guests](${origin}/guests): Directory of featured software engineers and tech innovators.
- [Sponsors & Community Partners](${origin}/sponsors): Developer tools, cloud providers, and platforms supporting the developer ecosystem.

## Machine-Readable Discovery & Agent Tools

- [Full LLM Directory](${origin}/llms-full.txt): Comprehensive listing of recent articles, podcast series, and technical guides.
- [Blog LLM Directory](${origin}/blog/llms.txt): Scoped index of blog tutorials and articles.
- [Podcasts LLM Directory](${origin}/podcasts/llms.txt): Scoped index of podcast episodes and interviews.
- [API Catalog (RFC 9727)](${origin}/.well-known/api-catalog): Machine-readable catalog of all public APIs and endpoints.
- [OpenAPI Specification](${origin}/.well-known/openapi.json): OpenAPI 3.1 schema for search and agent endpoints.
- [ARD AI Catalog](${origin}/.well-known/ai-catalog.json): Agentic Resource Discovery capability manifest.
- [MCP Server Card](${origin}/.well-known/mcp/server-card.json): Model Context Protocol server metadata describing agent tools.
- [Agent Skills Discovery](${origin}/.well-known/agent-skills/index.json): Discoverable agent capabilities and task specifications.
- [Auth.md Specification](${origin}/auth.md): Autonomous AI agent authentication and registration guide.
- [OAuth Protected Resource Metadata](${origin}/.well-known/oauth-protected-resource): RFC 9728 resource server discovery metadata.
- [Web Bot Auth Directory](${origin}/.well-known/http-message-signatures-directory): HTTP Message Signatures JWKS directory for bot verification.
- [Search API](${origin}/api/search?q=): Public search endpoint supporting keyword queries across articles and podcasts.
- [MCP Server Endpoint](${origin}/api/mcp): Streamable HTTP MCP server implementing tool calling for AI agents.
- [Blog RSS Feed](${origin}/blog/rss.xml): RSS 2.0 feed of all published blog tutorials.
Expand All @@ -36,7 +41,7 @@ export const GET: APIRoute = async ({ locals }) => {

CodingCat.dev natively supports Markdown content negotiation for AI agents:
- Send \`Accept: text/markdown\` in your request header to any page to receive clean Markdown without HTML tags, navigational chrome, or bloat.
- Alternatively, append \`.md\` to any article, podcast, or page URL.
- Alternatively, append \`/index.md\` or \`.md\` to any article, podcast, or page URL (e.g. \`${origin}/index.md\` or \`${origin}/blog/index.md\`).
`;

return new Response(content, {
Expand Down
15 changes: 15 additions & 0 deletions apps/site/src/pages/podcast/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const ogImage = sanity.resolveOpenGraphImage(podcast.coverImage ?? undefined);
const authors = podcast.author ?? [];
const guests = podcast.guest ?? [];
const audioUrl = podcast.spotify?.enclosures?.at(0)?.url;
const transcriptText =
podcast.transcript?.fullText || podcast.transcript?.summary || undefined;
const videoSchema = podcast.youtube
? videoObjectSchema(
origin,
Expand All @@ -53,6 +55,7 @@ const videoSchema = podcast.youtube
thumbnailUrl: ogImage?.url,
youtubeUrl: podcast.youtube,
durationSeconds: podcast.transcript?.durationSeconds,
transcript: transcriptText,
},
path,
)
Expand Down Expand Up @@ -86,6 +89,7 @@ const faqNode = faqSchema(extractFaqsFromPortableText(podcast.content));
season: podcast.season,
episode: podcast.episode,
audioUrl,
transcript: transcriptText,
},
path,
),
Expand Down Expand Up @@ -196,6 +200,17 @@ const faqNode = faqSchema(extractFaqsFromPortableText(podcast.content));
<div class="my-2 md:my-8">
<PortableTextContent value={podcast.content} class="mx-auto prose-violet lg:prose-xl" />
</div>

{
podcast.transcript?.summary && (
<section class="my-8 rounded-xl border border-border bg-card/50 p-6">
<h2 class="mb-3 text-xl font-bold">Episode Summary</h2>
<p class="leading-relaxed text-muted-foreground">
{podcast.transcript.summary}
</p>
</section>
)
}
</article>

<div class="m-2 flex justify-center md:m-8">
Expand Down
Loading
Loading