Skip to content

feat(docs): add fork-preview banner, dual Atom feeds, gh-pages reset - #449

Draft
netravnen wants to merge 17 commits into
peeringdb:masterfrom
netravnen:admincom/docs-improvements
Draft

feat(docs): add fork-preview banner, dual Atom feeds, gh-pages reset#449
netravnen wants to merge 17 commits into
peeringdb:masterfrom
netravnen:admincom/docs-improvements

Conversation

@netravnen

@netravnen netravnen commented Aug 7, 2026

Copy link
Copy Markdown

Draft PR for early feedback.

This branch started as infra for running a dev/preview fork of this
site, and grew to fix a few problems that showed up while doing that:
preview deploys were indistinguishable from production in a browser,
the two content streams (blog posts and release notes) only had one
combined feed to subscribe to, and gh-pages accumulated deploy
history that made no promise about matching the branch that built it.
A batch of unrelated content fixes rode along in the same session and
is included here too.

Opening as draft because part of this (the fork-preview banner) is
fork-specific scaffolding that may not be wanted upstream as-is;
flagging for discussion rather than asking for a merge.

Relates to #448

Changes

  • Fork-preview banner: shows fork repo URL, current serving URL,
    and where production lives, so a preview deploy is never mistaken
    for the real site. Suppressed via PROD=TRUE.
  • Two independent Atom feeds: docs/blog/atom.xml and
    docs/release_notes/atom.xml, so blog and release-notes readers can
    subscribe separately. Share a common scripts/atom_common.py.
    Release-notes entries are parsed from release_notes/index.md's
    existing ## Release X.Y.Z sections.
  • Site-URL resolution policy (scripts/site_url.py): one shared
    precedence (PROD=TRUE > SITE_URL_OVERRIDE > detected local
    dev-server address > http://localhost:8000/) used by the banner,
    both feeds, and dynamic docs/CNAME generation, so they can't
    disagree about what URL they're building for.
  • gh-pages deploy: added --no-history so every deploy is a
    single fresh commit built from the current branch, instead of
    accumulating commits on top of whatever was already there.
  • Content fixes (9 commits): dead links in FAQ/blog posts, a
    stale CentOS setup step, FAQ additions (RDAP affiliation, admin
    succession, deletion locks, Mergers & Acquisitions guidance),
    getting-started clarifications, a Deskpro public-response snippets
    index, and nav registration for pages that existed but weren't
    linked from the sidebar.

Testing

  • uv run mkdocs build --strict -- 0 warnings.
  • Feed self-links verified against both mkdocs serve and a
    separately-served static build.
  • Banner verified visually, including suppression under PROD=TRUE.

Notes for reviewers

scripts/generate_banner.py and the SITE_URL_OVERRIDE dev-domain
plumbing in docs-deploy.yml are the pieces most likely to need
changes, or removal, before this could land on master as-is. Happy
to split those out of the eventual mergeable set if preferred.

netravnen and others added 17 commits August 7, 2026 23:49
A codebase-onboarding pass flagged two minor doc issues: README.md's
CentOS setup step predates this repo's move to a venv + uv workflow,
and mkdocs.yml's commented-out repo_url ("Edit on GitHub" link) had
only a generic comment with no indication of whether it was disabled
on purpose. Confirmed via git history that the link was deliberately
removed (commit fcb252c, "Removed the 'Edit on GitHub' link.",
2024-02-05), though that commit itself records no reason -- so this
leaves it disabled and documents that provenance instead of guessing
at intent either way.

Changes:
- README.md: CentOS install step used `yum install python-pip`,
  the Python 2 package name on a package manager CentOS has since
  moved on from; updated to `dnf install python3 python3-pip` to
  match the rest of the guide's python3/venv-based setup
- mkdocs.yml: replace the one-line "This line adds the Edit on
  GitHub link" comment above the commented-out repo_url with one
  that records why it's off (deliberate removal, reason
  undocumented) so it reads as a known state, not leftover cruft

Security:
- N/A -- documentation only

Testing:
- N/A -- text-only changes; no build step to run for README.md, and
  mkdocs.yml's only functional line (repo_url) is unchanged, still
  commented out

Backwards Compatibility:
- N/A -- no behavior change; the Edit on GitHub link remains off,
  matching current site behavior

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ber steps

