From b43f8ff2f4986f6c3f8fb1ab807cff05e405e1a2 Mon Sep 17 00:00:00 2001 From: Wayne Sutton Date: Wed, 24 Dec 2025 00:55:55 -0800 Subject: [PATCH] fix(api): use JS Netlify Function with query param for /api/raw endpoint --- netlify.toml | 2 +- netlify/functions/raw.js | 77 +++++++++++++++++++++++++++ netlify/functions/raw.ts | 111 --------------------------------------- 3 files changed, 78 insertions(+), 112 deletions(-) create mode 100644 netlify/functions/raw.js delete mode 100644 netlify/functions/raw.ts diff --git a/netlify.toml b/netlify.toml index c80a5ec..c4359c6 100644 --- a/netlify.toml +++ b/netlify.toml @@ -8,7 +8,7 @@ # API raw markdown endpoint for AI tools (ChatGPT, Claude, Perplexity) [[redirects]] from = "/api/raw/*" - to = "/.netlify/functions/raw/:splat" + to = "/.netlify/functions/raw?slug=:splat" status = 200 force = true diff --git a/netlify/functions/raw.js b/netlify/functions/raw.js new file mode 100644 index 0000000..ab0a6a7 --- /dev/null +++ b/netlify/functions/raw.js @@ -0,0 +1,77 @@ +const fs = require("fs"); +const path = require("path"); + +/** + * Netlify Function: /api/raw/:slug + * + * Serves raw markdown files for AI tools (ChatGPT, Claude, Perplexity). + * Returns text/plain with minimal headers for reliable AI ingestion. + */ + +function normalizeSlug(input) { + return (input || "").trim().replace(/^\/+|\/+$/g, ""); +} + +function tryRead(p) { + try { + if (!fs.existsSync(p)) return null; + const body = fs.readFileSync(p, "utf8"); + if (!body || body.trim().length === 0) return null; + return body; + } catch { + return null; + } +} + +exports.handler = async (event) => { + const slugRaw = + event.queryStringParameters && event.queryStringParameters.slug; + const slug = normalizeSlug(slugRaw); + + if (!slug) { + return { + statusCode: 400, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Access-Control-Allow-Origin": "*", + }, + body: "missing slug", + }; + } + + const filename = slug.endsWith(".md") ? slug : `${slug}.md`; + const root = process.cwd(); + + const candidates = [ + path.join(root, "public", "raw", filename), + path.join(root, "dist", "raw", filename), + ]; + + let body = null; + for (const p of candidates) { + body = tryRead(p); + if (body) break; + } + + if (!body) { + return { + statusCode: 404, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Access-Control-Allow-Origin": "*", + }, + body: `not found: ${filename}`, + }; + } + + return { + statusCode: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=3600", + }, + body, + }; +}; + diff --git a/netlify/functions/raw.ts b/netlify/functions/raw.ts deleted file mode 100644 index badc53f..0000000 --- a/netlify/functions/raw.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; - -/** - * Netlify Function: /api/raw/:slug - * - * Serves raw markdown files for AI tools (ChatGPT, Claude, Perplexity). - * Returns text/plain with minimal headers for reliable AI ingestion. - */ - -// Inline types for Netlify Functions (avoids external dependency) -interface HandlerEvent { - path: string; - httpMethod: string; -} - -interface HandlerResponse { - statusCode: number; - headers: Record; - body: string; -} - -// Response headers optimized for AI crawlers -const AI_HEADERS: Record = { - "Content-Type": "text/plain; charset=utf-8", - "Access-Control-Allow-Origin": "*", - "Cache-Control": "public, max-age=3600", - // No Link, X-Robots-Tag, or SEO headers -}; - -// Extract slug from path like /api/raw/my-post or /.netlify/functions/raw/my-post -function extractSlug(rawPath: string): string | null { - // Handle both /api/raw/:slug and /.netlify/functions/raw/:slug patterns - const patterns = [/^\/api\/raw\/(.+)$/, /^\/.netlify\/functions\/raw\/(.+)$/]; - - for (const pattern of patterns) { - const match = rawPath.match(pattern); - if (match && match[1]) { - // Remove .md extension if present - return match[1].replace(/\.md$/, ""); - } - } - - return null; -} - -// Try to read markdown file from multiple locations -function readMarkdownFile(slug: string): string | null { - // Possible file locations (in order of priority) - const locations = [ - // Production: built output - path.join(process.cwd(), "dist", "raw", `${slug}.md`), - // Dev/Preview: source files - path.join(process.cwd(), "public", "raw", `${slug}.md`), - ]; - - for (const filePath of locations) { - try { - if (fs.existsSync(filePath)) { - return fs.readFileSync(filePath, "utf-8"); - } - } catch { - // Continue to next location - } - } - - return null; -} - -// Netlify Function handler -const handler = async (event: HandlerEvent): Promise => { - // Only allow GET requests - if (event.httpMethod !== "GET") { - return { - statusCode: 405, - headers: AI_HEADERS, - body: "Method not allowed. Use GET.", - }; - } - - // Extract slug from path - const slug = extractSlug(event.path); - - if (!slug) { - return { - statusCode: 400, - headers: AI_HEADERS, - body: "Bad request. Usage: /api/raw/{slug}", - }; - } - - // Try to read the markdown file - const content = readMarkdownFile(slug); - - if (!content) { - return { - statusCode: 404, - headers: AI_HEADERS, - body: `Not found: ${slug}.md`, - }; - } - - // Return the raw markdown content - return { - statusCode: 200, - headers: AI_HEADERS, - body: content, - }; -}; - -export { handler };