mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
feat(api): add /api/raw/:slug endpoint for AI tools (ChatGPT, Claude, Perplexity)
- Create netlify/functions/raw.ts Netlify Function - Returns text/plain with minimal headers for reliable AI ingestion - Reads from dist/raw/ (production) or public/raw/ (dev/preview) - Update CopyPageDropdown to use /api/raw/:slug for AI services - Keep /raw/:slug.md for View as Markdown browser viewing - Add @netlify/functions dev dependency
This commit is contained in:
24
changelog.md
24
changelog.md
@@ -4,6 +4,30 @@ 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.24.9] - 2025-12-24
|
||||
|
||||
### Added
|
||||
|
||||
- Safety-net raw markdown endpoint for AI tools (`/api/raw/:slug`)
|
||||
- New Netlify Function at `netlify/functions/raw.ts`
|
||||
- Returns `text/plain` with minimal headers for reliable AI ingestion
|
||||
- Reads from `dist/raw/` (production) or `public/raw/` (dev/preview)
|
||||
- Handles 400 (missing slug), 404 (not found), and 200 (success) responses
|
||||
- No Link, X-Robots-Tag, or SEO headers that cause AI fetch failures
|
||||
|
||||
### Changed
|
||||
|
||||
- AI service links (ChatGPT, Claude, Perplexity) now use `/api/raw/:slug` instead of `/raw/:slug.md`
|
||||
- Netlify Function endpoint more reliable for AI crawler fetch
|
||||
- "View as Markdown" menu item still uses `/raw/:slug.md` for browser viewing
|
||||
|
||||
### Technical
|
||||
|
||||
- `netlify/functions/raw.ts`: New Netlify Function to serve raw markdown
|
||||
- `netlify.toml`: Added redirect from `/api/raw/*` to the function
|
||||
- `src/components/CopyPageDropdown.tsx`: AI services use `/api/raw/:slug` endpoint
|
||||
- `package.json`: Added `@netlify/functions` dev dependency
|
||||
|
||||
## [1.24.8] - 2025-12-23
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
# API raw markdown endpoint for AI tools (ChatGPT, Claude, Perplexity)
|
||||
[[redirects]]
|
||||
from = "/api/raw/*"
|
||||
to = "/.netlify/functions/raw/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
# Raw markdown passthrough - explicit rule prevents SPA fallback from intercepting
|
||||
[[redirects]]
|
||||
from = "/raw/*"
|
||||
|
||||
106
netlify/functions/raw.ts
Normal file
106
netlify/functions/raw.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";
|
||||
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.
|
||||
*/
|
||||
|
||||
// Response headers optimized for AI crawlers
|
||||
const AI_HEADERS = {
|
||||
"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;
|
||||
}
|
||||
|
||||
const handler: Handler = async (
|
||||
event: HandlerEvent,
|
||||
_context: HandlerContext,
|
||||
) => {
|
||||
// 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 };
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"remark-gfm": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@netlify/functions": "^2.8.2",
|
||||
"@types/node": "^25.0.2",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
|
||||
@@ -322,27 +322,18 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
};
|
||||
|
||||
// Generic handler for opening AI services
|
||||
// Uses raw markdown URL for better AI parsing
|
||||
// Uses /api/raw/:slug endpoint for AI tools (ChatGPT, Claude, Perplexity)
|
||||
// IMPORTANT: window.open must happen BEFORE any await to avoid popup blockers
|
||||
const handleOpenInAI = async (service: AIService) => {
|
||||
// Use raw markdown URL for better AI parsing
|
||||
// Use /api/raw/:slug endpoint for AI tools - more reliable than static /raw/*.md files
|
||||
if (service.buildUrlFromRawMarkdown) {
|
||||
// Build absolute raw markdown URL using current origin (not props.url)
|
||||
// This ensures correct URL even if props.url points to canonical/deploy preview domain
|
||||
const rawMarkdownUrl = new URL(
|
||||
`/raw/${props.slug}.md`,
|
||||
// Build absolute API URL using current origin
|
||||
// Uses Netlify Function endpoint that returns text/plain with minimal headers
|
||||
const apiRawUrl = new URL(
|
||||
`/api/raw/${props.slug}`,
|
||||
window.location.origin,
|
||||
).toString();
|
||||
const targetUrl = service.buildUrlFromRawMarkdown(rawMarkdownUrl);
|
||||
|
||||
// For ChatGPT, add fallback if URL fetch fails
|
||||
if (service.id === "chatgpt") {
|
||||
// Open ChatGPT with raw URL
|
||||
window.open(targetUrl, "_blank");
|
||||
setIsOpen(false);
|
||||
// Optional: Could add error handling here if needed
|
||||
return;
|
||||
}
|
||||
const targetUrl = service.buildUrlFromRawMarkdown(apiRawUrl);
|
||||
|
||||
window.open(targetUrl, "_blank");
|
||||
setIsOpen(false);
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
--font-size-mobile-nav-link: var(--font-size-base);
|
||||
--font-size-mobile-home-link: var(--font-size-md);
|
||||
--font-size-mobile-toc-title: var(--font-size-2xs);
|
||||
--font-size-mobile-toc-link: var(--font-size-sm);
|
||||
--font-size-mobile-toc-link: var(--font-size-md);
|
||||
|
||||
/* Copy dropdown font sizes */
|
||||
--font-size-copy-trigger: var(--font-size-sm);
|
||||
@@ -3811,7 +3811,7 @@ body {
|
||||
}
|
||||
.mobile-menu-toc-level-3 {
|
||||
padding-left: 28px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.mobile-menu-toc-level-4,
|
||||
.mobile-menu-toc-level-5,
|
||||
|
||||
Reference in New Issue
Block a user