The getting-started path was scattered: index.md buried the
role-specific HOWTOs under 45 lines of archival material (release
notes, tools, presentations, beta server, historical data), the four
get-started-*.md guides each repeated an identical 370+ word "About
PeeringDB" paragraph, and their setup steps were an unordered list
that read more like notes than instructions. This restructures the
front door so a new user hits a role picker first and each guide
gets straight to numbered steps.

Changes:
- docs/index.md: move "Getting started" to the second section (after
  the intro video, before Policies); consolidate Release
  notes/Tools/Presentations/Open source/Beta/Historical data into a
  compact "Learn more" bullet list; move the tutorial/workshop list
  out to presentations.md; trimmed from 123 to 92 lines
- docs/howtos.md: add a "New here? Start with your role" intro
  linking the four get-started guides, and list query_the_api.md
  alongside search.md
- docs/howto/get-started-{operator,exchange,facility,carrier}.md:
  replace the duplicated "About PeeringDB" boilerplate with a single
  pointer to index.md; number the setup steps (create account,
  create org, create the role-specific object, follow-up step);
  standardize the "Why should I add my network?" heading to "Why?"
  to match the other three guides; add the missing "More
  information" section to get-started-carrier.md so all four guides
  match
- docs/presentations.md: add "Tutorials and workshops" section at
  top with the 7 entries moved from index.md

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; internal links
  between index.md, howtos.md, and the four get-started guides
  verified to resolve

Backwards Compatibility:
- N/A -- no URLs or anchors removed; index.md's #mailing-lists
  anchor (linked from 60+ blog posts) is unchanged

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
search.md had grown to 355 lines serving two audiences at once: a
beginner walkthrough of UI/browser-extension search, and a full API
reference (object types, authentication, curl/Python/jq examples).
Separately, authenticate.md's MFA section was four sentences with
three screenshots sitting unused in the images directory, leaving
users to figure out setup and org-level enforcement on their own.

Changes:
- docs/howto/query_the_api.md: new file (175 lines) with the API
  reference content moved from search.md -- object types
  (basic/derived), API key authentication, making requests, GET
  operations on single/multiple objects, nested sets/objects, and
  real-world query examples using curl, Python, and jq
- docs/howto/search.md: trimmed from 355 to 189 lines, now focused
  on UI/extension search; replaced the removed content with a
  pointer to query_the_api.md
- docs/howto/authenticate.md: expanded the MFA section into a full
  walkthrough with two new subsections ("Setting up your second
  factor", "Requiring MFA for your organization"), wiring in the
  three previously orphaned screenshots
  (Location_of_MFA_button.png, MFA_management.png,
  require_users_to_enable_2fa.png) with captions
- docs/howto/work_within_peeringdbs_query_limits.md,
  docs/release_notes/release_notes_2023.md: update cross-references
  from /howto/search to /howto/query_the_api/ to match the split

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; verified
  query_the_api.md anchors referenced by other pages resolve

Backwards Compatibility:
- search.md's URL is unchanged; only its content and length
  changed, so existing external links to /howto/search/ still
  resolve, just to a narrower page

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A link sweep across the docs site found three broken references
unrelated to the getting-started or API-split work: an FAQ anchor
pointing at a section that no longer exists, and two blog posts
linking to HOWTO pages that were renamed or never existed.

Changes:
- docs/faq.md: "How do I get started?" linked to
  http://docs.peeringdb.com/#quick-start, an anchor that doesn't
  exist in index.md; replaced with a link to /howtos/ describing
  the role-based guides
- docs/blog/mfa_mandatory.md: "second factor" linked to
  /howto/enable_require_2fa/, a page that was never published;
  redirected to /howto/authenticate/#multi-factor-authentication-mfa
- docs/blog/peeringdb_2022_user_survey.md: "HOWTO documentation
  series" linked to /howto/ (404, missing the plural); fixed to
  /howtos/

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; confirmed
  /howto/authenticate/#multi-factor-authentication-mfa resolves in
  the generated HTML

Backwards Compatibility:
- N/A -- these links were already broken; no working URLs changed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Admin Committee's public-facing Deskpro response snippets
(triggered via ;code shortcuts like ;net-approved) had no
human-readable index, making them hard to review or reference
outside the Deskpro UI itself.

Changes:
- docs/committee/admin/deskpro_snippets.md: new file indexing the
  public (non-staff-only, non-trivial) Deskpro snippets by category
  -- network registration & approvals, facility & carrier
  management, organization recovery & account verification, IP/IX-F
  conflict resolution, and general administrative responses -- with
  snippet ID, shortcut code, and full text for each

Security:
- N/A -- documentation only; content already public-facing via
  Deskpro shortcuts, this just mirrors it

Testing:
- mkdocs build --strict passes with 0 warnings

Backwards Compatibility:
- N/A -- new page, no existing links affected

Co-Authored-By: Gemini 3.6 Flash <noreply@google.com>
…uidance

Facility, exchange, and carrier applicants were finding out they
didn't qualify only after submitting and getting rejected, because
the eligibility bar for each object type lived only in the Admin
Committee's Deskpro response snippets, not in the public HOWTOs.
Likewise, none of the three guides warned that facility and IX
prefix objects can't be deleted once other records depend on them,
so operators hit that block with no context.

Changes:
- docs/howto/get-started-facility.md: add "Facility qualification &
  approval criteria" section (ownership/operation,
  public colocation, multi-carrier interconnection, public
  documentation, and explicitly ineligible site types) and a
  "Deleting a facility" warning admonition covering the
  connected-networks deletion lock
