import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { api } from "./_generated/api";
import { rssFeed, rssFullFeed } from "./rss";
const http = httpRouter();
// Site configuration
const SITE_URL = process.env.SITE_URL || "https://your-blog.netlify.app";
const SITE_NAME = "Wayne Sutton";
// RSS feed endpoint (descriptions only)
http.route({
path: "/rss.xml",
method: "GET",
handler: rssFeed,
});
// Full RSS feed endpoint (with complete content for LLMs)
http.route({
path: "/rss-full.xml",
method: "GET",
handler: rssFullFeed,
});
// Sitemap.xml endpoint for search engines
http.route({
path: "/sitemap.xml",
method: "GET",
handler: httpAction(async (ctx) => {
const posts = await ctx.runQuery(api.posts.getAllPosts);
const urls = [
// Homepage
`
${SITE_URL}/
daily
1.0
`,
// All posts
...posts.map(
(post) => `
${SITE_URL}/${post.slug}
${post.date}
monthly
0.8
`,
),
];
const xml = `
${urls.join("\n")}
`;
return new Response(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=7200",
},
});
}),
});
// API endpoint: List all posts (JSON for LLMs/agents)
http.route({
path: "/api/posts",
method: "GET",
handler: httpAction(async (ctx) => {
const posts = await ctx.runQuery(api.posts.getAllPosts);
const response = {
site: SITE_NAME,
url: SITE_URL,
description: "Developer and writer. Building with Convex and AI.",
posts: posts.map((post) => ({
title: post.title,
slug: post.slug,
description: post.description,
date: post.date,
readTime: post.readTime,
tags: post.tags,
url: `${SITE_URL}/${post.slug}`,
markdownUrl: `${SITE_URL}/api/post?slug=${post.slug}`,
})),
};
return new Response(JSON.stringify(response, null, 2), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "public, max-age=300, s-maxage=600",
"Access-Control-Allow-Origin": "*",
},
});
}),
});
// API endpoint: Get single post as markdown (for LLMs/agents)
http.route({
path: "/api/post",
method: "GET",
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const slug = url.searchParams.get("slug");
const format = url.searchParams.get("format") || "json";
if (!slug) {
return new Response(JSON.stringify({ error: "Missing slug parameter" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const post = await ctx.runQuery(api.posts.getPostBySlug, { slug });
if (!post) {
return new Response(JSON.stringify({ error: "Post not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
// Return raw markdown if requested
if (format === "markdown" || format === "md") {
const markdown = `# ${post.title}
> ${post.description}
**Published:** ${post.date}${post.readTime ? ` | **Read time:** ${post.readTime}` : ""}
**Tags:** ${post.tags.join(", ")}
**URL:** ${SITE_URL}/${post.slug}
---
${post.content}`;
return new Response(markdown, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=300, s-maxage=600",
"Access-Control-Allow-Origin": "*",
},
});
}
// Default: JSON response
const response = {
title: post.title,
slug: post.slug,
description: post.description,
date: post.date,
readTime: post.readTime,
tags: post.tags,
url: `${SITE_URL}/${post.slug}`,
content: post.content,
};
return new Response(JSON.stringify(response, null, 2), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "public, max-age=300, s-maxage=600",
"Access-Control-Allow-Origin": "*",
},
});
}),
});
// Escape HTML characters to prevent XSS
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Generate Open Graph HTML for a post
function generatePostMetaHtml(post: {
title: string;
description: string;
slug: string;
date: string;
readTime?: string;
}): string {
const siteUrl = process.env.SITE_URL || "https://your-blog.netlify.app";
const siteName = "Wayne Sutton";
const defaultImage = `${siteUrl}/og-image.png`;
const canonicalUrl = `${siteUrl}/${post.slug}`;
const safeTitle = escapeHtml(post.title);
const safeDescription = escapeHtml(post.description);
return `
${safeTitle} | ${siteName}
${safeTitle}
${safeDescription}
${post.date}${post.readTime ? ` ยท ${post.readTime}` : ""}
Redirecting to full article...
`;
}
// HTTP endpoint for Open Graph metadata
http.route({
path: "/meta/post",
method: "GET",
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const slug = url.searchParams.get("slug");
if (!slug) {
return new Response("Missing slug parameter", { status: 400 });
}
try {
const post = await ctx.runQuery(api.posts.getPostBySlug, { slug });
if (!post) {
return new Response("Post not found", { status: 404 });
}
const html = generatePostMetaHtml({
title: post.title,
description: post.description,
slug: post.slug,
date: post.date,
readTime: post.readTime,
});
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control":
"public, max-age=60, s-maxage=300, stale-while-revalidate=600",
},
});
} catch {
return new Response("Internal server error", { status: 500 });
}
}),
});
export default http;