Skip to content

Commit 1474dfa

Browse files
committed
Improve LLM visibility
1 parent 3ab4d2f commit 1474dfa

37 files changed

Lines changed: 524 additions & 30 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@
1818
npm-debug.log*
1919
yarn-debug.log*
2020
yarn-error.log*
21+
22+
# Local Netlify folder
23+
.netlify

docs/intro.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
slug: '/'
33
title: ''
44
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
5-
displayed_sidebar: tutorialSidebar
5+
displayed_sidebar: main
66
---
77

88
<h1>

docusaurus.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Options as PresetClassicOptions,
44
ThemeConfig as PresetClassicThemeConfig,
55
} from "@docusaurus/preset-classic";
6+
import { join } from "node:path";
67

78
import badgeRemarkPlugin from "./src/remark/badge";
89
import codeAnchorRemarkPlugin from "./src/remark/code-anchor";
@@ -28,6 +29,12 @@ const config: Config = {
2829
baseUrl: "/",
2930

3031
onBrokenLinks: "throw",
32+
// Anchors for configuration options are generated dynamically,
33+
// so Docusaurus can't know them in advance.
34+
// It'd be nice to be able to verify anchors, but for now,
35+
// let's just ignore broken anchors instead flooding the build
36+
// output with warnings.
37+
onBrokenAnchors: "ignore",
3138

3239
i18n: {
3340
defaultLocale: "en",
@@ -92,6 +99,8 @@ const config: Config = {
9299
},
93100
],
94101

102+
plugins: [join(__dirname, "src/plugins/llms.ts")],
103+
95104
presets: [
96105
[
97106
"classic",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import type { Config, Context } from "@netlify/edge-functions";
2+
import { extname } from "path";
3+
4+
const LLMS_REWRITES = new Set(["/llms.txt", "/llms-full.txt"]);
5+
6+
export default async function handler(request: Request, context: Context) {
7+
console.log("Received request:", request);
8+
// Only handle GET and HEAD requests
9+
if (request.method !== "GET" && request.method !== "HEAD") return;
10+
11+
// Skip Netlify's internal prerender/CDN requests (HeadlessChrome, CloudFront)
12+
const agentCategory = request.headers.get("netlify-agent-category") || "";
13+
if (agentCategory.includes("prerender")) return;
14+
15+
// Skip our own Algolia crawler — it follows rel="alternate" links and
16+
// would otherwise index the .md variants.
17+
const userAgent = request.headers.get("user-agent") || "";
18+
if (/algolia/i.test(userAgent)) return;
19+
20+
const url = new URL(request.url);
21+
const { pathname } = url;
22+
23+
// Respond with index.md for llms.txt and llms-full.txt,
24+
// as index.md is well suited for this
25+
if (LLMS_REWRITES.has(pathname)) {
26+
return buildTarget("/index.md", url);
27+
}
28+
29+
const ext = extname(pathname);
30+
if (ext === ".html" || ext === ".md") {
31+
// For direct requests to .html or .md files,
32+
// add a link header pointing to the alternate format.
33+
return modifyHeaders(await context.next(), (headers) => {
34+
addAlternateLink(headers, url);
35+
});
36+
} else if (ext) {
37+
// Skip other requests with file extensions,
38+
// as they are static assets that shouldn't have alternate links.
39+
return;
40+
}
41+
42+
// For other requests, check if the client prefers Markdown over HTML.
43+
// If so, try to serve the corresponding Markdown file
44+
// (e.g., /foo -> /foo/index.md).
45+
// If the Markdown file doesn't exist (404),
46+
// continue with the normal request handling.
47+
if (prefersMarkdown(request.headers.get("accept"))) {
48+
const target = buildTarget(joinIndex(pathname), url);
49+
const response = await fetch(target);
50+
if (response.status !== 404) return finalize(response, url);
51+
}
52+
53+
// For all other cases, proceed with the normal request handling.
54+
return finalize(await context.next(), url);
55+
}
56+
57+
function buildTarget(pathname: string, base: URL): URL {
58+
const target = new URL(pathname, base);
59+
target.search = base.search;
60+
return target;
61+
}
62+
63+
function joinIndex(pathname: string): string {
64+
return pathname.replace(/\/?$/, "/") + "index.md";
65+
}
66+
67+
function prefersMarkdown(accept: string | null): boolean {
68+
if (!accept) return false;
69+
70+
let markdownQ = -1;
71+
let htmlQ = -1;
72+
let textQ = -1;
73+
let anyQ = -1;
74+
75+
for (const part of accept.split(",")) {
76+
const segments = part.trim().split(";");
77+
const type = segments[0].trim().toLowerCase();
78+
if (!type) continue;
79+
80+
let q = 1;
81+
for (let i = 1; i < segments.length; i++) {
82+
const param = segments[i].trim();
83+
if (!param.startsWith("q=")) continue;
84+
const value = Number.parseFloat(param.slice(2));
85+
if (!Number.isNaN(value)) q = value;
86+
}
87+
88+
if (type === "text/markdown") {
89+
if (q > markdownQ) markdownQ = q;
90+
} else if (type === "text/html") {
91+
if (q > htmlQ) htmlQ = q;
92+
} else if (type === "text/*") {
93+
if (q > textQ) textQ = q;
94+
} else if (type === "*/*") {
95+
if (q > anyQ) anyQ = q;
96+
}
97+
}
98+
99+
if (htmlQ < 0) htmlQ = textQ > 0 ? textQ : anyQ;
100+
101+
return markdownQ > 0 && markdownQ >= htmlQ;
102+
}
103+
104+
function finalize(response: Response, url: URL): Response {
105+
return modifyHeaders(response, (headers) => {
106+
appendVary(headers, "Accept");
107+
addAlternateLink(headers, new URL(response.url, url));
108+
});
109+
}
110+
111+
function modifyHeaders(
112+
response: Response,
113+
fn: (headers: Headers) => void,
114+
): Response {
115+
const headers = new Headers(response.headers);
116+
117+
fn(headers);
118+
119+
return new Response(response.body, {
120+
status: response.status,
121+
statusText: response.statusText,
122+
headers,
123+
});
124+
}
125+
126+
function appendVary(headers: Headers, value: string) {
127+
const existing = headers.get("vary");
128+
if (!existing) {
129+
headers.set("vary", value);
130+
return;
131+
}
132+
const tokens = existing.split(",").map((s) => s.trim());
133+
if (tokens.some((t) => t.toLowerCase() === value.toLowerCase())) return;
134+
headers.set("vary", `${existing}, ${value}`);
135+
}
136+
137+
function addAlternateLink(headers: Headers, url: URL) {
138+
let alternatePath: string | null = null;
139+
let alternateType = "text/markdown";
140+
141+
const ext = extname(url.pathname);
142+
if (ext === ".html") {
143+
alternatePath = url.pathname.replace(/\.html$/, ".md");
144+
} else if (ext === ".md") {
145+
alternatePath = url.pathname.replace(/\.md$/, ".html");
146+
alternateType = "text/html";
147+
} else if (ext === "") {
148+
alternatePath = joinIndex(url.pathname);
149+
}
150+
151+
if (!alternatePath) return;
152+
153+
const alternateUrl = buildTarget(alternatePath, url);
154+
155+
const link = `<${alternateUrl}>; rel="alternate"; type="${alternateType}"`;
156+
headers.set("link", link);
157+
}
158+
159+
export const config: Config = {
160+
path: "/*",
161+
excludedPath: [
162+
"/**/*.js",
163+
"/**/*.css",
164+
"/**/*.png",
165+
"/**/*.jpg",
166+
"/**/*.jpeg",
167+
"/**/*.svg",
168+
"/**/*.ico",
169+
"/**/*.xml",
170+
"/robots.txt",
171+
"/404.html",
172+
"/_redirects",
173+
"/.nojekyll",
174+
],
175+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@docusaurus/types": "^3.9.2",
3434
"@eslint/js": "^9.39.1",
3535
"@evilmartians/lefthook": "^2.0.4",
36+
"@netlify/edge-functions": "^3.0.6",
3637
"@types/mdast": "4.0.4",
3738
"eslint": "^9.39.1",
3839
"eslint-config-prettier": "^10.1.8",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sidebars.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SidebarsConfig } from "@docusaurus/plugin-content-docs";
22

33
const sidebars: SidebarsConfig = {
4-
tutorialSidebar: [
4+
main: [
55
"getting_started",
66
{
77
type: "link",

0 commit comments

Comments
 (0)