- docs/howto/get-started-exchange.md: add a "3-ASN minimum" callout
  covering the participant threshold, route-server/affiliated-ASN
  exclusions, and RDAP-verifiable prefix requirements, plus a
  "Deleting an IX prefix" warning admonition covering the
  connected-networks deletion lock
- docs/howto/get-started-carrier.md: add "Carrier infrastructure
  qualification checklist" distinguishing eligible carrier-grade
  services (dark fibre, DWDM/wavelengths, Ethernet transport, MPLS
  VPN over owned infrastructure) from ineligible resold/leased
  transport

Content is sourced from the Admin Committee's public Deskpro
snippets already indexed in committee/admin/deskpro_snippets.md
(;fac-rejected, ;ix-rejected, ;car-rejected, ;deletion-fac,
;deletion-ixpfx) -- this makes criteria the committee already
applies visible to applicants before they submit.

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; admonition blocks
  confirmed to render as `class="admonition warning"` /
  `class="admonition important"` in the generated HTML

Backwards Compatibility:
- N/A -- additive content only, no headings renamed or removed, no
  existing anchors changed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan-By: Gemini 3.6 Flash <noreply@google.com>
…locks

Two recurring support patterns had no self-serve documentation: users
whose account email didn't match their ASN's RDAP record had no way
to learn that adding a secondary email would unblock automated
affiliation approval, and users who inherited an ASN or lost contact
with their org's admins had no documented path to reclaim access.
Separately, faq.md's Technical section covered API and sync
mechanics but nothing about why object deletion sometimes gets
blocked.

Changes:
- docs/howto/authenticate.md: add "Organization affiliation & admin
  succession" section with two subsections -- "RDAP email
  verification" (how the RDAP match works, why adding a secondary
  email is faster than changing your login email) and "Claiming
  admin rights for an existing organization" (contacting a reachable
  admin first, falling back to RDAP-verifiable proof via support,
  and what to provide after an ASN acquisition)
- docs/faq.md: add "Why can't I delete my Facility, IX Prefix, or
  Point of Contact?" under Technical, covering all three deletion
  locks and how to resolve each

Content is sourced from the Admin Committee's public Deskpro
snippets already indexed in committee/admin/deskpro_snippets.md
(;mismatch-email, ;contacts-existing-admins, ;deletion-fac,
;deletion-ixpfx, ;deletion-poc).

Security:
- N/A -- documentation only; no new authentication mechanism
  described, just an explanation of an existing one

Testing:
- mkdocs build --strict passes with 0 warnings; new mailto and RDAP
  links verified to resolve

Backwards Compatibility:
- N/A -- additive content only, no headings renamed or removed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan-By: Gemini 3.6 Flash <noreply@google.com>
committee/admin/deskpro_snippets.md and howto/query_the_api.md were
only reachable via inline links from their parent pages -- mkdocs
build listed both as present but not included in nav. Neither is a
broken build (mkdocs only warns at INFO level for this), but it
meant visitors browsing the site nav tree had no way to discover
either page.

