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
3 changes: 3 additions & 0 deletions apps/site/baseline/robots.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
User-Agent: *
Content-Signal: ai-train=no, search=yes, ai-input=yes
Content-Usage: / train-ai=n, search=y
Allow: /
Allow: /api/search
Allow: /api/mcp
Allow: /.well-known/
Allow: /llms.txt
Allow: /llms-full.txt
Allow: /auth.md
Allow: /AGENTS.md
Allow: /pricing.md
Disallow: /api/
Disallow: /dashboard/

Expand Down
7 changes: 6 additions & 1 deletion apps/site/src/components/ContentCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ const people = [
{person ? (
<CoverImage
image={item.coverImage}
fallbackAlt={item.title || "CodingCat.dev author photo"}
width={256}
height={256}
class="w-full h-auto aspect-square rounded-md object-cover"
priority={priority}
/>
) : (
<CoverImage image={item.coverImage} priority={priority} />
<CoverImage
image={item.coverImage}
fallbackAlt={item.title || "CodingCat.dev tutorial cover image"}
priority={priority}
/>
)}
</a>
)
Expand Down
5 changes: 4 additions & 1 deletion apps/site/src/components/CoverImage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SanityImageSource } from "@sanity/image-url";

interface Props {
image?: (SanityImageSource & { alt?: string }) | null;
fallbackAlt?: string;
width?: number;
height?: number;
quality?: number;
Expand All @@ -14,6 +15,7 @@ interface Props {

const {
image,
fallbackAlt = "CodingCat.dev tutorial cover image",
width = 1920,
height = 1080,
quality = 80,
Expand All @@ -30,7 +32,8 @@ const src = Astro.locals.sanity

// stegaClean: alt lands in an attribute, where stega's zero-width characters
// would be read out verbatim by screen readers.
const alt = stegaClean((image as { alt?: string } | null)?.alt) ?? "";
const rawAlt = stegaClean((image as { alt?: string } | null)?.alt);
const alt = rawAlt && rawAlt.trim().length > 0 ? rawAlt : fallbackAlt;
---

<div class="transition-shadow duration-200 shadow-md group-hover:shadow-lg sm:mx-0">
Expand Down
11 changes: 9 additions & 2 deletions apps/site/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import NavLink from "./NavLink.astro";
const year = new Date().getUTCFullYear();

const legalLinks = [
{ href: "/ftc-disclosure", label: "FTC Disclosure" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
{ href: "/developers", label: "Developers" },
{ href: "/privacy", label: "Privacy Policy" },
{ href: "/terms-of-use", label: "Terms of Use" },
{ href: "/privacy-policy", label: "Privacy Policy" },
{ href: "/ftc-disclosure", label: "FTC Disclosure" },
{ href: "/sponsors", label: "Sponsors" },
{ href: "/sponsorships", label: "Sponsorships" },
];
Expand Down Expand Up @@ -84,14 +87,18 @@ const socialLinks = [
action="https://buttondown.email/api/emails/embed-subscribe/codingcatdev"
method="post"
target="popupwindow"
toolname="subscribe_newsletter"
tooldescription="Subscribe to the CodingCat.dev web development newsletter"
>
<label class="sr-only" for="footer-email">Email</label>
<input
id="footer-email"
name="email"
type="email"
autocomplete="email"
required
placeholder="Enter your email"
toolparamdescription="Subscriber email address"
class="flex-1 h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
<input type="hidden" value="1" name="embed" />
Expand Down
1 change: 1 addition & 0 deletions apps/site/src/components/HomeSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const { title, description, items, muted = false } = Astro.props;
{item.coverImage && (
<CoverImage
image={item.coverImage}
fallbackAlt={item.title || "CodingCat.dev tutorial cover image"}
width={320}
height={180}
class="aspect-video w-full object-cover object-center"
Expand Down
7 changes: 7 additions & 0 deletions apps/site/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ const graph = buildGraph([
<link rel="openid-configuration" href={`${origin}/.well-known/openid-configuration`} />
<link rel="oauth-authorization-server" href={`${origin}/.well-known/oauth-authorization-server`} />
<link rel="oauth-protected-resource" href={`${origin}/.well-known/oauth-protected-resource`} />
<link rel="agent-guide" type="text/markdown" href={`${origin}/AGENTS.md`} />
<link rel="pricing" type="text/markdown" href={`${origin}/pricing.md`} />
<link rel="agents" href={`${origin}/.well-known/agents.json`} />
<link rel="author" type="text/markdown" href={`${origin}/auth.md`} />
<JsonLd data={graph} />
{/* WebMCP API: Expose client-side tools to browser AI agents */}
Expand All @@ -127,6 +130,10 @@ const graph = buildGraph([
mc.registerTool({
name: "search_content",
description: "Search CodingCat.dev tutorials, articles, podcasts, guests, and sponsors",
annotations: {
readOnlyHint: true,
untrustedContentHint: true,
},
inputSchema: {
type: "object",
properties: {
Expand Down
24 changes: 21 additions & 3 deletions apps/site/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export const onRequest = defineMiddleware(async (context, next) => {

const origin = context.locals.siteUrl.origin;
const pathname = context.url.pathname;
const isDedicatedMarkdownEndpoint = pathname === "/auth.md";
const isDedicatedMarkdownEndpoint =
pathname === "/auth.md" ||
pathname === "/pricing.md" ||
pathname.toLowerCase() === "/agents.md";
const isMarkdownUrl =
!isDedicatedMarkdownEndpoint &&
(pathname.endsWith(".md") || pathname.endsWith("/index.md"));
Expand All @@ -43,7 +46,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
const wantsMarkdown = isMarkdownUrl || Boolean(acceptMarkdown);

let response: Response;
if (isMarkdownUrl) {
if (pathname === "/agents.md") {
response = await context.rewrite("/AGENTS.md");
} else if (isMarkdownUrl) {
const cleanPath =
pathname.replace(/\/index\.md$/, "").replace(/\.md$/, "") || "/";
response = await context.rewrite(cleanPath);
Expand All @@ -58,10 +63,23 @@ export const onRequest = defineMiddleware(async (context, next) => {
response.headers.set("X-Robots-Tag", "noindex, nofollow");
}

// Security & Trust Headers
response.headers.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "SAMEORIGIN");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set("Content-Security-Policy", "frame-ancestors 'self'");

// AIPREF / AI Preference Headers
response.headers.set("Content-Usage", "train-ai=n, search=y");

// Always emit Link headers (RFC 8288) for AI agent discoverability
response.headers.set(
"Link",
`<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog", <${origin}/.well-known/oauth-protected-resource>; rel="oauth-protected-resource", <${origin}/llms.txt>; rel="llms-txt", <${origin}/auth.md>; rel="auth-md"`,
`<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog", <${origin}/.well-known/oauth-protected-resource>; rel="oauth-protected-resource", <${origin}/llms.txt>; rel="llms-txt", <${origin}/auth.md>; rel="auth-md", <${origin}/AGENTS.md>; rel="agent-guide", <${origin}/pricing.md>; rel="pricing"`,
);
response.headers.append("Vary", "Accept");

Expand Down
39 changes: 39 additions & 0 deletions apps/site/src/pages/.well-known/agent-card.json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { APIRoute } from "astro";

export const prerender = false;

/**
* Serves A2A Agent Card for agent-to-agent discovery
*/
export const GET: APIRoute = async ({ locals }) => {
const origin = locals.siteUrl.origin;

const agentCard = {
name: "CodingCat.dev Agent",
description:
"CodingCat.dev AI agent for fullstack web development tutorials, podcasts, and semantic search.",
url: origin,
version: "1.0.0",
protocol: "a2a",
endpoints: {
mcp: `${origin}/api/mcp`,
search: `${origin}/api/search`,
auth: `${origin}/agent/auth`,
claim: `${origin}/agent/claim`,
},
capabilities: {
search: true,
mcp: true,
read_content: true,
},
documentation: `${origin}/AGENTS.md`,
};

return new Response(JSON.stringify(agentCard, null, 2), {
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
"access-control-allow-origin": "*",
},
});
};
34 changes: 10 additions & 24 deletions apps/site/src/pages/.well-known/agent-skills/index.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,22 @@ export const GET: APIRoute = async ({ locals }) => {
const origin = locals.siteUrl.origin;

const skillsIndex = {
$schema: "https://agentskills.io/schema/v1/index.json",
version: "1.0",
$schema:
"https://raw.githubusercontent.com/cloudflare/agent-skills-discovery-rfc/main/schema/index.schema.json",
version: "0.2.0",
provider: {
name: "CodingCat.dev",
url: origin,
},
skills: [
{
id: "search-content",
name: "Search Content",
name: "search-content",
type: "skill-md",
description:
"Search full articles, podcast transcripts, and coding tutorials on CodingCat.dev across fullstack web development topics.",
endpoint: `${origin}/api/search`,
method: "GET",
parameters: [
{
name: "q",
type: "string",
required: true,
description: "The programming term or question to search",
},
],
},
{
id: "mcp-tools",
name: "Model Context Protocol Tools",
description:
"Streamable HTTP MCP tool execution for autonomous AI agent pair programming and search.",
endpoint: `${origin}/api/mcp`,
method: "POST",
documentation: `${origin}/.well-known/mcp/server-card.json`,
"Search fullstack web development tutorials, podcasts, and blog posts on CodingCat.dev",
url: `${origin}/.well-known/agent-skills/search-content/SKILL.md`,
digest:
"sha256:16567e9f7d6d3ed78c4be72cdb450eb46b384f5f91fc355ae866f364859164db",
},
],
};
Expand All @@ -45,6 +30,7 @@ export const GET: APIRoute = async ({ locals }) => {
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
"access-control-allow-origin": "*",
Vary: "Accept",
Link: `<${origin}/llms.txt>; rel="llms-txt", <${origin}/.well-known/api-catalog>; rel="api-catalog"`,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { APIRoute } from "astro";

export const prerender = false;

export const SKILL_CONTENT = `---
name: search-content
description: Search fullstack web development tutorials, podcasts, and blog posts on CodingCat.dev
---

# Search CodingCat.dev Content

Use this skill to search and retrieve CodingCat.dev tutorials, articles, podcasts, transcripts, and metadata across modern web development topics (Astro, TypeScript, React, Svelte, Sanity, Cloudflare Workers, Tailwind CSS).

## Actions
- Search API: GET https://codingcat.dev/api/search?q={query}
- Model Context Protocol (MCP): POST https://codingcat.dev/api/mcp
`;

export const GET: APIRoute = async () => {
return new Response(SKILL_CONTENT, {
headers: {
"content-type": "text/markdown; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
},
});
};
39 changes: 39 additions & 0 deletions apps/site/src/pages/.well-known/agents.json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { APIRoute } from "astro";

export const prerender = false;

/**
* Serves agents.json according to AI agent discovery conventions.
*/
export const GET: APIRoute = async ({ locals }) => {
const origin = locals.siteUrl.origin;

const agentsData = {
agents: [
{
name: "CodingCat Search & Content Agent",
description:
"Autonomous agent for searching and retrieving CodingCat.dev web development tutorials, articles, podcasts, and sponsor info.",
url: `${origin}/api/mcp`,
protocol: "mcp",
documentation: `${origin}/.well-known/mcp/server-card.json`,
},
{
name: "CodingCat A2A Agent",
description:
"Agent-to-agent interface for CodingCat.dev discovery and interactions.",
url: `${origin}/agent/claim`,
protocol: "a2a",
documentation: `${origin}/auth.md`,
},
],
};

return new Response(JSON.stringify(agentsData, null, 2), {
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
"access-control-allow-origin": "*",
},
});
};
8 changes: 8 additions & 0 deletions apps/site/src/pages/.well-known/api-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ export const GET: APIRoute = async ({ locals }) => {
{
anchor: `${origin}/api/search`,
"service-desc": [
{
href: `${origin}/openapi.json`,
type: "application/vnd.oai.openapi+json",
},
{
href: `${origin}/.well-known/openapi.json`,
type: "application/vnd.oai.openapi+json",
},
],
"service-doc": [
{
href: `${origin}/developers`,
type: "text/html",
},
{
href: `${origin}/llms.txt`,
type: "text/markdown",
Expand Down
26 changes: 1 addition & 25 deletions apps/site/src/pages/.well-known/mcp.json.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1 @@
import type { APIRoute } from "astro";

export const prerender = false;

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

const config = {
name: "codingcatdev-search-mcp",
version: "1.0.0",
description:
"Search CodingCat.dev web development guides, coding tutorials, and podcasts.",
endpoint: `${origin}/api/mcp`,
serverCard: `${origin}/.well-known/mcp/server-card.json`,
};

return new Response(JSON.stringify(config, null, 2), {
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "public, max-age=3600, s-maxage=86400",
Vary: "Accept",
Link: `<${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card"`,
},
});
};
export { GET, prerender } from "./mcp/server-card.json";
Loading
Loading