Skip to content

Migrate the latest Omarchy redesign to Astro with Cloudflare previews - #195

Open
ryanrhughes wants to merge 143 commits into
masterfrom
refactor/astro-framework
Open

Migrate the latest Omarchy redesign to Astro with Cloudflare previews#195
ryanrhughes wants to merge 143 commits into
masterfrom
refactor/astro-framework

Conversation

@ryanrhughes

@ryanrhughes ryanrhughes commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Port barisgirismen/omarchy-site:redesign through 221b61bf912123f04dd8a53c3b5fc231d05393bf to ThinkOodle's Astro 6 starter framework. Public pages ship static HTML from native Astro components, typed content collections, and committed data snapshots. TypeScript preserves the interactive controls and original canvas/audio engines; styling uses native CSS layers and semantic tokens.

Preserves the redesign discussed in #171, including its footer, header/menu transitions, parallelogram theme picker and frosted wipe, video glide/drag controls, persistent music, portrait clusters, testimonials, manual, news, and community pages. Imported HTML collection bodies render directly so indented source elements remain HTML. Important routes have Markdown twins, shared SEO metadata, LLM endpoints, and component Storybook stories. Meetups retain the country map, filters, linked pins, responsive cards, archive, and homepage rail. Astro's default page fade remains disabled.

The latest sync reviews 12 source commits since bb6cdcf:

  • Phones gain the section background bar, a wordmark when the first section touches it, and a subtle bottom hairline. A moving section boundary paints both background colors instead of briefly exposing the content beneath the wordmark.
  • The phone menu dims and blurs the page. The header surface has its own blur layer so it cannot prevent the menu backdrop from working. Touch hover no longer strands the hero labels; the full 260ms closing fold remains visible.
  • Portrait clusters gain clearance so lifted faces and their rings are not clipped at the grid edges.
  • News publishes article:published_time; search uses the phone keyboard's search action and announces result counts. The skip link matches the source styling.
  • Deployment/validation checkouts do not persist credentials, and workflows have bounded timeouts. Existing Astro behavior covers production canonicals, noindex 404, malformed hashes, escaped redirects, and manual focus outlines. React hydration/devtools changes and the parked marketplace's unused search field do not require a runtime port.

redesign-source.json records the full reviewed source hash and prior baseline. README documents the fetch/compare workflow. Reference fixtures come from the source checkout, never the Astro output, and validation checks that their hashes agree with the sync record. Builds use committed snapshots and do not fetch changing content.

Wrangler builds and serves dist/ on the existing omarchy Worker, with preview versions for non-production branches. GitHub PR validation runs Astro, Cloudflare packaging, content/asset checks, Storybook, and browser regressions. Storybook owns copying public assets, avoiding a race with Vite's duplicate copy.

Local validation:

  • Astro check: no errors, warnings, or hints. Production and Storybook builds pass.
  • 20,919 content, asset, route, and framework checks across 90 HTML pages and 84 source document records.
  • All 107 browser tests pass, including 30 source layout comparisons across 15 routes, two filtered meetup layouts, mobile header boundaries and touch behavior, search announcements, malformed hashes, keyboard access, motion interactions, and every Storybook state.
  • Source/production screenshot comparisons cover the phone hero, section boundaries, menu blur, and lifted portraits.
  • Wrangler dry run validates the static build and packaging.

Layout fixtures use loaded fonts, Tokyo Night, a fixed date, and reduced motion. Motion interactions run separately. Live service counts and meetup availability can differ between snapshot refreshes.

Cloudflare Workers Builds succeeded for 26ee4e7e0269bf33fe1eee99df85579de85c7596 and deployed preview version d68ce809-0373-43ce-83ad-c49a6a7a27ec. The branch preview passes all 32 live layout comparisons, meetup filters/pins, the phone header wordmark and section-edge backgrounds, menu blur, theme selection, search announcements, malformed hashes, article dates, and Markdown/RSS/assets. The GitHub Astro validation workflow also passes on this commit, including all 107 browser tests. This PR has not been merged or promoted to production.

