diff --git a/.gitignore b/.gitignore
index b2d6de3..2137a23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+# Local Netlify folder
+.netlify
diff --git a/docs/intro.mdx b/docs/intro.mdx
index f2750ba..2df91c8 100644
--- a/docs/intro.mdx
+++ b/docs/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index 4b42ff3..a2b7b79 100644
--- a/docusaurus.config.ts
+++ b/docusaurus.config.ts
@@ -3,6 +3,7 @@ import {
Options as PresetClassicOptions,
ThemeConfig as PresetClassicThemeConfig,
} from "@docusaurus/preset-classic";
+import { join } from "node:path";
import badgeRemarkPlugin from "./src/remark/badge";
import codeAnchorRemarkPlugin from "./src/remark/code-anchor";
@@ -28,6 +29,12 @@ const config: Config = {
baseUrl: "/",
onBrokenLinks: "throw",
+ // Anchors for configuration options are generated dynamically,
+ // so Docusaurus can't know them in advance.
+ // It'd be nice to be able to verify anchors, but for now,
+ // let's just ignore broken anchors instead flooding the build
+ // output with warnings.
+ onBrokenAnchors: "ignore",
i18n: {
defaultLocale: "en",
@@ -92,6 +99,8 @@ const config: Config = {
},
],
+ plugins: [join(__dirname, "src/plugins/llms.ts")],
+
presets: [
[
"classic",
diff --git a/netlify/edge-functions/llm-middleware.ts b/netlify/edge-functions/llm-middleware.ts
new file mode 100644
index 0000000..0c69357
--- /dev/null
+++ b/netlify/edge-functions/llm-middleware.ts
@@ -0,0 +1,223 @@
+import type { Config, Context } from "@netlify/edge-functions";
+import { extname } from "path";
+
+const ALLOWED_HTTP_METHODS = new Set(["GET", "HEAD"]);
+const LLMS_REWRITES = new Set(["/llms.txt", "/llms-full.txt"]);
+
+export const config: Config = {
+ // This middleware should run for all paths, but we explicitly exclude common static asset types
+ // and some specific files to avoid unnecessary middleware execution
+ path: "/*",
+ excludedPath: [
+ "/**/*.js",
+ "/**/*.css",
+ "/**/*.png",
+ "/**/*.jpg",
+ "/**/*.jpeg",
+ "/**/*.svg",
+ "/**/*.ico",
+ "/**/*.xml",
+ "/img/**",
+ "/robots.txt",
+ "/404.html",
+ "/_redirects",
+ "/.nojekyll",
+ ],
+};
+
+// This middleware serves Markdown content to clients that prefer it (like LLMs),
+// while still supporting regular HTML for browsers and other clients.
+// It also adds Link headers to indicate alternate formats and ensures proper Vary headers.
+export default async function handler(request: Request, context: Context) {
+ try {
+ // Only handle allowed HTTP methods
+ if (!ALLOWED_HTTP_METHODS.has(request.method)) return;
+
+ // Skip our own Algolia crawler — it follows rel="alternate" links and
+ // would otherwise index the .md variants.
+ const userAgent = request.headers.get("user-agent") || "";
+ if (/algolia/i.test(userAgent)) return;
+
+ const url = new URL(request.url);
+ const { pathname } = url;
+
+ // Respond with index.md for llms.txt and llms-full.txt,
+ // as index.md is well suited for this purpose
+ if (LLMS_REWRITES.has(pathname)) {
+ return buildTarget("/index.md", url);
+ }
+
+ const ext = extname(pathname);
+ if (ext === ".html" || ext === ".md") {
+ // For direct requests to .html or .md files,
+ // add a link header pointing to the alternate format.
+ return modifyHeaders(await context.next(), (headers) => {
+ addAlternateLink(headers, url);
+ });
+ } else if (ext) {
+ // Skip other requests with file extensions,
+ // as they are static assets that shouldn't have alternate links.
+ return;
+ }
+
+ // For other requests, check if the client prefers Markdown over HTML.
+ // If so, try to serve the corresponding Markdown file
+ // (e.g., /foo -> /foo/index.md).
+ // If the Markdown file doesn't exist (404),
+ // continue with the normal request handling.
+ if (prefersMarkdown(request.headers.get("accept"))) {
+ const target = buildTarget(joinIndexMD(pathname), url);
+ const response = await fetch(target);
+ if (response.status !== 404) return finalize(response, url);
+ }
+
+ // For all other cases, proceed with the normal request handling.
+ return finalize(await context.next(), url);
+ } catch (error) {
+ console.error("Error in LLM middleware:", error);
+ // In case of any error, proceed with the normal request handling
+ return context.next();
+ }
+}
+
+// Helper function to build a target URL based on the original URL and a new pathname,
+// while preserving the search parameters.
+function buildTarget(pathname: string, base: URL): URL {
+ const target = new URL(pathname, base);
+ target.search = base.search;
+ return target;
+}
+
+// Helper function to convert a pathname to its corresponding index.md path.
+function joinIndexMD(pathname: string): string {
+ return pathname.replace(/\/?$/, "/") + "index.md";
+}
+
+// Parses the Accept header to determine if the client prefers Markdown over HTML.
+function prefersMarkdown(accept: string | null): boolean {
+ if (!accept) return false;
+
+ // Quality values (q) indicate the client's preference for different content types.
+ // Values less than 0 mean that the type wasn't found in the Accept header
+ let markdownQ = -1;
+ let htmlQ = -1;
+ let textQ = -1;
+ let anyQ = -1;
+
+ // Parse the Accept header, which can contain multiple content types with optional quality values.
+ for (const part of accept.split(",")) {
+ // Each part can have parameters separated by semicolons, e.g., "text/html; q=0.9".
+ const segments = part.trim().split(";");
+ const type = segments[0].trim().toLowerCase();
+ if (!type) continue;
+
+ // Default quality value is 1 if the type is present without an explicit q parameter.
+ let q = 1;
+ // Look for a q parameter in the segments to determine the quality value for this content type.
+ for (let i = 1; i < segments.length; i++) {
+ const param = segments[i].trim();
+ if (!param.startsWith("q=")) continue;
+ const value = Number.parseFloat(param.slice(2));
+ if (!Number.isNaN(value)) q = value;
+ }
+
+ // Update the quality values for the relevant content types based on the parsed Accept header.
+ if (type === "text/markdown") {
+ markdownQ = Math.max(q, markdownQ);
+ } else if (type === "text/html") {
+ htmlQ = Math.max(q, htmlQ);
+ } else if (type === "text/*") {
+ textQ = Math.max(q, textQ);
+ } else if (type === "*/*") {
+ anyQ = Math.max(q, anyQ);
+ }
+ }
+
+ // If "text/html" isn't explicitly listed,
+ // use the quality values of "text/*" and "*/*" as a fallback for HTML,
+ if (htmlQ < 0) htmlQ = textQ > 0 ? textQ : anyQ;
+
+ // Markdown is preferred if it was explicitly listed with a quality value greater than 0,
+ // and its quality value is greater than or equal to that of HTML.
+ return markdownQ > 0 && markdownQ >= htmlQ;
+}
+
+// Finalize the response by adding necessary headers.
+// This function should be used only for responses to paths without file extensions
+// (e.g., /foo or /foo/).
+// For responses to direct requests to .html or .md files, the alternate link header
+// is added in the main handler function, and this finalize function is not used.
+function finalize(response: Response, url: URL): Response {
+ return modifyHeaders(response, (headers) => {
+ // Add "Accept" to the Vary header to indicate that the response may vary
+ // based on the Accept header, which is important for caching CDNs and browsers
+ // to work correctly with content negotiation.
+ appendVary(headers, "Accept");
+ // Add a Link header pointing to the alternate format (Markdown or HTML)
+ // for clients that can handle it.
+ addAlternateLink(headers, new URL(response.url, url));
+ });
+}
+
+// Helper function to create a new Response with modified headers based on an existing Response.
+function modifyHeaders(
+ response: Response,
+ fn: (headers: Headers) => void,
+): Response {
+ const headers = new Headers(response.headers);
+
+ fn(headers);
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+}
+
+// Helper function to append a value to the Vary header, ensuring that it doesn't create duplicates.
+function appendVary(headers: Headers, value: string) {
+ const existing = headers.get("vary");
+
+ // If there's no existing Vary header, just set it to the new value.
+ if (!existing) {
+ headers.set("vary", value);
+ return;
+ }
+
+ // If the Vary header already includes the value (case-insensitive), do nothing to avoid duplicates.
+ const tokens = existing.split(",").map((s) => s.trim());
+ if (tokens.some((t) => t.toLowerCase() === value.toLowerCase())) return;
+
+ // Otherwise, append the new value to the existing Vary header.
+ headers.set("vary", `${existing}, ${value}`);
+}
+
+// Helper function to add a Link header pointing to the alternate format (Markdown or HTML)
+// for a given URL.
+function addAlternateLink(headers: Headers, url: URL) {
+ let alternatePath: string | null = null;
+ let alternateType = "text/markdown";
+
+ const ext = extname(url.pathname);
+ if (ext === ".html") {
+ // For an HTML page, the alternate format is the corresponding Markdown file.
+ alternatePath = url.pathname.replace(/\.html$/, ".md");
+ } else if (ext === ".md") {
+ // For a Markdown page, the alternate format is the corresponding HTML file.
+ alternatePath = url.pathname.replace(/\.md$/, ".html");
+ alternateType = "text/html";
+ } else if (ext === "") {
+ // Paths without an extension are most likely point to /path/index.html,
+ // so we should add /index.md to it as the alternate path.
+ alternatePath = joinIndexMD(url.pathname);
+ }
+
+ // If we couldn't determine a valid alternate path, don't add a Link header.
+ if (!alternatePath) return;
+
+ // Build the full URL for the alternate format and add a Link header.
+ const alternateUrl = buildTarget(alternatePath, url);
+ const link = `<${alternateUrl}>; rel="alternate"; type="${alternateType}"`;
+ headers.set("link", link);
+}
diff --git a/package.json b/package.json
index eb62f78..7d4284c 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@docusaurus/types": "^3.9.2",
"@eslint/js": "^9.39.1",
"@evilmartians/lefthook": "^2.0.4",
+ "@netlify/edge-functions": "^3.0.6",
"@types/mdast": "4.0.4",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4afff0..9413eac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,9 @@ importers:
'@evilmartians/lefthook':
specifier: ^2.0.4
version: 2.0.4
+ '@netlify/edge-functions':
+ specifier: ^3.0.6
+ version: 3.0.6
'@types/mdast':
specifier: 4.0.4
version: 4.0.4
@@ -1399,6 +1402,14 @@ packages:
'@types/react': '>=16'
react: '>=16'
+ '@netlify/edge-functions@3.0.6':
+ resolution: {integrity: sha512-xkVcTcpAuQKAY5GXKOjPTIct5Mz53NPHXOasggA+LTAxDDV4ohqSM8BIaXh1SgbcniHZyFhBqhc5hxZ+fFz5bQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@netlify/types@2.6.0':
+ resolution: {integrity: sha512-yD20EizHJDQxajJ66Vo8RTwLwR2jMNVxufPG8MHd2AScX8jW4z0VPnnJHArq2GYPFTFZRHmiAhDrXr5m8zof6w==}
+ engines: {node: ^18.14.0 || >=20}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -7904,6 +7915,12 @@ snapshots:
'@types/react': 19.2.6
react: 18.3.1
+ '@netlify/edge-functions@3.0.6':
+ dependencies:
+ '@netlify/types': 2.6.0
+
+ '@netlify/types@2.6.0': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
diff --git a/sidebars.ts b/sidebars.ts
index 472d736..fc37b32 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1,7 +1,7 @@
import { SidebarsConfig } from "@docusaurus/plugin-content-docs";
const sidebars: SidebarsConfig = {
- tutorialSidebar: [
+ main: [
"getting_started",
{
type: "link",
diff --git a/src/plugins/llms.ts b/src/plugins/llms.ts
new file mode 100644
index 0000000..f2a771e
--- /dev/null
+++ b/src/plugins/llms.ts
@@ -0,0 +1,354 @@
+import { Plugin, RouteConfig } from "@docusaurus/types";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+
+const DOCS_PLUGIN_NAME = "docusaurus-plugin-content-docs";
+
+type SidebarItem = {
+ type: string;
+ label?: string;
+ href?: string;
+ items?: SidebarItem[];
+};
+
+type VersionContext = {
+ name: string;
+ label: string;
+ path: string;
+ isLast: boolean;
+ sidebar?: SidebarItem[];
+};
+
+type RouteEntry = {
+ path: string;
+ sourceFilePath?: string;
+ isCategory: boolean;
+ description?: string;
+ version: VersionContext;
+};
+
+// This plugin makes the docs better suited for consumption by LLMs.
+// It generates markdown files for each documentation page, including category index pages,
+// and injects alternate link tags in the HTML head to point to the markdown versions.
+//
+// The llm-middleware edge function can then rewrite requests for HTML pages to their
+// markdown counterparts if the client prefers markdown (based on the Accept header).
+export default function docusaurusLLMsPlugin(): Plugin {
+ return {
+ name: "docusaurus-llms-plugin",
+
+ // Inject an alternate link tag for the markdown version of the page to HTML head
+ injectHtmlTags() {
+ return {
+ headTags: [
+ {
+ tagName: "link",
+ attributes: {
+ rel: "alternate",
+ type: "text/markdown",
+ href: "index.md",
+ },
+ },
+ ],
+ };
+ },
+
+ // After the build is complete, generate markdown files for each documentation page
+ async postBuild({ routes, siteDir, outDir }): Promise {
+ // Find the root route for the docs plugin.
+ // Other plugins may also add routes, but we only care about the ones from the docs plugin.
+ const docsRoot = routes.find(
+ (route) => route.plugin.name === DOCS_PLUGIN_NAME,
+ );
+ if (!docsRoot) return;
+
+ // Collect all routes that have a source file or are category indexes,
+ // along with their version context.
+ const routeEntries = new Map();
+ for (const route of docsRoot.routes ?? [])
+ collectRoutes(route, undefined, routeEntries);
+
+ // for (const route of routeEntries.values()) {
+ // const versionLabel = route.version.isLast
+ // ? `${route.version.label} (last)`
+ // : route.version.label;
+ // const source = route.isCategory
+ // ? "(category)"
+ // : `<- ${route.sourceFilePath}`;
+ // console.log(`${route.path} [${versionLabel}] ${source}`);
+ // }
+
+ // Cache rendered sidebars by version name to avoid redundant rendering.
+ const renderedSidebars = new Map();
+ const getRenderedSidebar = (
+ version: VersionContext,
+ ): string | undefined => {
+ if (!version.sidebar?.length) return undefined;
+
+ const cached = renderedSidebars.get(version.name);
+ if (cached !== undefined) return cached;
+
+ const rendered = renderSidebar(version.sidebar);
+ renderedSidebars.set(version.name, rendered);
+ return rendered;
+ };
+
+ // Group routes by their path relative to the version root,
+ // so we can easily find "peer" routes in other versions.
+ const entriesByRelPath = new Map();
+ for (const route of routeEntries.values()) {
+ const relPath = versionRelativePath(route.path, route.version.path);
+ const list = entriesByRelPath.get(relPath) ?? [];
+ list.push(route);
+ entriesByRelPath.set(relPath, list);
+ }
+
+ // Ranking function to determine the order of versions in the "Other versions" section.
+ // "Last" version should come first, then "current" (unstable), then the rest sorted by label.
+ const versionRank = (entry: RouteEntry): number => {
+ if (entry.version.isLast) return 0;
+ if (entry.version.name === "current") return 1;
+ return 2;
+ };
+
+ // Render a list of links to the same page in other versions, if they exist.
+ const renderOtherVersions = (route: RouteEntry): string | undefined => {
+ // The "peer" routes are those that share the same relative path
+ // within their respective versions.
+ const relPath = versionRelativePath(route.path, route.version.path);
+ const peers = entriesByRelPath.get(relPath);
+ if (!peers) return undefined;
+
+ // Remove the current route from the list and sort the others by rank and label.
+ const sorted = peers
+ .filter((peer) => peer !== route)
+ .sort((a, b) => {
+ const rankDiff = versionRank(a) - versionRank(b);
+ if (rankDiff !== 0) return rankDiff;
+ return b.version.label.localeCompare(a.version.label);
+ });
+
+ // Render each peer as a markdown link with an appropriate label.
+ const lines = sorted.map((peer) => {
+ let label = peer.version.label;
+ if (peer.version.isLast) label += " (current)";
+ else if (peer.version.name === "current") label += " (unstable)";
+ return `- [${label}](${peer.path})`;
+ });
+
+ // Only return the section if there are other versions to link to.
+ return lines.length ? lines.join("\n") : undefined;
+ };
+
+ // Generate the markdown files for each route entry.
+ for (const route of routeEntries.values()) {
+ let content: string | undefined;
+
+ if (route.sourceFilePath) {
+ // For regular doc pages, load the source markdown file and clean it up.
+ content = await loadMarkdown(join(siteDir, route.sourceFilePath));
+ } else if (route.isCategory) {
+ // For category index pages, build the content from the sidebar items and description.
+ const category = findCategory(route.version.sidebar, route.path);
+ if (category)
+ content = buildCategoryContent(category, route.description);
+ }
+
+ // No content? Skip generating the file
+ if (!content) continue;
+
+ // Write the content to the appropriate location in the output directory.
+ // Add sidebar and other versions sections if applicable.
+ const dest = join(outDir, route.path, "index.md");
+ await writeMarkdown(
+ dest,
+ content,
+ getRenderedSidebar(route.version),
+ renderOtherVersions(route),
+ );
+ }
+ },
+ };
+}
+
+// Extract version information from a route's props, if available.
+function extractVersion(route: RouteConfig): VersionContext | undefined {
+ const version = route.props?.version as
+ | {
+ version?: string;
+ label?: string;
+ isLast?: boolean;
+ docsSidebars?: Record;
+ }
+ | undefined;
+
+ if (!version?.version || !version?.label) return undefined;
+
+ return {
+ name: version.version,
+ label: version.label,
+ path: route.path,
+ isLast: !!version.isLast,
+ sidebar: version.docsSidebars?.main,
+ };
+}
+
+// Load the markdown content from the source file and perform some cleanup
+async function loadMarkdown(sourcePath: string): Promise {
+ const source = await readFile(sourcePath, "utf8");
+ return replaceImageOnlyH1(stripFrontmatter(source)).replace(/^\s+/, "");
+}
+
+// Remove frontmatter from the markdown content, as it's not needed for
+// LLM consumption and may contain irrelevant metadata.
+function stripFrontmatter(content: string): string {
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
+}
+
+// Replace H1 headings that contain only an image with a placeholder text.
+// The root page uses a logo image as the H1, which doesn't provide useful information for LLMs,
+// so we replace it with the "imgproxy" heading
+function replaceImageOnlyH1(content: string): string {
+ return content.replace(/]*>([\s\S]*?)<\/h1>/g, (match, inner) => {
+ const remainder = inner.replace(/
]*\/?>/g, "").trim();
+ return remainder === "" ? "# imgproxy" : match;
+ });
+}
+
+// Recursively render the sidebar items as a markdown list,
+// with proper indentation for nested categories.
+function renderSidebar(items: SidebarItem[], depth = 0): string {
+ const indent = " ".repeat(depth);
+ const lines: string[] = [];
+
+ for (const item of items) {
+ if (!item.label) continue;
+
+ const line = item.href
+ ? `${indent}- [${item.label}](${item.href})`
+ : `${indent}- ${item.label}`;
+ lines.push(line);
+
+ if (item.items?.length) lines.push(renderSidebar(item.items, depth + 1));
+ }
+
+ return lines.join("\n");
+}
+
+// Write the generated markdown content to the specified destination path,
+// creating any necessary directories along the way.
+// Optionally include rendered sidebar and other versions sections if provided.
+async function writeMarkdown(
+ destPath: string,
+ content: string,
+ renderedSidebar?: string,
+ renderedOtherVersions?: string,
+): Promise {
+ // Use only the original content by default
+ const parts = [content.trimEnd()];
+
+ // If there's a sidebar or other versions to include,
+ // add a delimiter and section headers before appending them to the content.
+ if (renderedSidebar || renderedOtherVersions) parts.push("", "---");
+
+ // Append the rendered sidebar if it exists, under a "Navigation" header
+ if (renderedSidebar) parts.push("", "## Navigation", "", renderedSidebar);
+
+ // Append the links to other versions if they exist, under an "Other versions" header
+ if (renderedOtherVersions)
+ parts.push("", "## Other versions", "", renderedOtherVersions);
+
+ // Ensure the destination directory exists and write the content to the file
+ await mkdir(dirname(destPath), { recursive: true });
+ await writeFile(destPath, `${parts.join("\n")}\n`);
+}
+
+// Calculate the path of a route relative to its version root
+function versionRelativePath(routePath: string, versionPath: string): string {
+ if (versionPath === "/" || versionPath === "") return routePath;
+ if (routePath === versionPath) return "/";
+ if (routePath.startsWith(`${versionPath}/`))
+ return routePath.slice(versionPath.length);
+ return routePath;
+}
+
+// Recursively search the sidebar items to find the category that corresponds to the given href
+function findCategory(
+ items: SidebarItem[] | undefined,
+ href: string,
+): SidebarItem | undefined {
+ if (!items) return undefined;
+
+ for (const item of items) {
+ if (item.type === "category" && item.href === href) return item;
+
+ const found = findCategory(item.items, href);
+ if (found) return found;
+ }
+
+ return undefined;
+}
+
+// Build the markdown content for a category index page based on its sidebar items and description
+function buildCategoryContent(
+ category: SidebarItem,
+ description: string | undefined,
+): string {
+ // Start with the category label as the H1 heading
+ const lines = [`# ${category.label}`, ""];
+
+ // If the category has a description, include it at the top of the content.
+ if (description) lines.push(description, "");
+
+ // Render the category items as a markdown list
+ for (const item of category.items ?? []) {
+ // Skip items that don't have both a label and an href,
+ // as they won't be useful for LLM consumption
+ if (!item.href || !item.label) continue;
+
+ // Render the item as a markdown link
+ lines.push(`- [${item.label}](${item.href})`);
+ }
+ return lines.join("\n");
+}
+
+// Recursively traverse the route tree to collect all routes
+// that have a source file or are category indexes,
+// along with their version context, and store them in the output map.
+function collectRoutes(
+ route: RouteConfig,
+ parentVersion: VersionContext | undefined,
+ out: Map,
+): void {
+ // Parent version is passed down to children and has the highest priority.
+ // If there's no parent version, try to extract version information from the current route.
+ const version = parentVersion ?? extractVersion(route);
+
+ // Only collect routes that have version information
+ if (version) {
+ if (route.metadata?.sourceFilePath) {
+ // This is a regular doc page with a source markdown file.
+ out.set(route.path, {
+ path: route.path,
+ sourceFilePath: route.metadata.sourceFilePath,
+ isCategory: false,
+ version,
+ });
+ } else if (route.props?.categoryGeneratedIndex) {
+ // This is a category index page generated by the docs plugin.
+ // Collect its context to generate a markdown file for it later
+ const generated = route.props.categoryGeneratedIndex as {
+ description?: string;
+ };
+ out.set(route.path, {
+ path: route.path,
+ isCategory: true,
+ description: generated.description,
+ version,
+ });
+ }
+ }
+
+ // Recursively collect routes from child routes, passing down the version context.
+ for (const child of route.routes ?? []) collectRoutes(child, version, out);
+}
diff --git a/static/robots.txt b/static/robots.txt
index 6a6ec30..c66d764 100644
--- a/static/robots.txt
+++ b/static/robots.txt
@@ -1,2 +1,4 @@
User-agent: *
+Disallow:
+Content-Signal: search=yes, ai-train=yes, ai-input=yes
Sitemap: https://docs.imgproxy.net/sitemap.xml
diff --git a/versioned_docs/version-3.19.x/intro.mdx b/versioned_docs/version-3.19.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.19.x/intro.mdx
+++ b/versioned_docs/version-3.19.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.20.x/intro.mdx b/versioned_docs/version-3.20.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.20.x/intro.mdx
+++ b/versioned_docs/version-3.20.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.21.x/intro.mdx b/versioned_docs/version-3.21.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.21.x/intro.mdx
+++ b/versioned_docs/version-3.21.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.22.x/intro.mdx b/versioned_docs/version-3.22.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.22.x/intro.mdx
+++ b/versioned_docs/version-3.22.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.23.x/intro.mdx b/versioned_docs/version-3.23.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.23.x/intro.mdx
+++ b/versioned_docs/version-3.23.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.24.x/intro.mdx b/versioned_docs/version-3.24.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.24.x/intro.mdx
+++ b/versioned_docs/version-3.24.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.25.x/intro.mdx b/versioned_docs/version-3.25.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.25.x/intro.mdx
+++ b/versioned_docs/version-3.25.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.26.x/intro.mdx b/versioned_docs/version-3.26.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.26.x/intro.mdx
+++ b/versioned_docs/version-3.26.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.27.x/intro.mdx b/versioned_docs/version-3.27.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.27.x/intro.mdx
+++ b/versioned_docs/version-3.27.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.28.x/intro.mdx b/versioned_docs/version-3.28.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.28.x/intro.mdx
+++ b/versioned_docs/version-3.28.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.29.x/intro.mdx b/versioned_docs/version-3.29.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.29.x/intro.mdx
+++ b/versioned_docs/version-3.29.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.30.x/intro.mdx b/versioned_docs/version-3.30.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.30.x/intro.mdx
+++ b/versioned_docs/version-3.30.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-3.31.x/intro.mdx b/versioned_docs/version-3.31.x/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-3.31.x/intro.mdx
+++ b/versioned_docs/version-3.31.x/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_docs/version-4-pre/intro.mdx b/versioned_docs/version-4-pre/intro.mdx
index f2750ba..2df91c8 100644
--- a/versioned_docs/version-4-pre/intro.mdx
+++ b/versioned_docs/version-4-pre/intro.mdx
@@ -2,7 +2,7 @@
slug: '/'
title: ''
description: imgproxy is a fast and secure standalone server for resizing and converting remote images
-displayed_sidebar: tutorialSidebar
+displayed_sidebar: main
---
diff --git a/versioned_sidebars/version-3.19.x-sidebars.json b/versioned_sidebars/version-3.19.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.19.x-sidebars.json
+++ b/versioned_sidebars/version-3.19.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.20.x-sidebars.json b/versioned_sidebars/version-3.20.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.20.x-sidebars.json
+++ b/versioned_sidebars/version-3.20.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.21.x-sidebars.json b/versioned_sidebars/version-3.21.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.21.x-sidebars.json
+++ b/versioned_sidebars/version-3.21.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.22.x-sidebars.json b/versioned_sidebars/version-3.22.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.22.x-sidebars.json
+++ b/versioned_sidebars/version-3.22.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.23.x-sidebars.json b/versioned_sidebars/version-3.23.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.23.x-sidebars.json
+++ b/versioned_sidebars/version-3.23.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.24.x-sidebars.json b/versioned_sidebars/version-3.24.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.24.x-sidebars.json
+++ b/versioned_sidebars/version-3.24.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.25.x-sidebars.json b/versioned_sidebars/version-3.25.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.25.x-sidebars.json
+++ b/versioned_sidebars/version-3.25.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.26.x-sidebars.json b/versioned_sidebars/version-3.26.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.26.x-sidebars.json
+++ b/versioned_sidebars/version-3.26.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.27.x-sidebars.json b/versioned_sidebars/version-3.27.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.27.x-sidebars.json
+++ b/versioned_sidebars/version-3.27.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.28.x-sidebars.json b/versioned_sidebars/version-3.28.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.28.x-sidebars.json
+++ b/versioned_sidebars/version-3.28.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.29.x-sidebars.json b/versioned_sidebars/version-3.29.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.29.x-sidebars.json
+++ b/versioned_sidebars/version-3.29.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.30.x-sidebars.json b/versioned_sidebars/version-3.30.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.30.x-sidebars.json
+++ b/versioned_sidebars/version-3.30.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-3.31.x-sidebars.json b/versioned_sidebars/version-3.31.x-sidebars.json
index 108c582..43cf4c7 100644
--- a/versioned_sidebars/version-3.31.x-sidebars.json
+++ b/versioned_sidebars/version-3.31.x-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",
diff --git a/versioned_sidebars/version-4-pre-sidebars.json b/versioned_sidebars/version-4-pre-sidebars.json
index e2a5bb4..59f2341 100644
--- a/versioned_sidebars/version-4-pre-sidebars.json
+++ b/versioned_sidebars/version-4-pre-sidebars.json
@@ -1,5 +1,5 @@
{
- "tutorialSidebar": [
+ "main": [
"getting_started",
{
"type": "link",