Skip to content

Build and serve devdocs statically#2710

Open
simon04 wants to merge 6 commits into
mainfrom
modern
Open

Build and serve devdocs statically#2710
simon04 wants to merge 6 commits into
mainfrom
modern

Conversation

@simon04

@simon04 simon04 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

No description provided.

simon04 added 6 commits July 12, 2026 12:32
The service worker's precache list was templated per-user from the
`docs` cookie, so every /service-worker.js response varied by request
and the server had to read the cookie to render it. This removes that
dependency: the install list is now just the stable app shell, and the
fetch handler caches documentation index.json files at runtime as they
are requested, so enabled docs stay available offline without baking the
per-user list into the file.

This also makes the install more robust — cache.addAll is atomic, so a
momentarily-unavailable doc index can no longer fail the whole service
worker install.

With this, the server reads no cookies at all: drop the now-unused docs,
user_has_docs?, doc_index_urls and memoized_cookies helpers along with
the sinatra/cookies dependency. The enabled-docs list remains a
client-side cookie (via CookiesStore) and could later move to
localStorage since nothing server-side depends on it.
Add docs/storage.md describing the four storage layers (enabled-docs
list, per-doc index, doc content, service worker cache), where each
lives, and when it is written.
Single-doc mode (viewing one documentation in a focused, IndexedDB-off
view) was triggered by the server rendering other.erb with the doc's
metadata inlined as a data-doc body attribute. That required a distinct
per-doc server-rendered shell.

Detect it on the client instead: doc pages now render the normal app
shell (index.erb, which loads the full catalog), and on boot the app
inspects location.pathname — if the first segment resolves to a doc in
app.DOCS, it boots single-doc mode for that doc; otherwise it boots the
full app, which continues to resolve aliases, redirects and 404s. The
~33 KB catalog is loaded either way, which is an accepted tradeoff.

isSingleDoc() now reads an internal flag set from the URL rather than the
data-doc attribute; the flag is computed at the very top of init() so
error-tracking's mode tag stays accurate. Drops other.erb, the data-doc
plumbing and the now-unused doc_index_page? helper.
Upstream 669abd4 moved index.json to load from docs_origin (the
documents.devdocs.io CDN, a different origin in production). Update the
per-doc index and service worker sections to match.
Add an assets:render_static step that renders the app shell, the service
worker and the news feed to static files under public/, so the app can be
served without the Ruby process. Rendering goes through the real Rack app
(App.call) so the output is byte-identical to what Sinatra serves.

It runs after compile in the assets:precompile pipeline and re-points the
asset helpers at the freshly compiled manifest first, since the app booted
before the manifest was written. The generated files are gitignored.
Serve the prerendered public/ output (index.html, service-worker.js,
feed.atom and the compiled assets) with no Ruby at request time. The
config mirrors the request-time behaviour of lib/app.rb: the security and
per-path cache headers, the full redirect table (sponsor links, legacy
paths, DOC_REDIRECTS renames, angular/dom fragment rewrites, /maxcdn 410,
/search and /?q= to the hash query), doc-slug trailing-slash
canonicalization, the /feed Atom endpoint, the out.devdocs.io tracking
domain, and an index.html SPA fallback for everything else.

Additive only — config.ru and the Sinatra app are unchanged; this is an
alternative front end. Validated with caddy v2 and smoke-tested against
public/. Each rule is commented with the app.rb construct it mirrors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant