Serve the relaunched site - #124
Conversation
The site's CSS was one 200-line file appended to Bootstrap, and the order it was written in hid a bug. Our theme values landed *after* the Bootstrap import, where Bootstrap's own !default declarations had already won, so $primary stayed at Bootstrap's #0d6efd while :root separately declared --bs-blue: #4c60d8. The page carried two blues: buttons and links in one, headings and the navbar in the other. Nobody had written the wrong colour anywhere -- the file just could not express the intent in that order. So the entry point is now only an ordering statement: fonts, Bootstrap's functions, our variables, Bootstrap, then our own partials. --bs-primary and --bs-btn-bg both compile to #4c60d8 now. $blue is set alongside $primary because --bs-blue is what the pre-relaunch rules colour headings with, and leaving it at the default would have reproduced the same split from the other side. The partials that arrive here are the ones that style pages which exist today. Components and page styles belonging to the new pages come with those pages, rather than sitting in the tree as dead CSS in the meantime. pages/_legacy.scss holds the rules that belong to the pre-relaunch pages, kept verbatim and kept together so the cutover can delete a file instead of picking rules apart. One of them is the CSS that hides the Russian integrators from non-Russian visitors on /introduction: the replacement is a Ruby helper, but until that page goes this rule is the only thing enforcing it, and dropping it early would show that logo wall to everyone. The other legacy rule worth naming is the h2 title style. _base.scss gives article headings the new treatment; the legacy selector is longer and therefore wins, and it applies exactly to pages rendered inside the .container wrapper -- which is every page that is not full-bleed. The two rules split along the same line the layout's fullwidth switch will. FONTS. The stylesheet opened with three @import url() lines: two to fonts.googleapis.com and one to cdn.jsdelivr.net. Every visitor announced themselves to two third parties before the page could paint, and first paint sat behind DNS lookups we do not control. This is the same argument that moved the legacy logo off a maintainer's personal CDN in d60729a. IBM Plex and bootstrap-icons are now served from public/fonts, subset to latin, latin-ext and cyrillic; Chinese falls back to the system stack, because IBM Plex has no CJK glyphs and the families that do are megabytes. The files are committed, since public/ bypasses Sprockets and a generated file would have to exist before assets:precompile in the image and before CI renders a page, for no benefit. tools/copy-fonts.mjs regenerates them from the @fontsource packages and fails loudly if a file the stylesheet asks for is missing. JAVASCRIPT. One window.onload handler becomes modules under app/javascript/src. That handler assigned window.onload rather than adding a listener, so any second assignment would have silently replaced all of it, and it waited on every image -- on the Open Wall, the whole gallery -- before anything became interactive. It is DOMContentLoaded now. The zoom initialiser called `new bootstrap.Modal(document.getElementById(...))` unconditionally. Most pages have no zoomable image and so no #modalZoom, the constructor threw on null, and because every initialiser shared the one handler, the throw took external links, timestamps and confirmations down with it on those pages. It returns early now. @hotwired/turbo-rails and @hotwired/stimulus are removed: neither was ever imported. The layout's data-turbo-track goes with them, having been inert. The lockfile is regenerated with Yarn 4 -- it must never be rewritten by a v1 yarn, which produces a format `yarn install --immutable` in the Dockerfile cannot read. Bootstrap is imported per component rather than wholesale. 189.4 KB to 181.8 KB unminified, which is a small win; the point is the import list, which now states what the markup actually depends on. Audited against the views first: dropdown, collapse, offcanvas, carousel and Modal are used, and nothing uses tooltip, popover, tab, alert or scrollspy. Verified: 231 runs, 873 assertions, 0 failures. Diffing the compiled selector set against master's leaves exactly one difference -- label.required:after, which is now spelled ::after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The <head> carried a title and nothing else. A link to openipc.org shared in Telegram, in a forum post, or shown in a search result therefore appeared as a bare URL with no summary and no image -- for a project whose reach is almost entirely word of mouth between people posting links to each other. Added: a description, an OG/Twitter card set, a canonical, and a theme colour. Three of those have a detail worth keeping: The canonical carries the path and not the query string. ?locale=ru is a rendering of the same page rather than a different one, and pointing all three translations at one canonical is what stops them competing with each other for the same content. og:image is absolute, because crawlers do not resolve a relative one, and it lives in public/ rather than the asset pipeline. OG images are fetched once and cached by URL by every consumer that has ever seen the link, so a fingerprint in the name would invalidate those caches for no gain. The two font faces every page renders body text in are preloaded. crossorigin is required even though they are same-origin -- fonts are fetched in CORS mode, and a preload without it is simply fetched a second time. The navbar and footer move into layouts/ partials. Nothing about them changes: same links, same order, same locale switcher, and the footer's icons still resolve through asset_path, which is not cosmetic -- production runs with config.assets.compile = false, so a literal /assets/... path is a 404, and 2ec9baa fixed exactly these lines. Extracting them now means the relaunch can rewrite the navigation in one file a reviewer can read, instead of inside a diff that also touches <head>. <main> gains a content_for(:fullwidth) switch for pages that lay out their own full-bleed sections. Flash messages keep the container in both branches, or they would run edge to edge. Pages that set nothing keep the wrapper they have always had, and a test holds that. The tests are the ones that would have caught each way a <head> goes wrong: a missing or duplicated description, a description that came back as "translation missing", a canonical that kept the query string, a relative og:image, an og-default.png that is not there, and a preload naming a font file the stylesheet does not generate. 238 runs, 903 assertions, 0 failures. i18n-tasks reports no missing keys and 102 unused, unchanged -- the new key being used is what keeps that number flat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
Seven pages -- home, get-started, low-latency, ecosystem, business, community, donate -- plus the shared partials they are built from. They answer on their own URLs from this commit, but the root route and the navigation still serve the pre-relaunch structure, so nothing a visitor sees changes. The cutover is a separate change, and this one can go to dev.openipc.org and be looked at first. The copy is written in all three locales the site now serves. English and Russian are as drafted; the Chinese was written for this commit and has not been read by a native speaker -- it should be, before it is linked to. i18n-tasks reports 347 keys in each of the three files and no missing translations. Not everything from the draft survived contact with the current tree, and the rendering tests are what found each one. /get-started hardcoded four WebUI screenshots by filename -- webui/preview.jpg and three others. The gallery was reshot at twice the resolution in 0f42514 and every one of those files stopped existing; the page raised on render. It now takes them from WebuiGallery, the same manifest /web-interface reads and the same one tools/webui-gallery photographs from, so the two cannot disagree about what exists. Tile and full-resolution copy are passed as a pair, which is what data-zoom in src/zoom.js is for. Every link in the new copy that pointed at wiki.openipc.org now points at github.com/OpenIPC/wiki. That host is retired; 2842ffa and 73c0961 removed the last references to it and this copy would have put four of them back. The partner wall moves out of the markup into PagesHelper, transcribed from /introduction as it stands today rather than from the June draft, which was ten entries out of date. Entries that page has commented out are commented out here too, with their URLs, so nothing appears or disappears silently when /introduction goes. REVIEW POINT: the Russian integrator block is currently commented out on /introduction, so no visitor sees any of it. Rendering it for :ru brings it back. That is what the relaunch plan asks for and it is gated to :ru as the maintainers asked, but it is a content decision -- say so if it should stay hidden. Worth knowing: the CSS that was supposed to enforce that gate, `html:not([lang="ru"]) article.introduction .col.logo.ru`, has never matched anything. No logo on that page carries the `ru` class it selects on. The gate being in Ruby now is what makes it testable, and there is a test. /open-wall is uncommented. It was commented out in ed0e025, a bulk tidy-up, while five places that redirect to it were left in place -- twice in snapshots_controller, once in admin/snapshots_controller, and the breadcrumb on three views. All five fell through the catch-all and answered a 302 to the homepage. This exposes nothing new: `resources :snapshots` has served the same gallery at /snapshots throughout. Snapshot.latest_per_camera lifts the correlated subquery out of SnapshotsController#index, because the homepage mosaic wants the same list and two copies of that query is one too many. pages.donate belonged to the band rendered under every page. It is now the /donate page's, and the band's two keys moved to pages.donate_band -- otherwise lazy lookup in the partial and the new page's title would have fought over the same key, and the band would have lost its please_support string. 272 runs, 1223 assertions, 0 failures. Each page is rendered in each of the three locales and checked for "translation missing"; every internal link on them is followed and required not to land on the catch-all; the homepage is rendered against an empty database, which is what a fresh checkout has. rubocop is unchanged at 53 offences across the files this touches, and the two new files add none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The root becomes pages#home, the navigation becomes Get Started | Hardware | Low Latency | Ecosystem | For Business | Community, and the single copyright line becomes a four-column footer. Everything that has been building up over the last four changes is now what a visitor sees. The pre-relaunch URLs redirect rather than disappear. They are in search results, in forum posts and in the wiki, and none of that is ours to edit: 301 for the ones whose destination is settled, so what is indexed transfers instead of competing, and 302 for /about alone, because it is meant to become a page of its own and a 301 is cached by browsers indefinitely -- it would outlive the decision. There is a test per redirect, and a test that every target resolves, because a redirect to a path that has itself gone still answers 301 here and lands the visitor on the homepage. TWO ROUTES THAT ANSWERED 500. Building the new menu meant looking at every URL it would link, and two of them had never worked: /tools/bandwidth-calculator routed to pages#bandwidth_calculator. There is no such action and no such template. Every request raised AbstractController::ActionNotFound, which in production is a 500. /tools/timelaps-interval-calculator routed to pages#timelaps_interval_ calculator, which likewise does not exist -- the template is there but named with hyphens, so nothing could render it, and it has no translations either. Both routes are removed, so an inbound link now reaches the homepage rather than an error page. Neither page is linked from the new navigation, and the unfinished timelapse template is left in the tree: finishing it is a decision for whoever started it, not something to settle with a routing change. The upload API keeps its own test. Cameras POST to /snapshots and the gallery moved to /open-wall; the API did not move at all, and nothing in the relaunch may shadow it. That test found a third thing. Snapshot read Rails.application.credentials.mac.blacklisted directly, so without config/master.key -- which is what a fresh checkout and the test environment have -- credentials.mac is nil and every upload raised NoMethodError rather than simply having nothing to blacklist. Production has the key, so it never showed there; it made the API impossible to exercise anywhere else. It digs now. The pages that were replaced are deleted, along with their translations and the navigation entries that pointed at them: i18n-tasks unused goes from 102 to 72, below where it started, rather than up. pages/_legacy.scss loses two of its three rules with the pages they styled -- including the CSS territory gate that never matched anything. Two white stripes showed up in the rendered page where one dark band met the next: the CTA band and the footer both carried a top margin on top of their own padding. Both dropped. 289 runs, 1276 assertions, 0 failures. i18n-tasks reports no missing translations across en, ru and zh. rubocop is 734 offences over 114 files against 742 over 111 on master. Every page was rendered in a browser in all three languages before this was committed, which is how the footer stylesheet turned out never to have been imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
PR Summary by QodoCut over to the relaunched site with legacy redirects
AI Description
Diagram
High-Level Assessment
Files changed (20)
|
Code Review by Qodo
1. Legacy redirects drop locale
|
# Conflicts: # app/assets/stylesheets/application.bootstrap.scss # app/assets/stylesheets/pages/_legacy.scss # app/controllers/pages_controller.rb # app/views/layouts/_footer.html.erb # app/views/layouts/_navbar.html.erb # app/views/layouts/application.html.erb # app/views/pages/_donate.html.erb # app/views/pages/donate.html.erb # app/views/shared/_cta_band.html.erb # config/locales/pages.en.yml # config/locales/pages.ru.yml # config/locales/pages.zh.yml # config/routes.rb # test/controllers/page_metadata_test.rb # test/controllers/relaunch_pages_test.rb
Last of five. Stacked on #123 → #122 → #121 → #120. This is the one that
changes what a visitor sees.
Root becomes
pages#home, the navigation becomesGet Started | Hardware | Low Latency | Ecosystem▾ | For Business | Community▾,and the single copyright line becomes a four-column footer.
Redirects, not deletions
The pre-relaunch URLs are in search results, forum posts and the wiki, and none
of that is ours to edit. 301 where the destination is settled, so what's indexed
transfers rather than competes. 302 for
/aboutalone — it's meant to becomea page of its own, and a 301 is cached by browsers indefinitely, so it would
outlive the decision.
There's a test per redirect, plus one that every target resolves — a redirect
to a path that has itself gone still answers 301 and lands the visitor on the
homepage.
Two routes that answered 500
Building the new menu meant checking every URL it would link. Two had never
worked:
/tools/bandwidth-calculator→pages#bandwidth_calculator. No action, notemplate. Every request raised
AbstractController::ActionNotFound— a 500 inproduction.
/tools/timelaps-interval-calculator→pages#timelaps_interval_calculator,which likewise doesn't exist. The template is there but named with hyphens, so
nothing could render it, and it has no translations either.
Both routes are removed, so an inbound link reaches the homepage rather than an
error page. Neither is linked from the new navigation. The unfinished timelapse
template is left in the tree — finishing it is a decision for whoever started
it, not something to settle with a routing change.
A third bug, found by testing the upload API
Cameras POST to
/snapshots; the gallery moved to/open-walland the API didnot move at all, so there's a test that nothing shadows it. It failed:
Snapshotreadcredentials.mac.blacklisteddirectly, so withoutconfig/master.key— which a fresh checkout and the test environment have —every upload raised
NoMethodErrorrather than simply having nothing toblacklist. Production has the key, so it never showed there; it made the API
impossible to exercise anywhere else. It digs now.
Tidying
Replaced pages, their translations and their nav entries are deleted:
i18n-tasks unusedgoes 102 → 72, below where it started rather than up.pages/_legacy.scssloses two of three rules with the pages they styled.Two white stripes showed up where one dark band met the next — the CTA band and
the footer both carried a top margin on top of their own padding. Both dropped.
Verification
bin/rails testi18n-tasks missingi18n-tasks unusedrubocopThat last row earned its place: it's how the footer stylesheet turned out never
to have been imported — the suite was green while the footer rendered on white.
Still open for you
ruhomepagein these screenshots. Currently nobody sees it; this brings it back.
🤖 Generated with Claude Code
https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi