Build log · Software & apps

FPV Drones for Beginners — the Rotor Racing HK course site, mirrored page for page

Software & appsdone1 versionsofficial HTML20 pages140 images inlined41 MBzero requests

reflection not written yet · every fact below is generated from this project’s own spec and files

What it is

My school's five-day beginner FPV course ships its entire curriculum as a Google Site on the provider's own domain — Learn, Tinywhoop Go, Flight and Schedule, 20 pages, a full Monday-to-Friday timetable. The catch was the URL: a Workspace-published site's last segment is a name the author typed and nothing indexes it, so /fpv, /drone and 90 other guesses hit a sign-in wall. The real name was fpvdrones. This is that site mirrored as its own HTML — Google's markup and stylesheet, every image inlined, the nav rewired to local files. 41 MB, 20 pages, zero requests when it loads.

Version history

Straight from the project’s own card data — what each version added.

V1

20 pages · 140 images · 41 MB · official markup, scripts stripped, nav localised

Files on the site

What broke, and what I’d change

Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/fpv-drones.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.

Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.

Build spec

The project’s own build notes, rendered. Open the raw .md ↗

Show the spec

Build spec — FPV Drones for Beginners (Rotor Racing HK course site), official-HTML mirror

Goal

The five-day beginner FPV course material lives only as a Google Site published on a Workspace domain (sites.google.com/rotorracinghk.com/fpvdrones). Mirror the site's own HTML — not a remake — as a static folder that renders with no login, no Google session and no network.

Source

  • Root: https://sites.google.com/rotorracinghk.com/fpvdrones (public, no auth).
  • 20 pages; the navy bar exposes them all (/learn/<page>, /tinywhoop-go/<page>, /flight/<page>, /schedule).

Pipeline

  1. Render, don't crawl. curl returns a JS shell. Load each URL in headless Chrome (CDP :9444,
  2. 1440×1000), scroll to the bottom in ~0.85-viewport steps to trigger lazy images, scroll back, wait.

  3. Capture the hydrated DOM. document.documentElement.outerHTML — this is the site as it looks,
  4. including Google's inline stylesheet and computed layout.

  5. Inline images from inside the page. sitesv-images-rt URLs are session-signed: urllib → 403,
  6. in-page fetch(src,{credentials:'include'}) → 200. Convert each <img> to a data URL by rewriting src in the DOM before capture. Do the same for CSS background-image (the hero banner is a background, not an <img>): read getComputedStyle(el).backgroundImage and set an inline background-image:url(data:…) on the element.

  7. Strip Google's scripts. Remove every <script> and <link as="script">. Keeping Google's bundle makes the
  8. snapshot try to re-hydrate from the network — it blanks the local copy when offline.

  9. Localise the nav. Google writes tabs as root-relative /rotorracinghk.com/fpvdrones/…. Rewrite to
  10. per-depth relative paths (../learn/index.html) using a {page → folder} map; unknown targets point back at the live site.

  11. Shrink — sips -Z 1400 -s format jpeg -s formatOptions 85 for any data URL over 380 KB:
  12. 78.8 MB → 41.1 MB across 20 files (max 5.2 MB), all under the 25 MiB Pages cap.

  13. Layout — demos/fpv-drones/index.html + one folder per page (learn/antennas/index.html, …) so
  14. the site's own tabs resolve.

Constraints

  • /demos/fpv-drones stays in SKIP_PREFIX (~/scripts/theme-inject.py) so the site skin's +390 B and
  • theme button never touch the mirror.

  • Zero external requests at load: no script bundle, no remote <img>, no external href to the origin.
  • https:// links to third parties inside copy are left alone.

Verification (all automated)

  • 20 files written; per file: href="/rotorracinghk.com == 0, <img src="http == 0, Google <script src= == 0.

Step 6 — restore the menus (CSS-first, 20-line JS for the drawer)

Google's JS also drove the hover dropdowns and the mobile hamburger, so a stripped snapshot loses both. The site's own markup already contains the submenus (li[data-nav-level="1"] > .oGuwee); they are only display:none until Google's jsaction toggles them, and the items carry opacity:0 + margin-top:20px for the staggered reveal. scripts/fpv_add_dropdowns.py appends one <style id="fv-dropdowns"> and one <script id="fv-nav"> to every page (idempotent — it strips its own previous block first):

  • nav.plFg0c li[data-nav-level="1"]:hover > .oGuwee{display:block!important} — hover reveal, pure CSS,
  • right:0 so the Flight menu can't overflow the viewport, 300 px wide, padding:8px 18px per item, .PsKE7e{margin:0} to kill Google's staggered-reveal spacing (570 px → 350 px tall).

  • @media (max-width:920px) turns nav#yuynLe (the sidebar drawer) into a fixed panel, forces
  • opacity:1; transform:none on its items and expands the nested uls.

  • The 20-line script only wires the hamburger → nav#yuynLe display toggle, close on link click, Escape.
  • It must run on DOMContentLoaded — the block sits before </head>, where getElementById('s9iPrd') is still null.

Verify with CDP, not a screenshot alone: hover dispatch needs several Input.dispatchMouseEvent mouseMoved steps before CSS :hover applies, and .oGuwee must report display:block, width 300 and a rect whose right edge is inside the viewport.

  • Rendered home / Antennas / Schedule from the local copies against the live site: same header, banners,
  • nav highlight, timetable table intact.

  • Card modal (751 px iframe) opens ./demos/fpv-drones/ and the first page renders.

Known caveats (disclose, don't hide)

  • Dropdown menus and the site search are JS-driven, so they are inert in the snapshot.
  • YouTube embeds become poster frames / links.
  • Web fonts come from gstatic; offline they fall back to system faces.
  • Some of the origin's own image URLs already 403 — dead on the live page too.