CodeQL remains red on 26ee4e7: Actions and JavaScript analyses pass, but default setup still includes Ruby after removal of the Ruby development server; the PR scan reports 13 findings (4 high, 9 medium), the same counts as before this sync. These require security review before merge. Scanning settings have not been changed. The Astro 6/Storybook dependency tree retains the previously documented upstream audit advisories.

barisgirismen and others added 30 commits September 4, 2026 00:55
The redesign of omarchy.org, as this repository. Nothing about how the
site is written, generated or served changes: a post is still a
Markdown file rendered by bin/build-news and committed with the feed,
the manual is still rendered from the Omarchy repository and
committed, a theme is still a pull request that edits themes/index.html
and adds a screenshot, and GitHub Pages still uploads a folder on every
push to master. The app renders that folder from the files already
here, and pages.yml gains one build step in front of the upload it
already did.

Every route is rendered at build time. Server functions run at build
time and leave their results as static files, so nothing runs on a
server. The plugin listing arrives with its default page rendered and
does every filter, sort and page in the browser, over a trimmed
catalogue fetched once. All 2,186 plugin pages are rendered by name. A
/404/ route is written to 404.html, the file Pages serves for an
unknown address. The site's own URLs are kept: a trailing slash on
every page, news at its dated address.

The feed, the installer scripts, the security contact, the assets, the
images beside posts and chapters, and the pages not yet redesigned are
laid over the output byte for byte. Two addresses folded into other
pages, /manual/toc/ and /security/credits/, get redirect pages the way
/discord/ always has. A scheduled workflow refreshes the plugin
catalogue from the marketplace repository and commits it.

Proven cold, from a fresh checkout with no environment set: npm ci,
npm run build, npm run parity. Every one of the 571 addresses the site
serves today answers from the build, and 480 of 481 files pass through
byte-identical; the one that differs is 404.html, on purpose.

Nothing is removed. The generated pages and the hand-written ones stay
committed; the build reads them. Retiring any of it is a separate
decision.
The 01-05 numerals next to each pillar were extra chrome. Titles and bodies stay, as a plain list.
Both faces come from Fontsource. Headings, card titles, and other display type use Geist Variable. Everything else uses JetBrains Mono Variable. Inter is gone.
The pull quote stays in the heading face. The name and role sit in the body face with the rest of the copy.
Heading and body stacks named only those two faces. Canvas labels on the plugin map do the same.
paintFavicon removed every rel=icon node, including the one React keeps in the route head. The next commit then called removeChild on a node with no parent. The tab icon is created outside React and only that tagged link is replaced.
The Inter helper was renamed in two steps and HMR left the old name in the mounted effect, which threw out of the document shell. The snap now inlines the Geist and JetBrains checks and swallows failures. The wordmark preload uses the same CORS mode as mask-image, which was discarding it. The root route also has an error page.
Home was waiting on the full search index, which parses the 3.5MB plugin catalogue, even though the palette loads that index when it opens. The build still writes the file from the plugins page. Fontsource was also registering Cyrillic, Greek and Vietnamese faces the site never uses; only Latin and Latin-ext load now, with font-display block so headings do not reflow. Home plugin teasers serialize as catalogue cards rather than full plugin records.
Vite was serving styles.css as JavaScript, and the SSR stylesheet was empty, so a reload showed unstyled markup until hydration. Importing the CSS as a module fills that sheet. The old site's stylesheets are kept out of it. Fonts use swap so copy is visible while they arrive. The pixel field and the router devtools load after first paint, and the video thumbnails no longer preload ahead of the wordmark.
The pinned reveal grabbed the wheel at the bottom and finished the scroll itself. The footer is now ordinary content after the page.
Six lattice cells is about 66px on a desktop row, which cannot hold
"Install" in JetBrains Mono plus the button's horizontal padding. The
label sat against the edges. Size the button from its label and keep
the blended ghost the same width.
37signals is red, Basecamp gold, HEY purple, Cloudflare orange. The
White theme uses deeper versions of those hues so 13px type still
reads on the light field. The marks inherit the same color.
The name sat in a box that covered the preview. It is now just the
letters, with a stroke in the page ground so they still read on the
dimmer, including on White.
Fourteen and fifteen lattice cells were narrower than the labels in
JetBrains Mono plus the large button's padding, so Get Omarchy and
See it in action sat on the edges. Size them from the label. The 404
button had the same lock; it follows the label too.
The header already outlines keyboard focus. Footer chrome was
hover-only, so tabbing through the same links on every page was
invisible. Use the same 2px ring.
Iterate on the redesign
Without the plate, the name sat on the dimmer in the theme's text
colour with a stroke in its background. Right way round for the four
dark themes, wrong for Rosé Pine and White: dark letters on a dark
dimmer, held up by a thin light edge. Now the lighter of the two inks
fills the letters and the darker edges them, so the light themes get
their own cream or white, edged in their text colour.
The partner marks in the footer take a darkened set on White and the
true brand colours everywhere else. Rosé Pine defined none, so it
inherited the dark-ground set: Basecamp's yellow at 1.3:1 on the
cream. It now uses the same darkened set as White, 4.6:1 and up.
Five team photos on the teams page were circular crops with
transparent corners while the rest were square. The redesign had
square replacements parked in public/team and a swap in the porter
that pointed the page at them, which stopped matching once the build
read this repository's own files instead of the live site.