Changes:
- mkdocs.yml: nest "Admin Committee" into "Overview" (index.md) and
  "Deskpro Snippets" (deskpro_snippets.md); nest "HOWTOs" into
  "Overview" (howtos.md) and "Query the API" (query_the_api.md)

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; both pages no
  longer appear in the "not included in nav" info listing

Backwards Compatibility:
- N/A -- existing page URLs are unchanged; this only adds entries
  to the nav tree

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan-By: Gemini 3.6 Flash <noreply@google.com>
The FAQ had no guidance for a recurring support scenario: a company
acquires another company and wants to bring the acquired
organization's PeeringDB resources under its own management. Users
hitting this had to file a support ticket cold, with no documented
process or expectations set in advance. This also complements the
"Organization affiliation & admin succession" section already added
to authenticate.md, which covers the adjacent but distinct case of
claiming admin rights when the original admins can't be reached.

Changes:
- docs/faq.md: add "Mergers and Acquisitions" under General, right
  after "Affiliation requests" -- covers the acquisition scenario,
  a worked example (Company A acquired by Company B), the support
  ticket process, and what speeds up review (writing from an email
  address that admins both organizations); cross-references
  authenticate.md's admin-succession guidance for the
  unreachable-admin case
- docs/howto/authenticate.md: add a reverse cross-reference from
  "Claiming admin rights for an existing organization" to the new
  FAQ entry, for the voluntary-handoff case where the acquired
  organization's admins are still reachable

Security:
- N/A -- documentation only

Testing:
- mkdocs build --strict passes with 0 warnings; both cross-reference
  anchors (#mergers-and-acquisitions,
  #claiming-admin-rights-for-an-existing-organization) confirmed
  present in the generated HTML

Backwards Compatibility:
- N/A -- new content only, no existing links or anchors changed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs.peeringdb.com is served via GitHub Pages today, but entirely
manually -- someone runs `mkdocs gh-deploy --clean` from their own
machine, which pushes the built site to upstream's `gh-pages`
branch. There is no CI/CD in this repo's history at all, so nothing
catches a broken link or nav omission before it ships, and the fork
has no equivalent preview of its own. This adds two workflows
scoped to this fork only -- upstream's manual production deploy is
untouched.

Changes:
- .github/workflows/docs-build-check.yml: runs `mkdocs build
  --strict` on every pull request and on push to master/
  admincom/docs-improvements, using uv to install the pinned
  dependency set from uv.lock. Fails the check on any warning
  (broken links, pages missing from nav, etc.) -- the automated
  version of the manual --strict checks run throughout this
  session's doc work.
- .github/workflows/docs-deploy.yml: automates the exact
  `mkdocs gh-deploy --clean` command README.md already documents as
  the manual process, targeting the fork's own (currently
  nonexistent) gh-pages branch. Strips docs/CNAME before building
  first, since that file lives inside docs_dir and would otherwise
  get copied into the built site and wrongly claim
  docs.peeringdb.com as the fork's own custom domain. Runs on push
  to master/admincom/docs-improvements plus workflow_dispatch for
  manual redeploys; --force is required since a stateless CI runner
  never has prior gh-pages history to compare against.

Security:
- Deploy workflow requests `contents: write` on GITHUB_TOKEN,
  scoped to this repo only; PR-check workflow stays `contents:
  read` since it never publishes anything. Neither workflow runs on
  pull_request_target or handles third-party PR code with elevated
  permissions, so forked-PR privilege escalation isn't a concern.

Testing:
- Both YAML files parse cleanly with PyYAML; `mkdocs build --strict`
  still passes with 0 warnings locally. End-to-end verification
  (Actions tab going green, gh-pages branch creation, Pages URL
  serving correctly) requires pushing to GitHub and is documented as
  follow-up manual verification, not achievable from a local clone.

Backwards Compatibility:
- N/A -- new files only; no existing workflow, branch, or build
  behavior is changed. Upstream's manual deploy process is
  completely unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The blog page listed posts in docs/blogs.md with no way to subscribe
to new ones. Add a mkdocs hook that regenerates docs/atom.xml from
that list on every build, so a feed reader can follow new posts
without manually checking the page.

