Contributor: Nikhil Kumar Rajak (@ryzrr) Organization: webpack · Project: webpack-doc-kit Mentors: Aviv Keller (@avivkeller), Claudio Wunder (@ovflowd), Sebastian Beltran (@bjohansebas) Teammates: Mohamed Shams El-Deen (@moshams272), Tushar Thakur (@TusharThakur04) Period: 25 May to 17 August 2026 The problem webpack's docs lived at webpack.js.org and every API change meant somebody updating them by hand. Pages go stale and nobody notices until a reader does. webpack-doc-kit fixes that. It takes webpack's TypeScript declarations, runs them through TypeDoc, hands the output to nodejs/doc-kit for linking and UI, and produces a site that regenerates itself. We split the work three ways. Shams took AST parsing and content, Tushar took routing and navigation and UI, and I took the operational side: how docs get generated on a release, versioned & deployed. My six deliverables were PR-based doc sync, release-aware doc generation, versioned output folders, a deployment pipeline, CI validation before merge, and README fetch automation. All six shipped. Merged PRs in webpack-doc-kit 31 Lines added / removed +1,959 / −1,419 Distinct files touched 89 First / last merge 28 May (#110) / 14 Aug (#241) Merged PRs in other repos 2 Upstream issue filed and fixed 1 Everything below is merged into main. Nothing is open or pending. The release pipeline webpack releases happen in webpack/webpack. The docs live in webpack/webpack-doc-kit. A release in one needs to produce updated docs in the other with nobody doing anything. #110 set up versions.json as the single source of truth everything downstream reads, plus the script that maintains it and the workflow that runs it. My mentor proposed an object schema with latest, label, major, exactVersion, commit and frozen per entry. Review cut it to a flat array of tag strings, because everything else is derivable from the semver string and position [0] with unshift() already tells you which is latest. Right call, and I didn't see it. const [tag] = process.argv.slice(2); Enter fullscreen mode Exit fullscreen mode The script validates with semver, finds any entry with the same major, replaces in place or unshifts to the front. The workflow runs it on workflow_dispatch and opens a draft PR, with every action SHA pinned. That trigger was itself a review correction: I'd reached for something else before realising releases fire in a different repo entirely. #116 removed a commit input I'd added on the belief that cross-repo triggering needed it. Review asked why. It didn't. Opening a PR to delete your own code is a bit humbling, but the workflow came out correct. webpack/webpack#21074 is the dispatching half, in the core repo. It runs after release, finds webpack in the published-packages list, and calls createWorkflowDispatch against webpack-doc-kit's release.yml with tag: "v" + pkg.version. Review caught two things. I'd interpolated a GitHub Actions ${{ }} expression straight into JavaScript, which moved to an env value parsed with JSON.parse. And I picked the published package by index [0] instead of by name, which works right up until it doesn't. The version running today isn't quite that one. Two weeks after it merged, Aviv replaced the credential in #21082: the DISPATCH_TOKEN PAT became a GitHub App installation token, minted in a preceding step by actions/create-github-app-token from BOT_APP_ID and BOT_PRIVATE_KEY and handed to github-script as steps.app-token.outputs.token. A PAT is one person's account and one person's scope, sitting in the secret store until someone remembers to rotate it. An app token is org-owned, scoped to the app's installation, and expires within the hour. secrets.GITHUB_TOKEN was never an option here, it can't reach another repository, which is the entire reason a second credential exists in the first place. He also renamed the job from trigger-webpack-doc-kit to trigger-documentation-update, and left the script itself alone with both fixes included. Full chain: webpack publishes, core dispatches, it updates versions.json, opens a PR, Vercel builds a preview, maintainer merges. Fetching content across the ecosystem #122 automated pulling in loaders and plugins READMEs from across the org. At +612/−968 across 10 files, my second largest change. It pages through the org's public repos following rel="next" in the Link header, skips archived ones, sorts the rest by name suffix, then fetches, cleans and writes each README with a generated site.json for the sidebar. Review made GH_TOKEN optional, since it threw when unset and would have broken local builds. The harder problem was that the generated site.json files weren't picked up at all. Fix was converting the root site config from JSON to an .mjs module that imports and concatenates them, because a committed file can import uncommitted generated ones. That's why pages/site.mjs exists. I also hit a build failure I couldn't explain at first: unknown language tags in code fences caused a fatal Shiki error and an empty output folder. This was fixed upstream in the Node.js repo by my mentor. More information on it is in the PR review comments. #140 pulled cleanup out of both fetch scripts into one shared sanitize.mjs. My version inlined the patterns in each helper; review suggested hoisting every regex to a named constant and splitting cleanup into exported step functions composed through a STEPS array. That's what merged and it reads better. The key export rewrites relative links through a caller-supplied resolver, which matters because READMEs point back at their own repo while governance docs point at their migrated page. #127 did the same for webpack's governance docs. The part I like is that one ordered FILE_MAP drives output slugs, sidebar labels, sidebar order and the link rewrite table, derived with Object.fromEntries so they can't drift. Adding a doc is a one-line change. I caught a bug myself before review: nested sidebar groups key off label while only top-level groups use groupName, and mixing them up renders wrong in a way that's easy to miss. #215 fixed "Edit this page" pointing at a file that doesn't exist in this repo. Needed a source frontmatter with a GitHub edit URL, which turned out to be more than a string change: GitHub's /edit/ route needs a real branch name, and HEAD works for raw reads and silently doesn't work for editing. So repo discovery had to start capturing each repo's actual default_branch. #137 wrapped every external fetch in a retry helper after deploys started failing on transient 503s: baseDelay * 2 ** attempt + Math.random() * baseDelay Enter fullscreen mode Exit fullscreen mode It honours a retry-after header when the server sends one instead of guessing. A sustained outage still fails the build, because shipping docs with half the content missing is worse than not shipping. CI and validation #146 builds the site on every PR, caching webpack sources keyed on a hash of versions.json and uploading the output as an artifact so there's a downloadable version available. #155 added CodeQL on PRs, pushes to main, and a weekly schedule. #157 added zizmor, which lints GitHub Actions workflows themselves, and in the same pass added persist-credentials: false to every existing checkout step in the repo. #170 added dependency review. My version shipped a license allow and deny list; review cut it, because the project has no license policy and the config would have been inventing one. The merged step has no configuration at all, which is the right amount. #192 was my largest change and the most reviewed. 232 lines across four files, running Lighthouse against the Vercel preview on every PR and posting a score table. It's a two-workflow split. The audit runs unprivileged and uploads its result as an artifact; a second workflow triggered on completion, holding pull-requests: write, picks it up and posts. You can't give write access to a job that checked out untrusted fork code, so the split is what makes it safe rather than just convenient. Thresholds warn rather than error: "performance": ["warn", { "minScore": 0.9 }] Enter fullscreen mode Exit fullscreen mode Deliberate. A blocking gate on day one is a gate people learn to route around. The workflows are adapted from nodejs.org and carry an Adapted from header at review's request. #195 replaced the bare /docs and /guides entries with eight URLs covering one render path each, since auditing only index pages measures the path least likely to regress. This PR only exists because I implemented the change in review, then didn't commit it before #192 merged. #196 fixed the contrast and accessible-name failures the audit surfaced. The theme toggle and GitHub link got real accessible names instead of being unlabelled buttons to a screen reader. Merged under five hours after #192, which is the whole argument for shipping it advisory. #210 fixed the audit failing constantly. On pull_request it started before Vercel had a preview and spent five minutes polling a URL that didn't exist. Fix was to stop racing the deploy and be triggered by it: on: deployment_status: Enter fullscreen mode Exit fullscreen mode That also let me delete a third-party wait action. But it cost something I didn't expect: deployment_status carries no PR context, so the number has to be recovered through listPullRequestsAssociatedWithCommit and written into the artifact for the commenting workflow to find. prs.find(p => p.state === 'open') ?? prs[0] Enter fullscreen mode Exit fullscreen mode It warns instead of failing, since a missing comment beats a red X on a PR whose scores were fine. #234 added path-based labelling across seven categories. The trigger is the interesting bit: labelling a fork PR needs write access, which pull_request can't grant, so this uses pull_request_target with an explicit zizmor ignore. Safe only because the job never checks out PR code, and the annotation is in the file so the next person knows why. #135 fixed three small things. Our .jsx components were silently skipped by ESLint because no config block covered them. A typo in the lint-staged glob had json.yml as one extension instead of two, so neither matched. And CI had no concurrency, so stale runs didn't cancel. Docs migration and site fixes #163, #168, #177, #185 took pages/ from 130 #TODO[...] placeholder links to zero. Slicing it into batches worked: the last touched 35 files and merged with zero inline comments, because the earlier rounds had settled what a valid target looks like. The best decision was in #163. Instead of pinning links at /docs/api/v5.x/..., review steered them at unversioned paths with a Vercel redirect sending anything without a version segment to the current major, and update-versions.mjs rewriting that redirect when the version changes. Links survive a major bump without anyone editing them. #168 also converted legacy admonition markers to GFM alert syntax and added a Magic Comments section to the code splitting guide, because several resolved links needed a target that didn't exist yet. One rule came out of it: a TODO isn't resolved because the URL returns 200, it's resolved when the target is reachable from navigation. Where no good target existed, links got unwrapped to plain text instead, since a dead link is worse than no link. #153 added real landing pages for /docs/loaders and /docs/plugins, which the footer was already linking to. The tricky part wasn't content, it was ignore rules: per-package pages there are generated and ignored, but these two are hand-written and need to stay tracked, so both ignore files changed to a contents glob plus a negation for index.md. #160 added the API version picker. My first approach to detecting the current version pattern-matched against the sidebar JSON. It worked, but review pointed out the theme config already exposes it: import { version } from '#theme/config'; Enter fullscreen mode Exit fullscreen mode Check whether the boring solution already exists before reaching for a clever one. The same PR fixed the governance overview never highlighting, because it generated as /about/governance/index rather than the directory route. #171 fixed no top-level nav item ever highlighting, because the shared NavBar was never told which page it was rendering. Fixing it exposed that the two URL helpers disagreed about slashes, one stripping a leading slash and the other re-adding it, so versioned paths came out doubled. #223 was the same class of bug three weeks later: two places still building sidebar links by string concatenation instead of the helper that already existed. Three lines. That's when I stopped treating these as individual bugs. The helper it uses was written by Shams, and a lot of what made the back half of the summer faster was one of us fixing something the other two could then just use. #228 added category rules for five namespaces whose pages were generated, reachable by URL, and invisible in navigation. Content the site already had and wasn't showing. That surfaced a second problem immediately, nested namespaces all collapsing onto the same "Overview" label, fixed with a depth guard. #191 replaced six hand-written homepage SVGs with Lucide icons. My first version moved them into a dedicated file, still hand-authored; review suggested a library instead. My one concern was licensing, which was a non-issue since Lucide ships its license in source. The second commit deleted the file I'd just written. Net −108 lines. #231 stopped the homepage config section jumping on tab switch, by giving the code block a minimum height in line-heights rather than pixels, so the floor tracks the code font instead of quietly breaking next time type sizing changes. #236 moved the footer social links into the brand column wrapped in a labelled nav, so the group gets announced properly, and added Bluesky. #241 switched the theme toggle to doc-kit's island component, which owns its own state, and wrapped the sidebar as an island hydrated on idle. Upstream nodejs/nodejs.org#8999 fixed active nav link matching in the shared UI components package, which compared only the first path segment, so /about and /about/sponsors highlighted together. The fix makes the longest matching prefix win: active ?? (allowSubPath ? pathname.startsWith('/' + href.split('/')[1]) : href === pathname) Enter fullscreen mode Exit fullscreen mode A link only matches when the pathname equals it or begins with it followed by a slash, so /about doesn't match /about-us. Shipped with eight test cases. Node's own nav is unaffected since its items are single segment; this matters for consumers with nested navigation, which is us. nodejs/doc-kit#831 is an issue I filed rather than code I wrote, included for what it unblocked. doc-kit rendered GitHub alert syntax as plain blockquotes, since remark-gfm doesn't cover it. A maintainer implemented native support in #832 five days later, and that's what #168 relies on. Reporting it upstream fixed it for every doc-kit consumer instead of just us. Current state The pipeline is complete and running. A webpack release now produces updated docs without manual intervention. Every PR gets built, scanned by CodeQL and zizmor, checked for vulnerable dependencies, audited by Lighthouse against its own preview, and labelled by what it touched. All six deliverables landed. All 31 PRs are merged. What's left is the cutover from webpack.js.org, which sits with the maintainers. What's left Lighthouse assertions are still advisory. All four categories warn. Once scores are stable across the eight audited URLs, accessibility is the obvious first candidate to promote to error. The banner is manual. #151 landed as a hand-edited config after review argued maintainers sometimes need it to say something other than the latest release. A hybrid was discussed and deferred: manual config when present, latest blog post for a month otherwise. None of my 31 PRs shipped tests. Fine for CSS and link rewrites. Less fine for the Lighthouse formatting script and the category rules, both pure functions in a repo that now has a test harness. My teammate Shams is steadily writing tests and I'll be assisting him, so this one is a work in progress rather than an open gap. What I learned Picking the right trigger is the real design decision in a CI workflow. Across five CI pull requests I ended up using four different triggers, each one for a good reason and each one with a downside. When I moved away from pull_request, the workflow finally ran reliably, but I lost all the PR context that trigger used to give me automatically. Review kept making the diff smaller. Drop the license allowlist for a policy that doesn't exist. Use a library instead of hand-writing SVGs. Use the helper that already exists. Reduce the version schema because the rest is derivable. Almost every suggestion removed code rather than adding it, which is really a lesson about code quality that I picked up here. Any hand-built URL string in this codebase was a bug waiting. Four PRs, four symptoms, same root cause. The fix wasn't finding them all, it was routing everything through the shared helpers so new ones can't appear. CI is only worth adding if someone acts on it. Non-blocking is what got Lighthouse merged. Acting on it the same afternoon is what stopped it becoming another green badge nobody reads. Fix it upstream when you can. The alert syntax thing could have been worked around locally. Reporting it meant every doc-kit consumer got it. Push the change you agreed to before asking for merge. #195 exists because I didn't. Thanks To my mentors, Aviv Keller, Claudio Wunder and Sebastian Beltran, and special thanks to Alex and Even. Most of my PRs are better specifically because my first approach got pushed back on. To Shams and Tushar, for constant back-and-forth about where our three pieces met. And to the wider webpack team, for running an org where a first-time contributor gets treated as a contributor. All merged pull requests webpack/webpack-doc-kit PR Title #110 feat: add versions.json version metadata contract #116 feat: simplify release.yml #122 feat: automate fetching and sanitization of webpack ecosystem readmes #127 feat: add governance docs fetcher #135 chore: lint jsx components and add ci concurrency #137 fix(build): retry transient fetch fails during build #140 feat: sanitized fetched docs #146 ci: run site build on PRs #151 feat(banner): show latest webpack release instead of Node.js banner #153 feat(docs): add loaders and plugins index pages #155 ci: add CodeQL scanning #157 ci: add zizmor scanning #160 fix: version picker to the API docs and highlight the overview as active #163 fix(docs): resolve the guide TODOs for the plugin to its API references #168 fix(docs): resolve few more guide TODOs links and fixed warning tags #170 ci: add dependency review workflow #171 fix(theme): pass pathname to NavBar for active link highlighting #177 fix(guides): resolve few more TODOs links #185 fix(guides): resolve remaining TODO reference links #191 feat(home): add custom feature-section icons #192 ci: add Lighthouse audits on pull requests #195 fix(ci): audit one page per type in Lighthouse #196 fix(a11y): resolve axe color-contrast and accessible-name failure #210 fix(ci): run Lighthouse on deployment_status instead of pull_request #215 fix(markdown): point "Edit this page" at the upstream repo #223 fix(url): normalize the Options link #228 fix(sidebar): add groups for namespaced plugins #231 fix(home): stop config section resizing on tab switch #234 feat(ci): label pull requests by changed paths #236 feat(footer): group social links under the brand column and add Bluesky #241 fix: use doc-kit's island theme toggle webpack/webpack · #21074 ci: trigger webpack-doc-kit release workflow on publish nodejs/nodejs.org · #8999 fix: navbar active link should match the most specific path nodejs/doc-kit · #831 feat: Add support for GitHub Alerts natively (filed, fixed upstream in #832)
Making webpack's Docs Update Themselves | GSoC 2026, wrapped
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.