fix(api): use JS Netlify Function with query param for /api/raw endpoint

This commit is contained in:
Wayne Sutton
2025-12-24 00:55:55 -08:00
parent 6cbe5006ac
commit b43f8ff2f4
3 changed files with 78 additions and 112 deletions

View File

@@ -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

77
netlify/functions/raw.js Normal file
View File

@@ -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,
};
};

View File

@@ -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<string, string>;
body: string;
}
// Response headers optimized for AI crawlers
const AI_HEADERS: Record<string, string> = {
"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<HandlerResponse> => {
// 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 };