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;