diff --git a/README.md b/README.md
index 986dc21..2deed4a 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ A minimalist markdown site built with React, Convex, and Vite. Optimized for SEO
- `/api/posts` - JSON list of all posts for agents
- `/api/post?slug=xxx` - Single post JSON or markdown
- `/rss-full.xml` - Full content RSS for LLM ingestion
-- Copy Page dropdown for sharing to ChatGPT, Claude, Cursor, VS Code
+- Copy Page dropdown for sharing to ChatGPT, Claude
## Getting Started
@@ -202,9 +202,8 @@ npx convex deploy
- Publish directory: `dist`
4. Add environment variable:
- `CONVEX_DEPLOY_KEY` - Generate from [Convex Dashboard](https://dashboard.convex.dev) > Project Settings > Deploy Key
-5. Update `netlify.toml` with your production Convex HTTP URL (replace `YOUR_CONVEX_DEPLOYMENT`)
-The `CONVEX_DEPLOY_KEY` lets Netlify automatically deploy functions and set `VITE_CONVEX_URL` on each build.
+The `CONVEX_DEPLOY_KEY` lets Netlify automatically deploy functions and set `VITE_CONVEX_URL` on each build. Edge functions dynamically proxy RSS, sitemap, and API requests to your Convex HTTP endpoints.
**Build issues?** Netlify sets `NODE_ENV=production` which skips devDependencies. The `--include=dev` flag fixes this. See [netlify-deploy-fix.md](./netlify-deploy-fix.md) for detailed troubleshooting.
@@ -219,6 +218,11 @@ markdown-site/
│ ├── rss.ts # RSS feed generation
│ └── schema.ts # Database schema
├── netlify/ # Netlify edge functions
+│ └── edge-functions/
+│ ├── rss.ts # RSS feed proxy
+│ ├── sitemap.ts # Sitemap proxy
+│ ├── api.ts # API endpoint proxy
+│ └── botMeta.ts # OG crawler detection
├── public/ # Static assets
│ ├── images/ # Blog images and OG images
│ ├── robots.txt # Crawler rules
diff --git a/TASK.md b/TASK.md
index 68476f6..17bdaba 100644
--- a/TASK.md
+++ b/TASK.md
@@ -2,7 +2,7 @@
## Current Status
-v1.0.0 ready for deployment. Build passes. TypeScript verified.
+v1.1.0 ready for deployment. Build passes. TypeScript verified.
## Completed
@@ -26,13 +26,15 @@ v1.0.0 ready for deployment. Build passes. TypeScript verified.
- [x] Netlify build configuration verified
- [x] SPA 404 fallback configured
- [x] Mobile responsive design
+- [x] Edge functions for dynamic Convex HTTP proxying
+- [x] Vite dev server proxy for local development
## Deployment Steps
1. Run `npx convex dev` to initialize Convex
-2. Update `netlify.toml` with your Convex deployment URL
-3. Set `VITE_CONVEX_URL` in Netlify environment variables
-4. Connect repo to Netlify and deploy
+2. Set `CONVEX_DEPLOY_KEY` in Netlify environment variables
+3. Connect repo to Netlify and deploy
+4. Edge functions automatically handle RSS, sitemap, and API routes
## Future Enhancements
diff --git a/changelog.md b/changelog.md
index 80db3f4..5bd791e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## [1.1.0] - 2025-12-14
+
+### Added
+
+- Netlify Edge Functions for dynamic Convex HTTP proxying
+ - `rss.ts` proxies `/rss.xml` and `/rss-full.xml`
+ - `sitemap.ts` proxies `/sitemap.xml`
+ - `api.ts` proxies `/api/posts` and `/api/post`
+- Vite dev server proxy for RSS, sitemap, and API endpoints
+
+### Changed
+
+- Replaced hardcoded Convex URLs in netlify.toml with edge functions
+- Edge functions dynamically read `VITE_CONVEX_URL` from environment
+- Updated setup guide, docs, and README with edge function documentation
+
+### Fixed
+
+- RSS feeds and sitemap now work without manual URL configuration
+- Local development properly proxies API routes to Convex
+
## [1.0.0] - 2025-12-14
### Added
@@ -25,7 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- API endpoints for LLM access:
- `/api/posts` - JSON list of all posts
- `/api/post?slug=xxx` - Single post as JSON or markdown
-- Copy Page dropdown for sharing to ChatGPT, Claude, Cursor, VS Code
+- Copy Page dropdown for sharing to ChatGPT, Claude
- Open Graph and Twitter Card meta tags
- Netlify edge function for social media crawler detection
- Build-time markdown sync from `content/blog/` to Convex
diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md
index 10b1d4f..e779517 100644
--- a/content/blog/setup-guide.md
+++ b/content/blog/setup-guide.md
@@ -165,30 +165,18 @@ The HTTP URL uses `.convex.site` instead of `.convex.cloud`:
https://happy-animal-123.convex.site
```
-## Step 6: Configure Netlify Redirects
+## Step 6: Verify Edge Functions
-Open `netlify.toml` and replace `YOUR_CONVEX_DEPLOYMENT` with your actual deployment name:
+The blog uses Netlify Edge Functions to dynamically proxy RSS, sitemap, and API requests to your Convex HTTP endpoints. No manual URL configuration is needed.
-```toml
-# Before
-[[redirects]]
- from = "/rss.xml"
- to = "https://YOUR_CONVEX_DEPLOYMENT.convex.site/rss.xml"
+Edge functions in `netlify/edge-functions/`:
-# After (example)
-[[redirects]]
- from = "/rss.xml"
- to = "https://happy-animal-123.convex.site/rss.xml"
-```
+- `rss.ts` - Proxies `/rss.xml` and `/rss-full.xml`
+- `sitemap.ts` - Proxies `/sitemap.xml`
+- `api.ts` - Proxies `/api/posts` and `/api/post`
+- `botMeta.ts` - Serves Open Graph HTML to social media crawlers
-Update all redirect rules with your deployment name:
-
-- `/rss.xml`
-- `/rss-full.xml`
-- `/sitemap.xml`
-- `/api/posts`
-- `/api/post`
-- `/meta/post`
+These functions automatically read `VITE_CONVEX_URL` from your environment and convert it to the Convex HTTP site URL (`.cloud` becomes `.site`).
## Step 7: Deploy to Netlify
@@ -495,9 +483,10 @@ Your blog includes these API endpoints for search engines and AI:
### RSS/Sitemap not working
-1. Verify `netlify.toml` has correct Convex URL
-2. Check that Convex functions are deployed
-3. Test the Convex HTTP URL directly in browser
+1. Verify `VITE_CONVEX_URL` is set in Netlify environment variables
+2. Check that Convex HTTP endpoints are deployed (`npx convex deploy`)
+3. Test the Convex HTTP URL directly: `https://your-deployment.convex.site/rss.xml`
+4. Verify edge functions exist in `netlify/edge-functions/`
### Build failures on Netlify
@@ -545,6 +534,12 @@ markdown-site/
│ ├── posts.ts # Post queries/mutations
│ ├── rss.ts # RSS feed generation
│ └── schema.ts # Database schema
+├── netlify/
+│ └── edge-functions/ # Netlify edge functions
+│ ├── rss.ts # RSS proxy
+│ ├── sitemap.ts # Sitemap proxy
+│ ├── api.ts # API proxy
+│ └── botMeta.ts # OG crawler detection
├── public/ # Static assets
│ ├── robots.txt # Crawler rules
│ └── llms.txt # AI agent discovery
diff --git a/content/pages/docs.md b/content/pages/docs.md
index c493a15..221c12d 100644
--- a/content/pages/docs.md
+++ b/content/pages/docs.md
@@ -39,6 +39,12 @@ markdown-site/
│ ├── pages.ts # Page queries/mutations
│ ├── http.ts # API endpoints
│ └── rss.ts # RSS generation
+├── netlify/
+│ └── edge-functions/ # Netlify edge functions
+│ ├── rss.ts # RSS proxy
+│ ├── sitemap.ts # Sitemap proxy
+│ ├── api.ts # API proxy
+│ └── botMeta.ts # OG crawler detection
├── src/
│ ├── components/ # React components
│ ├── context/ # Theme context
@@ -186,9 +192,11 @@ body {
### Netlify setup
1. Connect GitHub repo to Netlify
-2. Set build command: `npm run deploy`
-3. Set publish directory: `dist`
-4. Add env variable: `VITE_CONVEX_URL`
+2. Build command: `npm ci --include=dev && npx convex deploy --cmd 'npm run build'`
+3. Publish directory: `dist`
+4. Add env variable: `CONVEX_DEPLOY_KEY` (from Convex Dashboard)
+
+The build automatically sets `VITE_CONVEX_URL` from the deploy key.
### Convex production
@@ -196,16 +204,9 @@ body {
npx convex deploy
```
-### netlify.toml
+### Edge functions
-Replace `YOUR_CONVEX_DEPLOYMENT` with your deployment name:
-
-```toml
-[[redirects]]
- from = "/rss.xml"
- to = "https://YOUR_DEPLOYMENT.convex.site/rss.xml"
- status = 200
-```
+RSS, sitemap, and API routes are handled by Netlify Edge Functions in `netlify/edge-functions/`. They dynamically read `VITE_CONVEX_URL` from the environment. No manual URL configuration needed.
## Convex schema
@@ -250,12 +251,13 @@ export default defineSchema({
**RSS/Sitemap errors**
-- Verify `netlify.toml` Convex URL
-- Test Convex HTTP URL directly
-- Check Convex function deployment
+- Verify `VITE_CONVEX_URL` is set in Netlify
+- Test Convex HTTP URL: `https://your-deployment.convex.site/rss.xml`
+- Check edge functions in `netlify/edge-functions/`
**Build failures**
-- Verify `VITE_CONVEX_URL` is set
+- Verify `CONVEX_DEPLOY_KEY` is set in Netlify
+- Ensure `@types/node` is in devDependencies
+- Build command must include `--include=dev`
- Check Node.js version (18+)
-- Review Netlify build logs
diff --git a/convex/rss.ts b/convex/rss.ts
index 7756b31..10e5776 100644
--- a/convex/rss.ts
+++ b/convex/rss.ts
@@ -2,9 +2,10 @@ import { httpAction } from "./_generated/server";
import { api } from "./_generated/api";
// Site configuration for RSS feed
-const SITE_URL = "https://your-blog.netlify.app";
-const SITE_TITLE = "Wayne Sutton";
-const SITE_DESCRIPTION = "Developer and writer. Building with Convex and AI.";
+const SITE_URL = process.env.SITE_URL || "https://markdowncms.netlify.app";
+const SITE_TITLE = "Markdown Site";
+const SITE_DESCRIPTION =
+ "An open source markdown site powered by Convex and Netlify.";
// Escape XML special characters
function escapeXml(text: string): string {
diff --git a/files.md b/files.md
index 709d974..b9b1136 100644
--- a/files.md
+++ b/files.md
@@ -112,9 +112,12 @@ Markdown files for static pages like About, Projects, Contact.
## Netlify (`netlify/edge-functions/`)
-| File | Description |
-| ------------ | ------------------------------------------------ |
-| `botMeta.ts` | Edge function for social media crawler detection |
+| File | Description |
+| ------------ | ----------------------------------------------------- |
+| `botMeta.ts` | Edge function for social media crawler detection |
+| `rss.ts` | Proxies `/rss.xml` and `/rss-full.xml` to Convex HTTP |
+| `sitemap.ts` | Proxies `/sitemap.xml` to Convex HTTP |
+| `api.ts` | Proxies `/api/posts` and `/api/post` to Convex HTTP |
## Public Assets (`public/`)
diff --git a/index.html b/index.html
index 08a7934..b6db832 100644
--- a/index.html
+++ b/index.html
@@ -40,10 +40,7 @@
-
+
{
+ const convexUrl =
+ Deno.env.get("VITE_CONVEX_URL") || Deno.env.get("CONVEX_URL");
+
+ if (!convexUrl) {
+ return new Response(
+ JSON.stringify({ error: "Configuration error: Missing Convex URL" }),
+ {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Construct the Convex site URL for the HTTP endpoint
+ const convexSiteUrl = convexUrl.replace(".cloud", ".site");
+ const url = new URL(request.url);
+ const targetUrl = `${convexSiteUrl}${url.pathname}${url.search}`;
+
+ try {
+ const response = await fetch(targetUrl, {
+ headers: {
+ Accept: "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ return new Response(JSON.stringify({ error: "API endpoint error" }), {
+ status: response.status,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ const data = await response.text();
+ const contentType =
+ response.headers.get("Content-Type") || "application/json";
+
+ return new Response(data, {
+ headers: {
+ "Content-Type": contentType,
+ "Cache-Control": "public, max-age=300, s-maxage=600",
+ "Access-Control-Allow-Origin": "*",
+ },
+ });
+ } catch {
+ return new Response(JSON.stringify({ error: "Failed to fetch from API" }), {
+ status: 502,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+}
+
+export const config = {
+ path: ["/api/posts", "/api/post"],
+};
diff --git a/netlify/edge-functions/rss.ts b/netlify/edge-functions/rss.ts
new file mode 100644
index 0000000..b6f132e
--- /dev/null
+++ b/netlify/edge-functions/rss.ts
@@ -0,0 +1,53 @@
+import type { Context } from "@netlify/edge-functions";
+
+// Edge function to proxy RSS feed to Convex HTTP endpoint
+export default async function handler(
+ request: Request,
+ _context: Context,
+): Promise {
+ const convexUrl =
+ Deno.env.get("VITE_CONVEX_URL") || Deno.env.get("CONVEX_URL");
+
+ if (!convexUrl) {
+ return new Response("Configuration error: Missing Convex URL", {
+ status: 500,
+ });
+ }
+
+ // Construct the Convex site URL for the HTTP endpoint
+ const convexSiteUrl = convexUrl.replace(".cloud", ".site");
+ const url = new URL(request.url);
+ const targetUrl = `${convexSiteUrl}${url.pathname}`;
+
+ try {
+ const response = await fetch(targetUrl, {
+ headers: {
+ Accept: "application/rss+xml",
+ },
+ });
+
+ if (!response.ok) {
+ return new Response("RSS feed not available", {
+ status: response.status,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+
+ const xml = await response.text();
+ return new Response(xml, {
+ headers: {
+ "Content-Type": "application/rss+xml; charset=utf-8",
+ "Cache-Control": "public, max-age=3600, s-maxage=7200",
+ },
+ });
+ } catch {
+ return new Response("Failed to fetch RSS feed", {
+ status: 502,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+}
+
+export const config = {
+ path: ["/rss.xml", "/rss-full.xml"],
+};
diff --git a/netlify/edge-functions/sitemap.ts b/netlify/edge-functions/sitemap.ts
new file mode 100644
index 0000000..e0fdcde
--- /dev/null
+++ b/netlify/edge-functions/sitemap.ts
@@ -0,0 +1,52 @@
+import type { Context } from "@netlify/edge-functions";
+
+// Edge function to proxy sitemap to Convex HTTP endpoint
+export default async function handler(
+ request: Request,
+ _context: Context,
+): Promise {
+ const convexUrl =
+ Deno.env.get("VITE_CONVEX_URL") || Deno.env.get("CONVEX_URL");
+
+ if (!convexUrl) {
+ return new Response("Configuration error: Missing Convex URL", {
+ status: 500,
+ });
+ }
+
+ // Construct the Convex site URL for the HTTP endpoint
+ const convexSiteUrl = convexUrl.replace(".cloud", ".site");
+ const targetUrl = `${convexSiteUrl}/sitemap.xml`;
+
+ try {
+ const response = await fetch(targetUrl, {
+ headers: {
+ Accept: "application/xml",
+ },
+ });
+
+ if (!response.ok) {
+ return new Response("Sitemap not available", {
+ status: response.status,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+
+ const xml = await response.text();
+ return new Response(xml, {
+ headers: {
+ "Content-Type": "application/xml; charset=utf-8",
+ "Cache-Control": "public, max-age=3600, s-maxage=7200",
+ },
+ });
+ } catch {
+ return new Response("Failed to fetch sitemap", {
+ status: 502,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+}
+
+export const config = {
+ path: "/sitemap.xml",
+};
diff --git a/vite.config.ts b/vite.config.ts
index 3dcfde5..a012abb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,11 +1,46 @@
-import { defineConfig } from "vite";
+import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react()],
- build: {
- outDir: "dist",
- },
-});
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), "");
+ // Convert Convex cloud URL to HTTP site URL
+ const convexUrl = env.VITE_CONVEX_URL || "";
+ const convexSiteUrl = convexUrl.replace(".cloud", ".site");
+ return {
+ plugins: [react()],
+ build: {
+ outDir: "dist",
+ },
+ server: {
+ proxy: {
+ // Proxy RSS and sitemap to Convex HTTP endpoints in development
+ "/rss.xml": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ "/rss-full.xml": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ "/sitemap.xml": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ "/api/posts": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ "/api/post": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ "/meta/post": {
+ target: convexSiteUrl,
+ changeOrigin: true,
+ },
+ },
+ },
+ };
+});