Skip to content

Commit 41753ef

Browse files
committed
Improve LLM visibility
1 parent 3ab4d2f commit 41753ef

37 files changed

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

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)