mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
fix(api): use JS Netlify Function with query param for /api/raw endpoint
This commit is contained in:
@@ -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
77
netlify/functions/raw.js
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user