Now there is one place. The five photos in assets/images/team are
replaced with square crops of the same pictures, from the same
profile photos, at the same 240x240 size and name. The six theme
preview screenshots move from public/ to assets/images/theme-previews.
public/team and the porter's swap go. public/ is left holding only
what the build generates.
The bar has two copies of its labels: a blended ghost over the hero
and the real layer, and one swap between them happens when the
pointer nears the bar and another when the hero scrolls away. The
real Install button wears a 1px transparent border; the ghost's
placeholder for it did not, so the ghost's icons sat 2px right of the
real ones and every swap read as a wiggle. The placeholder now wears
the same border, and the two layers land on the same pixel.
The same Base UI primitive underneath, with the edge fade as a prop. It
reads how far the content runs past each edge, which the viewport
already publishes, so the fade follows the scroll in every browser
rather than only in those with scroll-driven animations.
The chapter list and the column around it move to a layout route, so
every chapter renders inside one shell. They used to belong to each
page, and the manual's opening page and a chapter are two routes: every
move between them rebuilt the list, which restarted its scrollbar's
fade under a pointer that had never left it, and lost where the list
was scrolled to.

The list itself is a scroll area now. It keeps the page's own 3rem
above and below it, its entries are one line each with an ellipsis for
the long ones, and its scrollbar sits out by the rule rather than
against the names. It also sticks at the offset it already sits at
rather than at the bar, which used to move it the moment the page
scrolled.
A chapter's prose was set to 65 characters inside an 800px column, so
a third of the column stood empty beside every paragraph. Now one
measure holds everything in a page's prose - paragraphs, headings,
tables, code blocks and screenshots alike - and it is the width a news
post reads at, named once as --measure so the column, the title and
the prose cannot drift apart.

The chapter sits at the page's right edge, so the room left over is in
one place, between the sidebar's rule and the words. A heading jumped
to lands where the chapter's title does rather than pinned against the
bar; that offset is the manual's own, and elsewhere a jump still
arrives at the bar.
They wore each partner's brand colour, which made four bright spots in
a quiet corner. They are the page's text colour again, a shade above
the sentence around them so they still read as links, and they
brighten on hover. Every theme gets this from its own palette, so the
marks are light on a dark page and dark on White and Rosé Pine.

