Port the site from Webflow to Astro; deploy to Firebase Hosting - #411
Conversation
Raw reference snapshot of the live Webflow site so the port can run offline. See webflow-export/README.md for layout and the theme model. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
…iner - global.css: encode the live Webflow variables as CSS custom properties (dark by default on :root, light via [data-theme="light"] / .is--mode_1, dark re-assert via [data-theme="dark"] / .is--mode_0), responsive type and spacing scale, General Sans (500/600 woff2 from the live site), and Tailwind v4 @theme tokens (bg-surface-*, text-text-*, border-border, brand-*, button-*, text-title-*/body-*, py-section-*, px-container). Legacy token names used by pre-port pages keep working as aliases. - @Utility page-container / page-container-lg (Webflow g_page--container), title-*/body-*/label-* text styles, badge-icon, divider. - New Section.astro (theme/variant/spacing/container props) and Container.astro for Phase 2 pages to compose from. - lib/utils.ts: teach tailwind-merge about the custom font-size tokens so `text-title-l text-text-1` keeps both classes. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Ports g_button_primary (gradient pill with hover overlay), g_button_secondary (1.5px inset-border pill) and a tertiary text-link variant, with `default` and `small` sizes. Adds ButtonLink (renders a real <a>), ButtonIndicator (the "View our Demo" live dot) and a `trailing="dot"|"arrow"` prop. Old shadcn variants/sizes (default, outline, ghost, link, sm, lg, icon) and GradientOutlineButton still work as aliases. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Sticky 80px (72px mobile) nav with the live link set (Products dropdown with
Code Push / Shorebird CI, Docs, Pricing, Customers, Blog, About), Sign in
and Try for Free CTAs, and the mobile hamburger -> sheet menu. The nav
follows the theme of the section under its bottom edge via
elementsFromPoint + closest('[data-theme]') on scroll (no GSAP).
Logos now use currentColor so they render on both themes; LogoFull is the
exact 123x24 SVG from the live site. sheet.tsx gains opt-in
`overlayClassName` and `hideClose` props for the mobile menu.
Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Footer: brand column (logo, "Deploy on your schedule", LinkedIn/GitHub/ Discord/RSS circles) plus Product / Resources / Company / Legal columns with the live links; stacks and centers on mobile. The "Start shipping fast, today!" band is a separate page section on the live site, so it ships as cta-band.astro for pages to include above the footer. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
main.astro now loads the live Plausible script (pa-KkCTgCak2h0t6JBTj3Tgm) and init snippet plus the LinkedIn Insight tag (partner 8654276), preloads the General Sans woff2 files, applies the dark surface/text tokens on <body>, and accepts optional `description` and `image` props for the meta and OG/Twitter tags. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
scripts/import_webflow.py (stdlib only) converts webflow-export/cms/*.json into content collections, downloads referenced images with curl, and is idempotent. Blog schema gains intro/readingTime/ogImage/seoTitle/ seoDescription/highlight; success stories gain industry/companySize/ website/intro/seoTitle/seoDescription and highlights becomes optional. New blog authors (Wesley Peck, Abhishek Doshi, Nick Weatherley, Mac Carrithers, Brandon DeRosier) with headshots; existing job titles updated to match the CMS. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
69 posts: 39 keep their hand-written Markdown bodies with frontmatter regenerated from the CMS; 30 new posts converted from CMS HTML. Five repo slugs renamed to the Webflow slugs (redirects added in astro.config.mjs). Body images downloaded to src/assets/blog/<slug>/, covers to src/assets/blog/covers/, share images to public/blog/og/. The CMS API exports rich-text code blocks as empty <pre></pre>; the four affected new posts get their code from the rendered live pages saved in webflow-export/pages/blog__<slug>.html. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Success stories: 8 keep their bodies with frontmatter extended
(industry, companySize, website, intro, seoTitle, seoDescription); scapia
and vetc are new, with highlights derived from the story text.
src/data/{reviews,logos,team}.json for the homepage and about page, with
avatars, brand logos (SVG) and team headshots downloaded into src/assets.
Asset paths are /src/assets/... strings for use with import.meta.glob.
Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Rebuild `src/pages/index.astro` and `src/components/home/*` to match the live shorebird.dev homepage section for section: dark hero with the "home" Lottie (extracted from the exported .lottie bundle, bitmap inlined) and the "Trusted by" marquee from logos.json; the "opinionated platform" card with Code Push / Shorebird CI columns; Benefits; the dark Enterprise-scale card; Secure and Compliant; How Shorebird can help; a CSS scroll-snap testimonials row fed by reviews.json; Resources cards on the gradient band; the FAQ accordion as native <details>; then the shared CTA band and footer. Copy is verbatim from webflow-export/pages/home.html; images are copied into src/assets/home. Old home components without a live counterpart (problem, solution, for-developers, for-teams, pricing-overview, faq-overview, blog-overview, success-stories-overview, callout, features) are removed. logos.json drops the extra hand-added PushPress entry so the marquee shows the 7 CMS logos like the live site (the CMS "Apnaklub" asset is the PushPress mark). Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Adds /product/code-push built from the homepage patterns plus new components under src/components/product/ (hero with the Code Push Lottie, six-card benefits + PushPress quote, the dark "How it works" Arcade demo card, the four-card overview grid, success-story cards, and the yearly pricing overview). Reuses the homepage testimonials, resources and FAQ sections; the FAQ component now takes an optional `faqs` prop. The Lottie JSON is the Webflow export with its 2048px bitmap mask downscaled to 512px (2.9MB -> 336KB). Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
/product/shorebird-ci is now just the sunset notice in the hero slot, followed directly by the footer (no CTA band), matching the live page. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Adds /code-push-guide: title, intro rich text, the ebook cover and a React download form that posts to the Loops endpoint the Webflow form used (source=ebook-CP-guide, userGroup=web-download) and then shows the "Download the guide" link. The PDF is served from public/guides/ and the 1200x630 ebook image is the page's OG image. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
The content import kept the repo's original cover PNGs for posts that already existed locally, but the live site shows the redesigned (purple) covers from the CMS `featured-image` field for every post. Download those for the 38 posts that were still pointing at the old artwork, point their `cover:` frontmatter at the new files, and drop the orphaned PNGs. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Light page matching the live /blog: "Blog" hero, the highlighted article (`highlight: true`, newest wins) on the left with the three most recent posts as a compact list on the right (a horizontal scroller below 992px, like the live `.c_blog_latest--cms_list`), then the "All articles" three-column grid of every post newest first (the live page renders all 69 with no pagination), the CTA band and the footer. Card copy is the post description; same-day posts tie-break the way the live site orders them. Page title/description come from the live meta tags. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
`src/layouts/blog.astro` now composes `MainLayout` (so it picks up the shared head/analytics) and matches the live template: light section with the title, author headshot/name/title, date and the CMS `readingTime` in an 8-column article, plus a sticky 4-column aside with "In this article" (`intro`), LinkedIn / X / copy-link share buttons and the "Subscribe to our newsletter" card, then the CTA band and footer. `seoTitle` / `seoDescription` / `ogImage` feed the meta tags with fallbacks to the title / description / `/blog/open-graph.png`. Reading time is no longer computed from the word count. Article prose is `@tailwindcss/typography`'s `prose` plus a new `src/styles/rich-text.css` (`.prose.rich-text`) that overrides headings, links, lists, blockquotes, inline code, tables, images, iframes and Shiki code blocks (`#2b2b2b` like Webflow's `.w-code-block`) with the theme tokens; embeds keep 16:9 and code scrolls horizontally on mobile. Also: `NewsletterSignupForm` gains a `layout="stacked"` variant (full width field + primary button) used by the new `newsletter-card.astro`; `BlogCallout` is restyled with tokens for the layouts that still use it; the RSS builder moves to `src/lib/blog-feed.ts`, uses the post intro and is served at both `/rss.xml` and `/blog/rss.xml` (the live footer link). Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Dark "Customers Stories" hero, the 3-up grid of every story newest first (covers at their natural aspect ratio, name, summary), then the CTA band and footer, matching the live /success-stories page. Extract the story card into src/components/success-stories/story-card.astro and a cover/sort helper in src/lib/success-stories.ts; the product pages' "Customers stories" grid now uses the same card. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Dark page: publish date, customer name, one-line summary and the cover above the story rich text (8 columns), with a sticky aside (4 columns) holding the "In this story" summary, the Industry / Company size / Website facts and the newsletter card, then the CTA band and footer. SEO title/description come from the CMS fields with fallbacks; the OG image is the story cover. The old "Highlights" block is gone from the layout (the live template has none) but stays in the schema. Also fix a stray bold marker in the imported Scapia story. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Header with the monthly/yearly toggle, the four plan cards, the trusted-by marquee and the plan comparison table (single-plan dropdown on mobile), then the CTA band. The live page has no calculator or FAQ, so those components are removed. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Hero with the Shorebird/Flutter logos, the "Founded in 2023" intro, the dark mission card, the team grid from team.json (drafts hidden, order matching the live CMS), the shared Resources section and the CTA band. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
The live page is the Gem job board in an 800px iframe under the hero spacer (the board renders the logo, intro and open positions itself), so embed `config.jobBoardUrl` the same way. Note the board's CSP only allows https ancestors, so the frame is blank on http://localhost but renders on the deployed site. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
- `/legal/dpa` moves to `/dpa` (redirect kept); the page links the newer DPA PDF from the live site (the 2024 template stays in public/legal so old links keep working). - `/terms/ci` and `/terms/code-push` are gone on the live site: redirect them to `/terms`, which now links the PDF terms like the live page. The Markdown sources move to `src/legal/` so `/terms/raw` keeps serving them. - `/privacy` copy matches the live text; headings become h2 like the live rich text and the H1/"last updated" line move to the hero. - `markdown.astro` composes the shared layout: dark hero with the title (and optional `subtitle`), rich text in the middle 8 columns, CTA band. - rich-text.css: nested lists use circle bullets like the live rich text. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Not a page on the live Webflow site, but the blog newsletter form links it (config.newsletterSubscriptionUrl); give it the legal-page hero and the stacked newsletter form in a card. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Stdlib-only crawler over the built site: reports any same-site href/src/ srcset/og:image that does not resolve to a file in dist/. Exposed as `npm run check:links` (run after `npm run build`). Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
The wordmark it references is served locally (public/shorebird-logo-wordmark.png). Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
The old dark cover had the same filename, so the cover swap skipped it. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
The reference export (CMS JSON, rendered pages, CSS, assets, screenshots) now lives in ../webflow-migration/ — 65MB of read-only material that should never ship with the site. scripts/import_webflow.py reads it from there (WEBFLOW_EXPORT overrides). The 69 per-post og:image files are JPEG q85 (6MB, was 42MB as PNG); JPEG on purpose because social crawlers don't reliably read WebP/AVIF for share images. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Same copy, bird, and layout as the live utility page. On phones the live page's float overflows the viewport; ours stacks instead. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
Webflow's 301 rules are not in any export we have and must be pulled by hand or via the v2 redirects API before DNS moves. Also records the trailing-slash decision (keep Astro's default) and the Cloudflare hosting assumptions. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
…robots.txt Verified via the Webflow API: 15 forms all posting to the same Loops endpoint our forms use, no email notifications, no webhooks, no registered scripts; site custom code is Plausible + Unify (head) and LinkedIn + HubSpot (footer). Live robots.txt only names the sitemap, so add one pointing at Astro's sitemap-index.xml. Claude-Session: https://claude.ai/code/session_012ffrD4Bm1fgzhYuSmUeAKz
team.json carried every Webflow "Teams" CMS entry including drafts that the live site never rendered. Keep only the published members, remove the draft flag and the unused headshots.
Own GCP project (shorebird-website) so the site's IAM is separate from production; apex via A records in the existing Cloud DNS zone, so nothing else on shorebird.dev moves. Cloudflare was considered and rejected: it needs the whole zone, and its one real advantage (free egress) is irrelevant at this site's traffic. - firebase.json / .firebaserc; trailingSlash, immutable cache on /_astro - src/integrations/firebase.ts copies astro.config.mjs redirects into firebase.json at build so they are real 301s; CI fails on drift - workflow: PR gate + preview channel per PR + deploy on main, WIF auth through the org's existing github-actions pool - drop public/CNAME (GitHub Pages) - CUTOVER.md / README updated
…tier The GCP project, Hosting site, deploy service account and a repo-scoped WIF pool now exist (CUTOVER.md §2). The org pool in code-push-dev is Tofu-managed and holds production repos, so the website gets its own. The integration formats firebase.json through Prettier so the build's regeneration and the format check agree.
|
🐦 Preview of f134350: https://shorebird-website--pr-411-8lydbxt5.web.app (expires in 7 days) |
- Drop the /workshops redirect (pointed at a Calendly for workshops we no longer run; already a 404 on live) and note that in the announcement post - Remove /demo (meta-refresh to a personal Calendly), /contact and /newsletter-signup: pre-Webflow pages nothing linked to - Remove the newsletter entirely (footer block on / and the guide page, blog and success-story sidebar card, form component, config URL) — it was taken off the live site on 2026-09-15. The Code Push guide form stays. - Fix dead links in posts: updater/issues/new (issues disabled there) → shorebird/issues; the RN-vs-Flutter post's /contact → /talk-to-sales - PORT_REPORT / CUTOVER updated to match
- Mobile menu drawer: drop the 80px top padding above the divider and
tighten item spacing (gap-8 -> gap-4, no extra gap under Products);
measured against live at 390px, every element now within 1px
- Navbar no longer swaps light/dark as sections scroll under it. Live
fixes the theme per page (light on blog pages, dark elsewhere), which
the existing initialTheme prop already expresses
- /product/shorebird-ci: live's notice is past tense now ("was
deprecated on August 8, 2026"); match it
Brings in the dependency bumps and CI fixes from #413. Conflict resolutions: - package.json: the port's dependency set (without @base-ui-components, @radix-ui/react-accordion, lucide-react and react-markdown, which it removed) at main's versions, including astro 7. package-lock.json regenerated from scratch; 0 audit vulnerabilities. - .github/workflows/main.yaml: keeps the Firebase build/preview/deploy jobs, takes main's actions@v7 and Node 24.x, and moves the deploy job's checkout to v7 with the rest. - README.md / .cspell.yaml: the port's versions, which already drop the archival notice and list `webflow`. README now says Astro 7. - src/pages/jobs.astro: the port's iframe embed replaces the markup main fixed for the astro 7 compiler, so that fix is moot here. - src/content/config.ts moves to src/content.config.ts (astro 7 dropped legacy content collections); the port's added schema fields carry over. - One blog post reformatted by prettier 3.9.6, which wraps `$1,000-2,000` style text differently. Verified: npm ci, allow-scripts, format:check, cspell, build, check:links and the firebase.json drift check all pass. Built output compared against the pre-merge branch: all 107 pages identical except smartypants punctuation on 7 blog pages (`--` now an en dash, one `'ol` apostrophe now curling the other way). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLGw8ogSPAXVnhTv2fbdee
The lockfile from the merge commit only listed linux optional packages, so `npm ci` on the macOS runner reported every darwin binary as missing from the lock file and refused to install. npm resolves the ideal tree from an existing node_modules when one is present, and then records only the optional binaries installable on the current platform. Regenerating with node_modules removed restores all 18 darwin and 18 win32 entries alongside the linux ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLGw8ogSPAXVnhTv2fbdee
|
Comments, corrections welcome @easymac |
easymac
left a comment
There was a problem hiding this comment.
Dug through the code and left some nits, mostly performance related and a couple of things to verify.
If we have the hero Lottie animation working, I think I'm satisfied saying this is plenty good enough to ship.
- `.well-known/microsoft-identity-association` was silently dropped from the deploy twice over: `firebase.json`'s `"**/.*"` ignore excluded it, and `actions/upload-artifact` skips hidden files by default. Un-ignore it, set `include-hidden-files: true`, and give it an explicit `Content-Type` (it has no extension, so Hosting would serve it as octet-stream). CUTOVER has a check to confirm it on the preview channel, and the alternative if nobody needs the Entra app any more. - Cache `/fonts/**` for a year as `immutable` instead of the default hour. They are preloaded on every page and the filenames are stable; README notes that a re-exported face has to be renamed rather than overwritten. - `/terms/raw` and `/privacy/raw` set `content-type: application/json` on a `Response` that a static build throws away. Rename both to `raw.json.ts` so the extension carries the type, and 301 the old paths there. - Add LinkedIn to the vendor list in the privacy policy; the Insight tag ships on every page but only Plausible and Posthog were listed. - Drop `role="menu"`/`role="menuitem"` from the Products dropdown: they promise arrow-key roving focus and typeahead we do not implement. It is a `<ul>` of links behind a button with `aria-expanded`, so Tab walks it like the rest of the nav. - Require first name and make last name optional in the guide form (Webflow had it the other way round, which is the wrong half to require). - Halve the Code Push hero Lottie's bitmap, 144kB -> 72kB, with a byte-identical render: every visible pixel is white and the artwork is entirely in the alpha channel, but the export left undefined RGB noise under the transparent pixels, which is incompressible. It is a halftone world map, not the soft ellipse the comment claimed, so it cannot become a CSS gradient and must not be downscaled further. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJaeks4xiq6ZvgZwjcQ2pf
|
Worked through @easymac's review in f134350. Details are on each thread; summary: Fixed
Not fixed, deliberately
Two things that need a human decision, both raised on their threads:
Generated by Claude Code |
Captures @eseidel's note on #411 that the terms need another pass with our lawyers, so it lives in the burn-down rather than in a review thread on a merged PR. Two known gaps, both pre-existing on the live Webflow site rather than introduced by the port: the Cookies section's claim that we use no advertising/analytics/tracking cookies sits badly next to the LinkedIn Insight tag, and the vendor list needs to match whatever we end up shipping. Also notes that the existing "Decide on HubSpot chat" item gates this one. The Astro site does not load HubSpot at all (deliberately, see PORT_REPORT.md §8), so listing it as a vendor before deciding to port it back would be wrong in the other direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJaeks4xiq6ZvgZwjcQ2pf
Ports shorebird.dev off Webflow into this repo (Astro 7 + Tailwind v4) and replaces the GitHub Pages deploy with Firebase Hosting in a dedicated GCP project.
PORT_REPORT.md— page-by-page status and accepted differences from WebflowCUTOVER.md— burn-down for leaving Webflow: redirects export, DNS, one-time GCP/WIF setup, cutover day.github/workflows/main.yaml— PR gate, preview channel per PR, deploy onmainDraft until the
shorebird-websiteGCP project, Hosting site, deploy service account and WIF binding exist (CUTOVER.md §2); thepreviewjob will fail at auth until then. Merging deploys nothing user-visible: DNS still points at Webflow until §4.