Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
.docusaurus
.cache-loader

# Versioned docs snapshots: regenerated from git history by CI
# (versioning/generate_versions.sh); never committed. See versioning/README.md.
/versioned_docs
/versioned_sidebars
/versions.json

# Misc
.DS_Store
.env
Expand All @@ -21,3 +27,4 @@ yarn-debug.log*
yarn-error.log*
yarn.lock
pnpm-lock.yaml
/.buildlogs
126 changes: 120 additions & 6 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,68 @@

require('dotenv').config();

const fs = require('fs');

// Docs versioning.
//
// We snapshot the docs as they existed at each STABLE litellm release (plus the
// latest rc). The version list lives in the committed manifest
// (versioning/manifest.json); the snapshots themselves (versioned_docs/,
// versioned_sidebars/, versions.json) are NOT committed — they are regenerated
// from git history at build time by versioning/prepare-snapshots.sh (the npm
// "prebuild" hook), so the repo stays clean.
let docsManifest = {versions: [], latest_stable: null};
try {
docsManifest = JSON.parse(
fs.readFileSync(__dirname + '/versioning/manifest.json', 'utf8'),
);
} catch (e) {}
const latestStable = docsManifest.latest_stable || null;
const rcVersions = (docsManifest.versions || [])
.filter((v) => v.channel === 'rc')
.map((v) => v.version);

// DOCS_VERSIONS_BUILD_LIMIT:
// all (default) -> every stable version + latest rc + "main"
// current -> current docs only (fast preview; no version snapshots)
// <N> -> "main" + the latest N versions
const versionsBuildLimitRaw = (
process.env.DOCS_VERSIONS_BUILD_LIMIT || 'all'
).toLowerCase();
const currentOnly = ['current', 'none', '0', ''].includes(versionsBuildLimitRaw);

// Snapshots present on disk (regenerated by prepare-snapshots.sh)? versions.json
// is written by generate_versions.sh alongside versioned_docs/.
let builtVersions = []; // newest-first
try {
builtVersions = JSON.parse(fs.readFileSync(__dirname + '/versions.json', 'utf8'));
} catch (e) {}
const snapshotsPresent = Array.isArray(builtVersions) && builtVersions.length > 0;
const buildsVersions = snapshotsPresent && !currentOnly;

// Which versions to actually render.
let includedVersions = [];
if (buildsVersions) {
if (versionsBuildLimitRaw === 'all') {
includedVersions = builtVersions;
} else {
const n = Math.max(1, parseInt(versionsBuildLimitRaw, 10) || builtVersions.length);
includedVersions = builtVersions.slice(0, n);
}
}

// Default version served at /docs/ is the latest STABLE — never the rc or "main".
const defaultVersion = buildsVersions
? includedVersions.includes(latestStable)
? latestStable
: includedVersions[0]
: 'current';

const currentDocsPath = buildsVersions ? '/docs/main/' : '/docs/';
const versionUrl = (version) =>
version === defaultVersion ? '/docs/' : `/docs/${version}/`;