The partner colour tokens go with them; nothing else used them.
The two links carrying a partner mark were laid out as flex boxes
aligned to the middle of the line, which put their words a little
below the baseline the rest of the sentence sits on. They are plain
inline text again, with the mark beside them as a glyph nudged onto
that same baseline, so "37signals" and "Cloudflare" line up with the
words around them.
The footer credited four partners and drew two of them. Basecamp comes
in as one shape with an even-odd fill, so the peak and the slope stay
as cut-outs in a single ink rather than flattening into a blob, and
HEY as the drawn weight of its hand rather than the hairline of its
pinned-tab icon, which went spidery beside the others at this size.
Both take the page's text colour like the two already there, sit on
the sentence's own baseline, and keep Cloudflare's gap between mark
and word.
The three paragraphs under the wordmark sat at three different
distances: 20px after the lede, 8px between the two credit lines. They
are 16px apart now, the same as the space under the wordmark, so the
column steps down evenly.
A column of five pillars ran down the right of this section, making in
a list the points the sentences and the quote beside it already make.
It goes: the page introduces the idea here, and the manual does the
explaining.

What is left is set across the page rather than stacked - the words on
the left at their own measure, the quote holding the right edge across
from them - so the section reads as one thought instead of a column
with an empty half beside it.
barisgirismen and others added 21 commits September 6, 2026 17:56
Stamping a target on every outgoing link as soon as the page loaded
changed the server HTML before React had taken it over, and every one
of those links came up as a hydration mismatch. Now a click on a link
to another host sets the target right before the browser follows it.
Same result, nothing touched up front, and a modifier or middle click
keeps its own meaning.
The refresh job now reads the Omarchy calendar on Luma into
src/data/meetups.json: title, when, where, link, hosts, and the cover
picture saved small under public/images/meetups. With a LUMA_API_KEY
in the repository secrets it uses Luma's API, which is where the covers
come from; without one it reads the calendar's public feed, which has
everything else, so the page can be built and checked either way. The
key is read and write for the whole calendar and Luma makes no read
only kind, so it lives in the secrets only, and only the list is ever
read. The first snapshot here is from the feed.
The download figure was quoted by hand from the news. The refresh job
can now count it: each run asks Cloudflare, which serves
iso.omarchy.org, how many bytes the ISO files sent on the days since
the last count, divides by the size of the ISO, and adds that to the
total, so the figure only moves forward and a missed run is caught up.
A browser's one request and a download manager's many pieces count the
same. Countries are the ones that took at least a whole ISO in a day.
It needs CLOUDFLARE_API_TOKEN, a token that can only read that zone's
analytics, and CLOUDFLARE_ZONE_ID, both repository secrets; without
them the figure stays as it is.
Port redesign a91c428 to native Astro components, content collections,
layered CSS, shared metadata, Markdown twins, and curated LLM endpoints.
Preserve the content, public URLs, original assets, canvas, themes, music,
and keyboard interactions. Update the static Pages deployment to dist/.

Prerender named Storybook states for a usable static component library.
Validate content and asset parity, site interactions, accessibility, and
all Storybook stories in development and production.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 6, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
omarchy 26ee4e7 Commit Preview URL

Branch Preview URL
Sep 07 2026, 02:05 AM

Comment thread scripts/refresh-data.mjs Fixed
Comment thread tests/browser/site.spec.ts Fixed
Comment thread tests/parity.mjs
"robots.txt",
])
check(
read(path.join(root, endpoint)).includes("https://omarchy.org"),
Comment thread scripts/add-voice.mjs
}

