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",