From a6464ecdf18252b4306403f14d883f397fd97c71 Mon Sep 17 00:00:00 2001 From: klocke-io Date: Thu, 30 Apr 2026 14:00:08 +0200 Subject: [PATCH 1/3] Add Netlify build plugin to cache docforge GitHub API responses Deploy preview builds hit GitHub API rate limits because docforge fetches content from 35+ repos on every build. This local Netlify plugin uses the built-in cache utility to persist docforge's HTTP disk cache between deploy preview builds. - Production builds always pull fresh (no cache) - Cache invalidates when any .docforge/*.yaml config changes - Cache expires after 24 hours (TTL) --- .gitignore | 3 ++ Makefile | 2 +- netlify.toml | 15 ++++-- .../netlify-plugin-docforge-cache/index.js | 51 +++++++++++++++++++ .../manifest.yml | 1 + 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 plugins/netlify-plugin-docforge-cache/index.js create mode 100644 plugins/netlify-plugin-docforge-cache/manifest.yml diff --git a/.gitignore b/.gitignore index 8281b795e..ad5b5c76b 100755 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,9 @@ dist # Docforge binary directory /bin +# Docforge HTTP cache (persisted by Netlify plugin between builds) +.docforge-cache + # vuepress build output .vuepress/dist diff --git a/Makefile b/Makefile index b6226ca22..b2e21b5d0 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ build: ## Build the documentation site docforge-ci: docforge-download ## Run docforge in CI mode (non-interactive) @echo "Running docforge (CI)..." @export DOCFORGE_CONFIG=.docforge/config.yaml && \ - ./bin/docforge + ./bin/docforge --cache-dir ./.docforge-cache .PHONY: ci-build ci-build: docforge-ci install post-process build ## Run all steps for building in CI \ No newline at end of file diff --git a/netlify.toml b/netlify.toml index 52810d367..fe405a292 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,13 @@ +[build] + command = "make ci-build" + +[context.deploy-preview] + command = "make ci-build" + +[[plugins]] + package = "./plugins/netlify-plugin-docforge-cache" + [[headers]] - for = "/*" - [headers.values] - X-Frame-Options = "DENY" + for = "/*" + [headers.values] + X-Frame-Options = "DENY" diff --git a/plugins/netlify-plugin-docforge-cache/index.js b/plugins/netlify-plugin-docforge-cache/index.js new file mode 100644 index 000000000..e0820ff1c --- /dev/null +++ b/plugins/netlify-plugin-docforge-cache/index.js @@ -0,0 +1,51 @@ +import { readdir } from 'fs/promises'; +import { join } from 'path'; + +const CACHE_DIR = '.docforge-cache'; +const CACHE_TTL = 86400; + +async function collectDigests(dir) { + const digests = []; + const entries = await readdir(dir, { withFileTypes: true, recursive: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.yaml')) { + digests.push(join(entry.parentPath || entry.path, entry.name)); + } + } + return digests; +} + +export const onPreBuild = async function ({ utils }) { + if (process.env.CONTEXT === 'production') { + console.log('Production build — skipping docforge cache restore (always fresh).'); + await utils.cache.remove(CACHE_DIR); + return; + } + + const restored = await utils.cache.restore(CACHE_DIR); + if (restored) { + console.log('Docforge cache restored from previous build.'); + } else { + console.log('No docforge cache found — will be populated after this build.'); + } +}; + +export const onPostBuild = async function ({ utils }) { + if (process.env.CONTEXT === 'production') { + console.log('Production build — not saving docforge cache.'); + return; + } + + const digests = await collectDigests('.docforge'); + + const saved = await utils.cache.save(CACHE_DIR, { + ttl: CACHE_TTL, + digests, + }); + + if (saved) { + console.log(`Docforge cache saved (TTL: 24h, tracking ${digests.length} digest files).`); + } else { + console.log('Docforge cache directory not found — nothing to save.'); + } +}; diff --git a/plugins/netlify-plugin-docforge-cache/manifest.yml b/plugins/netlify-plugin-docforge-cache/manifest.yml new file mode 100644 index 000000000..a344a8f0f --- /dev/null +++ b/plugins/netlify-plugin-docforge-cache/manifest.yml @@ -0,0 +1 @@ +name: netlify-plugin-docforge-cache From efec46ace0282227185d10d4eab59603877da11b Mon Sep 17 00:00:00 2001 From: klocke-io Date: Thu, 30 Apr 2026 14:35:52 +0200 Subject: [PATCH 2/3] Fix Node.js compatibility in collectDigests function Replace recursive readdir with manual directory walk to avoid dependency on entry.parentPath (Node 21.4+). Also handle ENOENT gracefully when .docforge/ directory doesn't exist. --- .../netlify-plugin-docforge-cache/index.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/plugins/netlify-plugin-docforge-cache/index.js b/plugins/netlify-plugin-docforge-cache/index.js index e0820ff1c..e4ed12f88 100644 --- a/plugins/netlify-plugin-docforge-cache/index.js +++ b/plugins/netlify-plugin-docforge-cache/index.js @@ -4,14 +4,28 @@ import { join } from 'path'; const CACHE_DIR = '.docforge-cache'; const CACHE_TTL = 86400; -async function collectDigests(dir) { +async function collectDigests(baseDir) { const digests = []; - const entries = await readdir(dir, { withFileTypes: true, recursive: true }); - for (const entry of entries) { - if (entry.isFile() && entry.name.endsWith('.yaml')) { - digests.push(join(entry.parentPath || entry.path, entry.name)); + + async function walk(dir) { + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch (err) { + if (err.code === 'ENOENT') return; + throw err; + } + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.yaml')) { + digests.push(fullPath); + } } } + + await walk(baseDir); return digests; } From 6139d8a034b64eaeeb27b3b9689051466e51f7af Mon Sep 17 00:00:00 2001 From: klocke-io Date: Thu, 30 Apr 2026 14:42:35 +0200 Subject: [PATCH 3/3] Don't remove shared cache during production builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production builds should simply ignore the cache, not delete it. The cache is shared across contexts — removing it during prod would force the next deploy preview to rebuild from scratch. --- plugins/netlify-plugin-docforge-cache/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/netlify-plugin-docforge-cache/index.js b/plugins/netlify-plugin-docforge-cache/index.js index e4ed12f88..a86bb69c4 100644 --- a/plugins/netlify-plugin-docforge-cache/index.js +++ b/plugins/netlify-plugin-docforge-cache/index.js @@ -31,8 +31,7 @@ async function collectDigests(baseDir) { export const onPreBuild = async function ({ utils }) { if (process.env.CONTEXT === 'production') { - console.log('Production build — skipping docforge cache restore (always fresh).'); - await utils.cache.remove(CACHE_DIR); + console.log('Production build — skipping docforge cache (always fresh).'); return; }