voices.push(entry)
writeFileSync(DATA, JSON.stringify(voices, null, 2) + '\n')
Comment thread scripts/refresh-data.mjs Fixed
Comment thread scripts/refresh-data.mjs Fixed
Comment thread scripts/refresh-data.mjs Fixed
Comment thread scripts/refresh-data.mjs Fixed
Comment thread scripts/refresh-data.mjs Fixed
Comment thread src/lib/theme.ts Outdated
Comment on lines +64 to +66
export const themeInitScript = `(function(){try{var t=localStorage.getItem('${THEME_KEY}');var ok=${JSON.stringify(
SITE_THEMES.map((t) => t.id),
)};if(ok.indexOf(t)<0){t=ok[Math.floor(Math.random()*ok.length)];localStorage.setItem('${THEME_KEY}',t)}document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme='${DEFAULT_THEME}'}if(!document.querySelector('link[rel="icon"][data-theme-icon]')){var l=document.createElement('link');l.rel='icon';l.type='image/svg+xml';l.href='/brand/omarchy-logo.svg';l.setAttribute('data-theme-icon','');document.head.appendChild(l)}})()`;
Port the current redesign's layouts, native interactions, content, and assets while retaining the Astro framework. Capture desktop and mobile layout references from source commit 8419a5a and run those regressions, motion tests, and Storybook checks in PR validation.
@ryanrhughes ryanrhughes changed the title Migrate the Omarchy redesign to Astro with Cloudflare previews Migrate the latest Omarchy redesign to Astro with Cloudflare previews Sep 6, 2026
Comment thread scripts/refresh-data.mjs
Comment on lines +29 to +30
s
.replaceAll("&amp;", "&")
Comment on lines +10 to +11
plainText(s)
.replace(/&amp;/g, "&")
Comment thread tests/browser/site.spec.ts Fixed
Comment thread scripts/refresh-data.mjs
}));
await writeFile(
path.join(OUT, "plugins.json"),
JSON.stringify({ generatedAt: catalog.generatedAt, plugins }),
Comment thread scripts/refresh-data.mjs
}
await writeFile(
path.join(PUBLIC_DATA, "explorer.json"),
JSON.stringify(explorer),
Comment thread scripts/refresh-data.mjs
// wants it and a refresh never shows up as a formatting change.
await writeFile(
MOMENTUM,
await prettier.format(JSON.stringify(momentum), { parser: "json" }),
Comment thread scripts/refresh-data.mjs
momentum.checked = day(new Date());
await writeFile(
MOMENTUM,
await prettier.format(JSON.stringify(momentum), { parser: "json" }),
Comment thread scripts/refresh-data.mjs
.slice(at + 1)
.replaceAll("\\n", "\n")
.replaceAll("\\,", ",")
.replaceAll("\;", ";");
Comment thread src/lib/theme.ts
Comment on lines +71 to +73
)};var light=${JSON.stringify(
SITE_THEMES.filter((t) => t.light).map((t) => t.id),
)};var dark=${JSON.stringify(
Comment thread src/lib/theme.ts
Comment on lines +73 to +75
)};var dark=${JSON.stringify(
SITE_THEMES.filter((t) => !t.light).map((t) => t.id),
)};if(ok.indexOf(t)<0){var pool=window.matchMedia&&matchMedia('(prefers-color-scheme: light)').matches?light:dark;t=pool[Math.floor(Math.random()*pool.length)];localStorage.setItem('${THEME_KEY}',t)}document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme='${DEFAULT_THEME}'}if(!document.querySelector('link[rel="icon"][data-theme-icon]')){var l=document.createElement('link');l.rel='icon';l.type='image/svg+xml';l.href='/brand/omarchy-logo.svg';l.setAttribute('data-theme-icon','');document.head.appendChild(l)}})()`;
Port the latest page headers, Windows showcase, videos, meetup map and rails, and theme/menu transitions. Record the reviewed source commit and compare workflow for future updates, with source-derived content/layout fixtures and browser coverage.
await page.locator("[data-carousel-video]").first().click();
await expect(page.locator("[data-carousel] iframe")).toHaveAttribute(
"src",
/youtube-nocookie\.com\/embed\//,
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.

6 participants