// @ts-ignore
const lightCodeTheme = require('prism-react-renderer/themes/vsLight');
// @ts-ignore
Expand Down Expand Up @@ -70,13 +132,18 @@ const config = {
favicon: '/img/favicon.ico',

// Set the production url of your site here
url: 'https://docs.litellm.ai/',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
url: process.env.DOCS_SITE_URL || 'https://docs.litellm.ai/',
baseUrl: process.env.DOCS_BASE_URL || '/',

onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
onBrokenLinks: process.env.DOCS_ON_BROKEN_LINKS || 'warn',
onBrokenMarkdownLinks: process.env.DOCS_ON_BROKEN_MARKDOWN_LINKS || 'warn',

// Exposed to client pages (e.g. /versions) to build links to each version.
customFields: {
builtVersions,
defaultVersion,
currentDocsPath,
},

// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
Expand All @@ -87,6 +154,13 @@ const config = {
},
plugins: [
require('./plugins/optimize-images'),
// Adds <meta name="robots" content="noindex"> + a canonical link to the
// latest equivalent page on every non-latest docs version (old pip versions
// and the in-development "main"). Prevents duplicate-content SEO dilution
// across ~73 versions and keeps crawlers / Inkeep scoped to the latest docs.
...(buildsVersions
? [[require('./plugins/versioned-seo'), {}]]
: []),
...(hasInkeepSearch
? [
[
Expand Down Expand Up @@ -258,6 +332,30 @@ const config = {
docs: {
sidebarPath: require.resolve('./sidebars.js'),
remarkPlugins: [require('./src/remark/raw-markdown')],
// Serve the latest STABLE release at /docs/; the unversioned working
// tree is "main" at /docs/main/ (unreleased banner); the latest rc gets
// an unreleased banner too; older stables get the "unmaintained" banner.
...(buildsVersions
? {
lastVersion: defaultVersion,
onlyIncludeVersions: ['current', ...includedVersions],
versions: {
current: {label: 'main 🚧', path: 'main', banner: 'unreleased'},
// rc releases: show an "unreleased" banner, not "unmaintained".
...Object.fromEntries(
rcVersions
.filter((v) => includedVersions.includes(v))
.map((v) => [v, {banner: 'unreleased'}]),
),
},
}
: snapshotsPresent
? {
// Snapshots on disk but building current-only (fast preview).
lastVersion: 'current',
onlyIncludeVersions: ['current'],
}
: {}),
},
blog: false, // Disable the default blog plugin from preset-classic
pages: {},
Expand All @@ -275,6 +373,10 @@ const config = {
swcHtmlMinimizer: true,
lightningCssMinimizer: true,
mdxCrossCompilerCache: true,
// Use the Rust-based rspack bundler instead of webpack. Essential for
// many-version builds: rspack uses a fraction of webpack's memory, which
// otherwise OOMs (>8GB heap) when bundling dozens of doc versions.
rspackBundler: true,
},
},

Expand Down Expand Up @@ -325,6 +427,18 @@ const config = {
},
{ to: '/release_notes', label: 'Changelog', position: 'left' },
{ to: '/blog', label: 'Blog', position: 'left' },
// Native version dropdown (all versions are built same-origin), with a
// link to the full /versions page.
...(buildsVersions
? [
{
type: 'docsVersionDropdown',
position: 'right',
dropdownItemsAfter: [{to: '/versions', label: 'All versions →'}],
dropdownActiveClassDisabled: true,
},
]
: []),
{
href: 'https://docs.litellm-agent-platform.ai/',
label: 'LiteLLM Agent Platform',
Expand Down
Binary file added img/enterprise_vs_oss.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"build": "NODE_OPTIONS='--require ./versioning/graceful-fs-preload.js' docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
"write-heading-ids": "docusaurus write-heading-ids",
"prebuild": "bash versioning/prepare-snapshots.sh"
},
"dependencies": {
"@docusaurus/core": "3.8.1",
Expand All @@ -27,7 +28,8 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"sharp": "0.32.6",
"uuid": "9.0.1"
"uuid": "9.0.1",
"graceful-fs": "4.2.11"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.1",
Expand Down
96 changes: 96 additions & 0 deletions plugins/versioned-seo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* versioned-seo
*
* Post-build pass that marks every NON-latest docs page as `noindex` and points
* its canonical URL at the equivalent page on the latest version. This prevents
* ~73 backfilled versions from creating duplicate-content SEO dilution and keeps
* search crawlers (incl. Inkeep's indexer) scoped to the latest docs.
*
* "Non-latest" = any HTML under `<outDir>/docs/<segment>/...` where `<segment>`
* is a semver-looking version (e.g. 1.79.0) or the in-development `main`. The
* latest version is served directly under `<outDir>/docs/...` and is left alone.
*/
const fs = require('fs');
const path = require('path');

const VERSION_SEGMENT = /^(?:\d+\.\d+\.\d+|main)$/;

function walk(dir, out) {
let entries;
try {
entries = fs.readdirSync(dir, {withFileTypes: true});
} catch (e) {
return out;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, out);
} else if (entry.isFile() && entry.name.endsWith('.html')) {
out.push(full);
}
}
return out;
}

/** Build the canonical site path for an old-version HTML file. */
function canonicalPathFor(relFromDocs) {
// relFromDocs e.g. "1.79.0/proxy/configs/index.html" or "main/index.html"
const parts = relFromDocs.split(path.sep);
parts.shift(); // drop the version segment -> equivalent latest path
let rest = parts.join('/');
rest = rest.replace(/index\.html$/, '').replace(/\.html$/, '');
if (rest.length && !rest.endsWith('/')) rest += '/';
return '/docs/' + rest;
}

module.exports = function versionedSeoPlugin() {
return {
name: 'versioned-seo',
async postBuild({siteConfig, outDir}) {
const docsRoot = path.join(outDir, 'docs');
if (!fs.existsSync(docsRoot)) return;

const base = (siteConfig.url || '').replace(/\/$/, '');
let patched = 0;

for (const seg of fs.readdirSync(docsRoot, {withFileTypes: true})) {
if (!seg.isDirectory() || !VERSION_SEGMENT.test(seg.name)) continue;
const versionDir = path.join(docsRoot, seg.name);

for (const file of walk(versionDir, [])) {
const relFromDocs = path.relative(docsRoot, file);
const canonical = base + canonicalPathFor(relFromDocs);

let html = fs.readFileSync(file, 'utf8');
const robots =
'<meta name="robots" content="noindex, follow"/>';
const canonicalTag =
`<link rel="canonical" href="${canonical}"/>`;

// Replace Docusaurus' self-referential canonical, if present.
if (/<link[^>]+rel="canonical"[^>]*>/i.test(html)) {
html = html.replace(
/<link[^>]+rel="canonical"[^>]*>/i,
canonicalTag,
);
} else {
html = html.replace('</head>', canonicalTag + '</head>');
}

// Add robots noindex once.
if (!/name="robots"/i.test(html)) {
html = html.replace('</head>', robots + '</head>');
}

fs.writeFileSync(file, html);
patched++;
}
}

console.log(
`[versioned-seo] noindex + canonical applied to ${patched} non-latest docs pages`,
);
},
};
};
Loading