Post dates come from git history rather than blogs.md's hand-typed
date text: a bulk git-log pass over docs/blog/ resolves each post's
first-added commit date in one shot, falling back to a slower
per-file `git log --follow` lookup only for the handful of posts that
pass has missed (files introduced via a rename, which git classifies
as an R event rather than A, so a --diff-filter=A bulk pass silently
skips them). This gives real commit timestamps (time of day, correct
timezone) instead of a hand-typed day-only date, and self-corrects if
blogs.md's date text is ever wrong or stale.

The feed's self-link is driven by an ATOM_SITE_URL environment
variable (falling back to mkdocs.yml's site_url) rather than always
hardcoding the production URL, so this fork's own preview deploy can
correctly self-reference wherever it actually serves from instead of
falsely claiming to be docs.peeringdb.com. docs-deploy.yml sets it
from GitHub Actions context; docs-build-check.yml gets fetch-depth: 0
added to its checkout, which the git-log date lookups require --
without full history, every post would appear to have no git history
at all, and mkdocs build --strict promotes that warning to a failure.

Duplicate timestamps (two posts added in the same commit) are nudged
apart by 1-second increments after date resolution, so every entry's
atom:updated value stays unique per the W3C Feed Validator's
interoperability recommendation, without affecting sort order.

Changes
- scripts/generate_atom_feed.py (new): on_pre_build hook. Bulk +
  per-file git-log date lookup, markdown-to-plaintext excerpt
  extraction for each entry's summary, ISO-8601 formatting (normalized
  to UTC for git's real commit timezones), duplicate-timestamp
  dedupe, Atom 1.0 XML construction via stdlib ElementTree/minidom --
  no third-party feed library.
- mkdocs.yml: register the hook under hooks:.
- docs/blogs.md: "Subscribe via Atom feed" line; also the underlying
  source list this hook parses.
- peeringdb_theme/main.html: <link rel="alternate"> feed-discovery tag.
- .gitignore: docs/atom.xml is a generated build artifact, not tracked.
- .github/workflows/docs-build-check.yml,
  .github/workflows/docs-deploy.yml: fetch-depth: 0 (required for the
  git-log date lookups); docs-deploy.yml sets ATOM_SITE_URL from
  GitHub Actions context and strips any production docs/CNAME before
  building, so this fork's preview never falsely claims to be
  docs.peeringdb.com.

Security
- N/A

Testing
- uv run mkdocs build --strict: 0 warnings.
- Spot-checked generated dates against git log for several posts,
  including a renamed file resolved via the --follow fallback.
- Confirmed duplicate-timestamp entries get distinct atom:updated
  values while preserving their original relative order.

Backwards Compatibility
- N/A

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The fork's preview site needs its own domain, distinct from upstream
production's docs.peeringdb.com -- docs-dev.peeringdb.dk. The
straightforward way to do this (edit docs/CNAME's tracked content
directly) has the same problem as removing it outright: docs/CNAME is
the same file both this fork's automation and upstream's manual
`mkdocs gh-deploy --clean` rely on, so whatever value sits in it on
this branch would silently become production's value the moment this
branch merges upstream -- either breaking the custom domain entirely
(if removed) or replacing it with the wrong one (if hardcoded to the
dev domain).

Instead of choosing a value to store, generate the file at build time
from configuration, mirroring the pattern already used for the atom
feed's self-link: an optional SITE_URL_OVERRIDE env var takes
priority when set, falling back to mkdocs.yml's site_url
(https://docs.peeringdb.com/) when unset. This makes the merge-safety
concern moot rather than just documented: upstream's deploy process
sets no override, so it always regenerates the correct production
CNAME automatically, with no manual edit required at merge time.

Changes:
- scripts/generate_cname.py: new mkdocs on_pre_build hook. Derives
  the GitHub Pages custom domain from SITE_URL_OVERRIDE if set, else
  config.site_url, via urllib.parse (stdlib only, no new dependency),
  and writes docs/CNAME before mkdocs collects static files -- same
  timing/mechanism already used for the atom feed.
- scripts/generate_atom_feed.py: rename the ATOM_SITE_URL env var to
  SITE_URL_OVERRIDE, since it now drives two hooks, not just the feed.
- mkdocs.yml: register the new hook alongside the existing one.
- docs/CNAME: untracked (git rm --cached) and added to .gitignore,
  alongside docs/atom.xml -- both are now build artifacts regenerated
  every run, not source content.
- .github/workflows/docs-deploy.yml: drop the now-unnecessary
  "strip CNAME" step, and set SITE_URL_OVERRIDE from a repo variable
  (Settings -> Actions -> Variables -> SITE_URL_OVERRIDE, already
  configured to https://docs-dev.peeringdb.dk) rather than hardcoding
  the domain in the workflow file.

Security:
- N/A -- documentation only. urllib.parse.urlparse on a value from a
  repo-scoped Actions variable is not an injection vector.

Testing:
- mkdocs build --strict passes with 0 warnings in both modes: with no
  SITE_URL_OVERRIDE set, site/CNAME correctly resolves to
  docs.peeringdb.com (config.site_url fallback, matching current
  production behavior exactly); with SITE_URL_OVERRIDE=
  https://docs-dev.peeringdb.dk/, site/CNAME resolves to
  docs-dev.peeringdb.dk and the atom feed's self-link matches it too,
  confirming both hooks stay in sync off the same override.

Backwards Compatibility:
- N/A for upstream -- unchanged behavior when no override is set.
  For this fork specifically, docs/CNAME's tracked value changes from
  a static docs.peeringdb.com to being generated at build time from
  the SITE_URL_OVERRIDE repo variable; the fork's actual GitHub Pages
  custom-domain setting and docs-dev.peeringdb.dk DNS record still
  need to be configured to match (outside this branch's scope).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
generate_cname.py and generate_atom_feed.py each independently
resolved SITE_URL_OVERRIDE-or-site_url with slightly different
fallback logic. Extract one shared policy so both (and later hooks)
always agree on what "the site URL" is for a given build.

scripts/site_url.py's resolve_site_url() resolves in this order:
1. PROD=TRUE -- real production, full stop.
2. SITE_URL_OVERRIDE, if set.
3. The local dev-server address mkdocs itself is serving on (e.g.
   http://127.0.0.1:8000/), if `mkdocs serve` is what's actually
   running. Requires callers to be wired to on_pre_build, not
   on_config: mkdocs's own serve.py only overwrites config.site_url
   with the dev address *after* on_config has already run (see
   commands/serve.py's `config.site_url = f'http://{config.dev_addr}...'`,
   right before it calls build()), so on_config never sees it.
4. LOCAL_SITE_URL ("http://localhost:8000/", mkdocs's own default
   --dev-addr) -- the fallback when none of the above apply: a plain
   `mkdocs build` with no env vars and no live dev server, e.g.
   docs-build-check.yml's CI check or a one-off local build served
   separately afterward (a static server started after the build
   already finished can't be predicted at build time -- there's
   nothing to detect there). Deliberately assumes "local" rather than
   guessing a real GitHub Pages URL from `git remote get-url origin`:
   a real checkout always has an `origin` remote, so a git-remote-based
   guess would almost always "succeed" with a URL that doesn't
   correspond to anything actually being served, which is more
   misleading than an honest localhost default.

Changes
- scripts/site_url.py (new): resolve_site_url() and origin_repo_url(),
  the shared policy described above.
- scripts/generate_cname.py: use the shared resolver instead of its
  own SITE_URL_OVERRIDE-or-site_url fallback. Also now skips writing
  docs/CNAME when the resolved host is a loopback address, since a
  CNAME file only means anything for a real GitHub Pages deploy.
- scripts/generate_atom_feed.py: use the shared resolver instead of
  its own fallback.

Security
- N/A

Testing
- uv run mkdocs build --strict in four scenarios (no env vars /
  SITE_URL_OVERRIDE / PROD=TRUE / both), all 0 warnings.
- Real `mkdocs serve` run: both docs/CNAME and the feed's self-link
  correctly resolve to the actual dev-server address, confirmed via
  curl that the server responded at that address.
- Confirmed a plain `mkdocs build` with no env vars and no dev server
  running resolves to http://localhost:8000/ rather than a GitHub
  Pages guess, and that docs/CNAME is correctly left untouched (a
  loopback host skips the write).

Backwards Compatibility
- N/A

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This fork's preview deploy currently looks identical to real
production, making it easy to mistake one for the other. Add a
banner above the nav bar showing the fork's repo, the URL currently
serving the page, and where production actually lives.

The banner is on by default -- any build without further signal is
assumed to be a dev/fork build -- and is suppressed only when PROD=TRUE
is set at build time, deliberately the opposite default from
site_url.resolve_site_url()'s own production-safe fallback, since a
real production deploy lives outside this repo and can set PROD=TRUE
explicitly.

Changes
- scripts/generate_banner.py (new): on_pre_build hook that exposes
  fork repo/live/production URLs to templates via
  config.extra.fork_banner, gated on PROD=TRUE. Uses on_pre_build
  (like the cname/atom-feed hooks) rather than on_config, required for
  resolve_site_url()'s local-serve detection tier to see mkdocs's
  overwritten config.site_url.
- peeringdb_theme/base.html (new): full copy of mkdocs 1.4.3's packaged
  base.html (pinned version) with the banner markup inserted before the
  nav bar div, since no block wraps that div for main.html to override.
- docs/extra.css: .fork-banner styling. Height is fixed to 3.5rem to
  match the nav bar's own documented height (mkdocs's base.css:
  scroll-padding-top comment), which .nav-link's 1rem/1rem padding plus
  Bootstrap's 1.5rem line-height already add up to -- no JS measurement
  needed. The banner is sticky at top:0 with the nav bar offset below it
  (body.has-fork-banner .navbar.fixed-top { top: 3.5rem }) so both stay
  pinned together while scrolling.
- mkdocs.yml: register generate_banner.py in hooks:.

Security
- N/A

Testing
- uv run mkdocs build --strict in the same four scenarios as the
  previous commit, all 0 warnings, banner present/absent as expected
  in each.
- Visually verified in-browser: banner height matches the nav bar,
  stays sticky above it while scrolling, single-line content doesn't
  wrap.

Backwards Compatibility
N/A -- peeringdb_theme/base.html duplicates mkdocs 1.4.3's packaged
base.html. If mkdocs is ever upgraded, this file needs to be re-diffed
against the new packaged version and the banner insertion reapplied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The blog feed generator (generate_atom_feed.py) inlined its Atom
feed/entry XML construction, ISO-8601 formatting, markdown-link
stripping, and timestamp-collision dedupe logic (commit a02694c6).
A second, independent feed for release notes is coming in a follow-up
commit -- pull the generic, non-blog-specific pieces out into a shared
scripts/atom_common.py first so that feed doesn't have to duplicate
~60 lines of ElementTree/minidom boilerplate.

Rename generate_atom_feed.py to generate_blog_atom_feed.py for
symmetry with the upcoming generate_release_notes_atom_feed.py, and
relocate its output from docs/atom.xml to docs/blog/atom.xml so the
feed is co-located with its source content the same way the release
notes feed will be (docs/blog/ + docs/release_notes/).

Changes
- scripts/atom_common.py (new): build_feed(), dedupe_published(),
  isoformat(), plain_text(), truncate() -- extracted unchanged from
  generate_atom_feed.py.
- scripts/generate_atom_feed.py -> scripts/generate_blog_atom_feed.py
  (renamed + refactored): keeps its blog-specific git-log date
  derivation and post-excerpt logic, now calls atom_common.build_feed()
  instead of inlining feed construction. Output moved to
  docs/blog/atom.xml.
- mkdocs.yml: hooks: entry updated for the rename.
- peeringdb_theme/main.html: blog feed's <link rel="alternate"> href
  updated to blog/atom.xml.
- docs/blogs.md: "Subscribe via Atom feed" link updated to the new
  blog/atom.xml path.
- .gitignore: docs/atom.xml entry updated to docs/blog/atom.xml.

Security
- N/A

Testing
- uv run mkdocs build --strict in four scenarios (no env vars /
  SITE_URL_OVERRIDE / PROD=TRUE / both), all 0 warnings.
- Diffed the feed's content against a pre-refactor build: byte-
  identical apart from the self-link, confirming the atom_common
  extraction and the file move didn't change any feed data.
- Confirmed docs/blogs.md's and site/blogs/index.html's rendered
  Subscribe links resolve to the new blog/atom.xml path.

Backwards Compatibility
Subscribers following the old docs/atom.xml URL on this fork's preview
will need to resubscribe at docs/blog/atom.xml -- flagging since it's
a real, if minor, break. Production (docs.peeringdb.com) has never had
this feed live, so nothing changes there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The blog feed was the only Atom feed this repo generated. Add a
second, independent feed for release notes, sourced from
docs/release_notes/index.md's `## Release X.Y.Z` sections, so
subscribers can follow releases without also getting blog posts (and
vice versa). Builds on the shared scripts/atom_common.py extracted in
the previous commit.

Release notes entry dates come straight from each section's
`Release Date:` line (falling back to `Beta Announcement Date:` if
absent) rather than git history -- unlike blog posts, this content is
already hand-dated inline, so there's no gap to fill. Entry URLs are
built with markdown.extensions.toc.slugify() (mkdocs's own anchor
slugifier, confirmed against the real built HTML: "Release 2.81.0"
becomes id="release-2810") rather than a hand-rolled one, guaranteeing
they always match the real heading IDs mkdocs generates. Entry
summaries are a plain issue count ("N GitHub issues addressed in this
release"), counted by matching table rows starting with `| [` rather
than parsing cell contents -- the summary column has occasional
unescaped `{`/`[`/`'` characters that would make a content-parsing
regex fragile for no real benefit, since a feed entry's job is to
notify, not replace the page a click away. Only index.md is scoped in,
not the per-year release_notes_YYYY.md archives -- a feed is about
what's new, and index.md alone already covers back to release 2.75.0.

Changes
- scripts/generate_release_notes_atom_feed.py (new): on_pre_build hook
  parsing docs/release_notes/index.md into feed entries as described
  above, writing docs/release_notes/atom.xml via atom_common.build_feed().
- mkdocs.yml: hooks: entry for the new hook.
- peeringdb_theme/main.html: second <link rel="alternate"> for the
  release notes feed.
- docs/release_notes/index.md: "Subscribe via Atom feed" line, matching
  docs/blogs.md's existing convention.
- .gitignore: docs/release_notes/atom.xml entry -- a build artifact,
  not tracked.

Security
- N/A

Testing
- uv run mkdocs build --strict in four scenarios (no env vars /
  SITE_URL_OVERRIDE / PROD=TRUE / both), all 0 warnings, both feeds'
  self-links agreeing with docs/CNAME in every scenario.
- Spot-checked the feed: Release 2.81.0's entry URL
  (.../release_notes/#release-2810) matches the real anchor ID in the
  built HTML, published date matches its Release Date line, and the
  issue count (8) matches a manual count of that section's table rows.

Backwards Compatibility
N/A -- new feed, nothing previously depended on this URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mkdocs gh-deploy already regenerates the whole site tree from the
current branch on every run, so removed pages never linger. What
wasn't guaranteed was the gh-pages branch's commit history: without
--no-history, ghp-import fetches the existing gh-pages HEAD and
commits on top of it, so the branch's state technically depends on
whatever was already there rather than purely on this run's build.
--no-history makes every deploy a single force-pushed root commit,
so gh-pages is unconditionally reset on top of whatever this branch
built, with no path for prior branch state to carry forward.

Changes:
- Added --no-history to the mkdocs gh-deploy invocation in
  docs-deploy.yml, alongside the existing --clean --force flags.

Security:
- N/A

Testing:
- Not run through an actual gh-deploy (would force-push the shared
  gh-pages branch); verified by reading mkdocs/ghp-import's
  documented --no-history behavior against the existing --force
  (permissions: contents: write) setup already in this workflow.

Backwards Compatibility:
- gh-pages loses its multi-commit history going forward (each deploy
  now replaces it with one commit instead of appending). The served
  site content is unaffected; only the branch's own commit log style
  changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netravnen netravnen changed the title Docs infra: fork-preview banner, per-section Atom feeds, safer gh-pages deploys feat(docs): add fork-preview banner, dual Atom feeds, gh-pages reset Aug 7, 2026
@netravnen

netravnen commented Aug 7, 2026

Copy link
Copy Markdown
Author

I run a public dev preview of this branch at
https://docs-dev.peeringdb.dk. The fork-preview banner and both Atom
feeds can be checked live there rather than only from local builds.

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