<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keymel Gaston</title>
    <description>The latest articles on DEV Community by Keymel Gaston (@keymelgaston).</description>
    <link>https://dev.to/keymelgaston</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4091311%2F202f74be-1cfe-4d87-9adf-146f414eb066.jpg</url>
      <title>DEV Community: Keymel Gaston</title>
      <link>https://dev.to/keymelgaston</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keymelgaston"/>
    <language>en</language>
    <item>
      <title>Missing RLS: The Most Underrated Breach Cause of 2026</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Sat, 12 Sep 2026 23:19:25 +0000</pubDate>
      <link>https://dev.to/keymelgaston/missing-rls-the-most-underrated-breach-cause-of-2026-1o4a</link>
      <guid>https://dev.to/keymelgaston/missing-rls-the-most-underrated-breach-cause-of-2026-1o4a</guid>
      <description>&lt;p&gt;You know why there are so many breaches in databases? Perfect, me neither. Let's find out in a few minutes.&lt;/p&gt;

&lt;p&gt;Everyone worries about encryption. Almost nobody worries enough about the one thing that's actually been breaking production databases all year: a table with Row Level Security simply left off.&lt;/p&gt;

&lt;p&gt;I ran into this while researching how to safely migrate Firestore Security Rules into Postgres RLS policies for a side project. What started as "how do I translate this correctly" turned into "oh, this is the single most common way people get breached right now." Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, with real numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🐞 CVE-2025-48757 (May 2025).&lt;/strong&gt; 303 endpoints across 170 applications built with Lovable, an AI app-building tool, had Supabase tables publicly readable because RLS was never enabled. Not misconfigured — never turned on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🕵️ March 2026.&lt;/strong&gt; A database belonging to an AI platform was exfiltrated after an attacker found an instance with, again, missing RLS. Admin emails and internal schema metadata were exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📡 Ongoing.&lt;/strong&gt; Security researchers have shown that thousands of Supabase instances can be queried with nothing more than a plain &lt;code&gt;curl&lt;/code&gt; request and the public "anon" key, dumping entire tables. Estimates put it at hundreds to thousands of misconfigured instances, globally, right now.&lt;/p&gt;

&lt;p&gt;Multiple 2025-2026 sources converge on the same line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data protection in Supabase is roughly 90% access control, 10% encryption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Encryption isn't the weak point. The access control layer is — specifically, the step where someone creates a new table and either forgets to turn RLS on, or writes a policy that's more permissive than they think it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a new mistake, just a new stage
&lt;/h2&gt;

&lt;p&gt;This isn't unique to Supabase or even to 2026. Firebase had its own version of the exact same failure: a documented, verified leak exposed plaintext passwords and sensitive data belonging to &lt;strong&gt;over 1.8 million users&lt;/strong&gt;, across more than &lt;strong&gt;900 mobile apps&lt;/strong&gt;, because Realtime Database instances were left publicly accessible — spanning health, finance, and education apps.&lt;/p&gt;

&lt;p&gt;Same root cause, different technology: a control-access layer that someone had to configure by hand, and didn't configure correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps happening
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's easy to get right once, and easy to forget under pressure.&lt;/strong&gt; Supabase actually improved here in 2026 — RLS is now enabled by default on new tables, and the dashboard flags tables that don't have it. That's a real fix for the "I forgot" case. But it doesn't help with the harder case: recreating access logic that used to live somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations are exactly when this goes wrong.&lt;/strong&gt; If you're moving from Firestore, your access control used to live in Security Rules — a completely different language, with a completely different mental model, often mixing "who can access this" with "does this data have the right shape" in the very same rule. Rebuilding that from scratch, by hand, under the time pressure of a migration, is precisely the moment this class of bug is most likely to slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And almost nobody automates that translation.&lt;/strong&gt; I looked. Guides for migrating off Firestore, even ones published this year, list "recreate your security rules as RLS policies" as a manual checklist item — nobody's shipping something that does it for you. &lt;code&gt;rebasepro/rebase&lt;/code&gt; sidesteps the whole problem by having you define policies once in its own DSL instead of translating what you already have. &lt;code&gt;supashim&lt;/code&gt; has rule translation listed as "in active development," with no public code yet.&lt;/p&gt;

&lt;p&gt;Part of why: Firestore rules genuinely mix two concerns that Postgres keeps separate — access control (RLS) and data shape validation (&lt;code&gt;CHECK&lt;/code&gt; constraints, column types). Translating well means untangling that mix first, not just mapping syntax A to syntax B. It's a semantic problem, not a find-and-replace one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually means if you're migrating
&lt;/h2&gt;

&lt;p&gt;If you're moving off Firestore (or any system where security rules were baked into the data layer), don't treat the security side as an afterthought you'll "get to after the data's in." The data being correct and the access control being correct are two separate risks, and the second one is the one with a CVE number attached to it right now, not a hypothetical.&lt;/p&gt;

&lt;p&gt;A few things that seem to actually help, based on what's out there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default to &lt;strong&gt;deny&lt;/strong&gt;, never to a guessed permissive policy, for any rule you can't confidently translate.&lt;/li&gt;
&lt;li&gt;Treat every generated policy as a &lt;strong&gt;draft that needs human review&lt;/strong&gt;, not something to apply automatically.&lt;/li&gt;
&lt;li&gt;If you're on Supabase specifically, lean on the fact that RLS is now on by default for new tables — but don't assume that saves you if your migration path creates tables outside the normal flow.&lt;/li&gt;
&lt;li&gt;Tools like &lt;code&gt;pgrls&lt;/code&gt; (a static RLS linter with dozens of rules for catching tenant-scoping bugs and inverted auth checks) are worth running against whatever you end up with, translated or hand-written.
## The uncomfortable part&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of the breaches above were exotic attacks. No zero-days, no clever exploits — just a &lt;code&gt;curl&lt;/code&gt; request against a table that should have had a policy on it and didn't. The failure mode is boring, which is exactly why it keeps happening: boring mistakes don't feel urgent enough to build tooling around, until they show up in a CVE.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you had to rebuild access control logic during a database migration? Curious whether you did it by hand, found a tool, or just... didn't think about it until later.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Built at 3am, still holding the morning coffee ☕&lt;/p&gt;

</description>
      <category>security</category>
      <category>postgres</category>
      <category>discuss</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Why Nobody Has Solved Firestore-to-Postgres Migration Well</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Sun, 06 Sep 2026 21:07:34 +0000</pubDate>
      <link>https://dev.to/keymelgaston/why-nobody-has-solved-firestore-to-postgres-migration-well-3nh4</link>
      <guid>https://dev.to/keymelgaston/why-nobody-has-solved-firestore-to-postgres-migration-well-3nh4</guid>
      <description>&lt;p&gt;Search for "Firestore to Postgres migration tool" and you'll find the same pattern over and over: a repo with good intentions, some initial traction, then silence. This isn't bad luck. It's a &lt;strong&gt;structural pattern&lt;/strong&gt;, and it's worth understanding why before anyone tries to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graveyard
&lt;/h2&gt;

&lt;p&gt;Let's start with the facts. Here are the real attempts that exist, or existed, to tackle this problem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Peak traction&lt;/th&gt;
&lt;th&gt;Status today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fireway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;282 ⭐ / 47 forks&lt;/td&gt;
&lt;td&gt;Archived by its own maintainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mad-migration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~30 ⭐&lt;/td&gt;
&lt;td&gt;Frozen since 2020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pydantic-firestore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 ⭐&lt;/td&gt;
&lt;td&gt;Confirmed inactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FireSync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;brand new&lt;/td&gt;
&lt;td&gt;v0.1.x, too early to tell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;fireway&lt;/code&gt; is the most illustrative case. A Firestore schema migration CLI, inspired by Flyway, with real traction — 282 stars and 47 forks don't happen by accident. Throughout 2022 and 2023, real users actively asked whether the project was still alive. Eventually, the maintainer himself answered honestly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This repo is unmaintained. I'll archive it for clarity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The community didn't fully give up — at least two forks emerged to keep it going — but the original project, the one with the most traction, was abandoned by its own creator.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mad-migration&lt;/code&gt; tried to solve migrations between different database types. Its last documented activity dates to 2020: issues asking for column indexing support when migrating to MongoDB, extra-table support via YAML, async drivers. None resolved since.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pydantic-firestore&lt;/code&gt; showed up more recently and is already confirmed as inactively maintained, with zero stars on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FireSync&lt;/code&gt; is the newest and most ambitious attempt — it brings a Terraform-style workflow (&lt;code&gt;pull → plan → apply&lt;/code&gt;) for managing indexes and TTL policies with version control. But it's still at version 0.1.x. Too early to know if it'll meet the same fate as its predecessors.&lt;/p&gt;

&lt;p&gt;None of these projects failed for lack of technical skill. &lt;code&gt;fireway&lt;/code&gt; solved the problem reasonably well for a lot of people. The underlying pattern is something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the pattern repeats
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🔁 It's a one-time event, not a recurring one.&lt;/strong&gt; A company migrates from Firestore to Postgres once in the life of its project. It doesn't need to do it again. That breaks the recurring-revenue model that sustains most software — and it breaks the maintainer's own motivation too: they wrote the tool because &lt;em&gt;they&lt;/em&gt; were migrating, solved their own problem, and never had a second case to keep polishing it for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔒 Nobody trusts production data to a black box.&lt;/strong&gt; One real company (Traba, an industrial staffing platform) publicly documented its own migration:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A full year of work. A team of roughly 12 engineers. Built entirely from scratch — trigger-based replication, per-collection reconciliation scripts, retry logic for referential integrity, a complete rollback plan.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They didn't use &lt;em&gt;any&lt;/em&gt; external tool. The risk of getting production data wrong was too high to trust to some unknown GitHub project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧠 The hardest step resists automation.&lt;/strong&gt; Deciding whether a nested field belongs in a separate table or stays as &lt;code&gt;jsonb&lt;/code&gt; isn't a purely technical call — it depends on how that data will be queried down the line, something only someone with business context can judge well. Supabase's own community discussion on this exact kind of migration explicitly recommends &lt;em&gt;against&lt;/em&gt; over-normalizing upfront. That's a judgment call, not an algorithm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 The market is fragmented by technology.&lt;/strong&gt; Firestore→Postgres, MongoDB→Postgres, DynamoDB→Postgres are, in practice, distinct problems with different SDKs and data models. None is big enough on its own to sustain a robust tool, while a generic "any NoSQL to any SQL" migrator is a far more ambitious project than any solo maintainer can sustain in their spare time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🏢 The platforms that own the data have no incentive to solve it.&lt;/strong&gt; Neither Firebase nor Supabase builds a complete migration tool — and that's not negligence, it's design. Firebase/Google is stable enough that most users never need to leave. Supabase invests in securing whoever &lt;em&gt;already arrived&lt;/em&gt; (RLS enabled by default on new tables, for instance) but not in translating what someone brings over from Firestore — that only benefits someone leaving the competitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💸 And the consultancies that solve it well have no incentive to automate themselves.&lt;/strong&gt; Certified AWS consultancies offer Firebase-to-Supabase migrations as a packaged service. Agencies specializing in MongoDB→Postgres report 30+ projects delivered, priced in the thousands of dollars each. They all sell engineer-hours doing the work by hand. Automating that process would cannibalize their own high-margin business.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern repeats beyond Firestore
&lt;/h2&gt;

&lt;p&gt;This isn't exclusive to Firestore. One engineer described helping &lt;strong&gt;34 different companies&lt;/strong&gt; migrate from MongoDB to PostgreSQL over 8 years, always describing the same sequence: initial excitement about being "web scale," then slow queries, then an unsustainable bill, then finally the migration. Documented cases include companies that spent &lt;strong&gt;$60,000&lt;/strong&gt; with full senior engineering teams. On the DynamoDB side, some companies migrated there to save money and ended up spending &lt;strong&gt;$200,000&lt;/strong&gt; learning architecture lessons before finding the right access pattern.&lt;/p&gt;

&lt;p&gt;The root cause is always the same: NoSQL databases are designed around the initial use case's queries, but the business changes over time, and the way you interact with the database becomes unmanageable. It's what happens when development speed is chosen over relational structure early on, and the business matures faster than the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed recently
&lt;/h2&gt;

&lt;p&gt;There's one piece that didn't exist just a couple of years ago, and it might explain why this problem could finally get a sustainable solution: &lt;strong&gt;reasonably good, cheap LLMs&lt;/strong&gt;. The most serious attempts at solving schema inference with AI help (instead of fixed rules or training a model from scratch) only started appearing in 2025-2026 — the first tools capable of reasoning with something resembling judgment about ambiguous field names, types, and implicit relationships, without needing a training dataset or years of hand-written rules.&lt;/p&gt;

&lt;p&gt;That alone doesn't solve the incentive problem — but it does solve the technical piece that was missing before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;This isn't a market gap nobody noticed. Talented people noticed it, more than once. The gap stays open because the combination of factors surrounding it — one-time event, no platform incentive, need for trust, need for human judgment, fragmented market — makes it genuinely hard to turn into a sustainable product, not just a script that works once.&lt;/p&gt;

&lt;p&gt;That doesn't mean it isn't worth trying. It means whoever tries should start knowing exactly why the previous attempts didn't survive, instead of finding out the hard way, a year in, with an archived repo and an "I don't have time to maintain this anymore" message.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you had to migrate from Firestore (or any NoSQL database) into a relational world? I'd genuinely like to hear how you handled it — by hand, with one of these tools, or by paying someone else to do it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Built in one afternoon, still holding the morning coffee ☕&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Astro 7.3's Smallest New Feature Might Be the Most Useful One for Testing</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Fri, 04 Sep 2026 21:34:38 +0000</pubDate>
      <link>https://dev.to/keymelgaston/astro-73s-smallest-new-feature-might-be-the-most-useful-one-for-testing-1hgm</link>
      <guid>https://dev.to/keymelgaston/astro-73s-smallest-new-feature-might-be-the-most-useful-one-for-testing-1hgm</guid>
      <description>&lt;p&gt;Astro 7.3 shipped a few days ago. Most of the attention is probably going to the bigger stuff, but there's a one-line change in the CLI that I think is more useful day-to-day than it looks: &lt;code&gt;astro preview&lt;/code&gt; finally got an &lt;code&gt;--ignore-lock&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some background first
&lt;/h2&gt;

&lt;p&gt;Astro 7.0 introduced a lockfile for &lt;code&gt;astro dev&lt;/code&gt; — every time you start a dev server, Astro writes a small file recording its URL, port, and PID, specifically so AI coding agents don't accidentally spin up duplicate dev servers for the same project while you're already running one.&lt;/p&gt;

&lt;p&gt;Reasonable problem to solve. But it had a side effect: if &lt;em&gt;you&lt;/em&gt; wanted to run a second dev server on purpose — say, a throwaway instance with verbose logging while your main one keeps running — Astro would just error out. The lockfile didn't know the difference between "an agent accidentally duplicating" and "a human doing this deliberately."&lt;/p&gt;

&lt;p&gt;Astro 7.1 fixed that for &lt;code&gt;astro dev&lt;/code&gt; with &lt;code&gt;--ignore-lock&lt;/code&gt;, which skips the lockfile check entirely so a second instance can run alongside the first. Astro 7.2 then added the same lockfile protection to &lt;code&gt;astro preview&lt;/code&gt; — but forgot the escape hatch. So you'd hit the exact same wall, just one command later.&lt;/p&gt;

&lt;p&gt;Astro 7.3 closes that gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;astro preview &lt;span class="nt"&gt;--ignore-lock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this specifically matters for testing
&lt;/h2&gt;

&lt;p&gt;The Astro team calls out the actual use case in their own release notes: &lt;strong&gt;Playwright and other E2E testing workflows that need multiple preview servers running at once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've set up &lt;code&gt;playwright.config.ts&lt;/code&gt; with a &lt;code&gt;webServer&lt;/code&gt; block (the way most Astro + Playwright setups do it), the test runner spins up its own preview server automatically. If you're also running your own preview server locally — to manually poke at the same build, or running two suites against two different ports — you'd hit the lock before 7.3, full stop.&lt;/p&gt;

&lt;p&gt;I just went through setting up a Playwright suite for a recent project, and this is exactly the kind of small friction that doesn't show up until you're mid-debug and confused about why your test runner won't start. Nice to see it closed less than a year after the original lockfile shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small feature, but worth knowing
&lt;/h2&gt;

&lt;p&gt;Nothing here changes how you build a site. It's the kind of change that's easy to skim past in a changelog and then rediscover the hard way three months later when you actually need it. If you're running Playwright against an Astro project, it's worth having this in the back of your mind before it becomes a "why is this broken" moment instead of a one-line fix.&lt;/p&gt;

&lt;p&gt;Written at 3am, fueled by coffee and questionable life choices ☕&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Went Looking for Why Lenis Beat Locomotive Scroll. The Real Story Surprised Me.</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:58:54 +0000</pubDate>
      <link>https://dev.to/keymelgaston/i-went-looking-for-why-lenis-beat-locomotive-scroll-the-real-story-surprised-me-4ed3</link>
      <guid>https://dev.to/keymelgaston/i-went-looking-for-why-lenis-beat-locomotive-scroll-the-real-story-surprised-me-4ed3</guid>
      <description>&lt;p&gt;I use Lenis for smooth scroll in most of my projects, and I'd always assumed it just won a popularity contest against Locomotive Scroll — more stars, more downloads, moved on. So I went looking for the actual reason. What I found was more interesting than "it's more popular," and it ends with a twist I didn't expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 1: The problem nobody was talking about
&lt;/h2&gt;

&lt;p&gt;For years, most smooth-scroll libraries — including older versions of Locomotive Scroll — used what's basically a clever hack: fix a wrapper element, then apply a CSS &lt;code&gt;transform&lt;/code&gt; to it on every frame, based on an eased scroll value. It looks smooth. It works.&lt;/p&gt;

&lt;p&gt;It also means the page never actually scrolls. The browser's real scroll position stays put; you're just visually translating content around.&lt;/p&gt;

&lt;p&gt;That hack quietly breaks a handful of things developers don't notice until they ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;position: sticky&lt;/code&gt; — stops working correctly, because the element isn't really moving through a real scroll container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scroll-snap&lt;/code&gt; — same problem&lt;/li&gt;
&lt;li&gt;Native browser search (Ctrl+F) and anchor links — can behave unpredictably when the "scroll" is fake&lt;/li&gt;
&lt;li&gt;Screen readers and accessibility tooling — lose meaningful context&lt;/li&gt;
&lt;li&gt;GPU load — large, repeated transforms aren't free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lenis took a different approach on purpose: keep the browser's real, native scroll active, and layer interpolation &lt;em&gt;on top of it&lt;/em&gt; — actual &lt;code&gt;scrollTo&lt;/code&gt; calls, eased, not a transform illusion. From Lenis's own site:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What began as an internal tool for syncing WebGL and the DOM is now the default smooth scroll across the industry — even powering libraries like Locomotive Scroll. The smoothness was a happy accident."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the origin story, by the way — Lenis wasn't built to "win" the smooth-scroll library wars. It was an internal tool at a WebGL agency (darkroom.engineering) for syncing DOM scroll with WebGL canvases, and the smooth-scroll behavior came along for the ride.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 2: The twist
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised me. I went looking for a "Locomotive Scroll vs Lenis, which is better" comparison. What I found instead: &lt;strong&gt;Locomotive Scroll v5 is built on top of Lenis.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "inspired by." Not "similar architecture." Built on it. Their own release notes say it directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Version 5 is a complete rewrite of Locomotive Scroll, now built on top of Lenis."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And their docs summarize their own history bluntly: &lt;em&gt;"This library has evolved considerably over the years. From jQuery to vanilla ES6, from custom engines to Lenis foundation."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The reasons they list for the rewrite read like a direct admission of the exact problems the old transform-hack approach caused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;"No more greedy CSS transforms breaking your layouts"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Works perfectly with position: sticky — No conflicts, no workarounds"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bundle size dropped from ~12.1kB to 9.4kB gzipped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't really a story about one library beating another. It's a story about the ecosystem converging on a better-solved primitive. Locomotive didn't lose — they made the pragmatic call to stop maintaining their own lower-level scroll engine (which had real, filed bugs — GitHub issues #519 and #532, among others) and build their higher-level features (parallax, intersection detection) on top of infrastructure the community had already gotten right.&lt;/p&gt;

&lt;p&gt;For context on scale: Lenis sits at roughly 14k GitHub stars with 35 contributors, npm downloads growing from ~93k/month to ~343k/month over the past year. Locomotive Scroll has ~8.6k stars, 25 contributors. Not a landslide — but a clear enough gap that "we'll build on the leader" made sense for Locomotive's team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act 3: The thing that might make this whole comparison irrelevant
&lt;/h2&gt;

&lt;p&gt;While Lenis and GSAP's ScrollTrigger were winning their category, the browser itself started doing some of this natively. CSS now has &lt;code&gt;animation-timeline: scroll()&lt;/code&gt; and &lt;code&gt;animation-timeline: view()&lt;/code&gt; — scroll-driven animations with zero JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome's had full support since 2023. Safari joined in September 2025. And this is real enough that a design agency wrote publicly: &lt;em&gt;"We've removed framer-motion from three client sites this year because of them [native scroll-driven animations]."&lt;/em&gt; That's not a hypothetical threat — that's a JS animation library actually getting removed from production sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But&lt;/strong&gt; — and this is the part that matters — it's not a clean "native CSS wins" story either. The nuance, well put by one source I found:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use native CSS when the animation is linear, depends on scroll or viewport position, and doesn't require programmatic timeline control (play, pause, reverse, scrub) or complex sequencing. GSAP remains necessary for advanced animation sequencing, reverse timelines with speed control, and custom physics."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And specifically for Lenis: native scroll-driven animations sync an animation's &lt;em&gt;progress&lt;/em&gt; to scroll position. They don't smooth the scrolling motion itself. That's a different job — the one Lenis actually does. Native CSS doesn't touch it.&lt;/p&gt;

&lt;p&gt;There's also a real reason this took Firefox until 2026 to catch up (still behind a flag in stable, as of writing). I checked Mozilla's own bug tracker — their official standards position is supportive, this isn't a "we disagree with the spec" situation. It's scale: the meta-bug tracking implementation has 30+ dependent sub-bugs, including real correctness issues (one engineer explicitly blocked shipping on a bug involving scroll animations breaking a common CSS custom-property technique) that Chrome's larger engineering team could parallelize through faster. A frustrated developer's post on Mozilla Connect noted a related, older bug has been open for almost a decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves things
&lt;/h2&gt;

&lt;p&gt;Lenis didn't win by being more popular — it won by fixing a real architectural problem (fake scroll breaking accessibility and native browser features) that its "competition" eventually admitted to having too, and built on top of instead of continuing to fight. Meanwhile, native CSS is genuinely eating the simple end of scroll animation — the reveals, the parallax, the progress bars — while leaving the complex, sequenced, physics-driven work (and the actual job of smoothing scroll itself) untouched.&lt;/p&gt;

&lt;p&gt;It's not a popularity contest and it's not "native CSS will kill JS libraries" either. It's two different tools settling into the jobs they're actually good at.&lt;/p&gt;




&lt;p&gt;I write about this stack (Astro + GSAP + Lenis) semi-regularly — my &lt;a href="https://dev.to/keymelgaston/can-you-type-named-slots-in-astro-short-answer-not-yet-4707"&gt;last post covered why Astro can't type named slots yet&lt;/a&gt;, and the one before that covered &lt;a href="https://dev.to/keymelgaston/4-motion-design-bugs-that-break-astro-gsap-lenis-sites-in-production-31ac"&gt;4 production bugs common to this exact combo&lt;/a&gt;. If you've got a scroll-animation war story of your own, I'd genuinely like to hear it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>javascript</category>
      <category>webperf</category>
    </item>
    <item>
      <title>Can You Type Named Slots in Astro? (Short Answer: Not Yet)</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Fri, 28 Aug 2026 20:54:36 +0000</pubDate>
      <link>https://dev.to/keymelgaston/can-you-type-named-slots-in-astro-short-answer-not-yet-4707</link>
      <guid>https://dev.to/keymelgaston/can-you-type-named-slots-in-astro-short-answer-not-yet-4707</guid>
      <description>&lt;p&gt;Someone asked me this in the Astro Discord recently, and I went looking for a real answer instead of guessing. Sharing it here because it's a question that comes up a lot, and the honest answer isn't what most people expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;You can type a component's props in Astro easily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your editor gives you autocomplete and errors if you get it wrong. Great.&lt;/p&gt;

&lt;p&gt;But what about named slots?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
type Props = {
  children: string;
}
---
&amp;lt;slot /&amp;gt;
&amp;lt;slot name="foo" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can you type the &lt;code&gt;foo&lt;/code&gt; slot the same way — so TypeScript warns you if a consumer misspells the slot name, or passes the wrong kind of content?&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;You can't. Not properly, at least not yet.&lt;/p&gt;

&lt;p&gt;Astro's &lt;code&gt;Props&lt;/code&gt; typing only covers the &lt;em&gt;default&lt;/em&gt; slot, via &lt;code&gt;children&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no equivalent for named slots. This isn't something hidden in the docs that you missed — it's a real, acknowledged gap in the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just me saying that
&lt;/h2&gt;

&lt;p&gt;There's a &lt;a href="https://github.com/withastro/roadmap/discussions/414" rel="noopener noreferrer"&gt;GitHub RFC discussion&lt;/a&gt; proposing typed slots, going back to December 2022. An Astro core maintainer replied directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Typed slots is already something we're planning, though it's currently not in our roadmap. The tricky part of this is mostly the implementation, TSX doesn't have proper tools for this and our current slot implementation is challenging to support, we're still investigating the way to go for that."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's from 2022. As of writing this, it's still not implemented. The RFC author followed up in 2024 saying the problem still needs addressing — no resolution since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's actually hard, not just neglected
&lt;/h2&gt;

&lt;p&gt;Props are easy to type because they're a flat object — &lt;code&gt;{ name: string, age: number }&lt;/code&gt;. TypeScript has handled that shape forever.&lt;/p&gt;

&lt;p&gt;Slots aren't data, though — they're markup being injected into a specific spot in a component's template. That's a fundamentally different kind of thing to describe than "this value should be a string." TSX itself doesn't have great tooling for this, and Astro's internal slot implementation adds its own layer of complexity on top. It's less "nobody got around to it" and more "the tools to do it properly don't fully exist yet."&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can actually do today
&lt;/h2&gt;

&lt;p&gt;Two practical options, neither of them compile-time type safety, but both better than nothing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Document it in a comment.&lt;/strong&gt; Not enforced, but at least visible to whoever uses your component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// Slots: default, "header", "footer"
type Props = { children: any };
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Runtime checks with &lt;code&gt;Astro.slots.has()&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
const hasFooter = Astro.slots.has("footer");
---
{hasFooter &amp;amp;&amp;amp; &amp;lt;slot name="footer" /&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches missing slots at runtime, not compile time — but if a slot is required for your component to render correctly, at least you can branch on whether it was passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson here
&lt;/h2&gt;

&lt;p&gt;Not every "how do I do X" question has an answer. Sometimes the honest answer is "you can't, here's why, here's the closest workaround" — and that's more useful than a fake solution that falls apart the moment someone tries it. If you're ever unsure whether a limitation is you missing something or a real framework gap, checking the RFC discussions and issue trackers directly is worth the extra five minutes.&lt;/p&gt;




&lt;p&gt;If you're building with Astro + GSAP + Lenis specifically, I write about that stack a bit — &lt;a href="https://dev.to/keymelgaston/4-motion-design-bugs-that-break-astro-gsap-lenis-sites-in-production-31ac"&gt;my last post covered 4 common bugs in that combo&lt;/a&gt;. Happy to talk through Astro TypeScript quirks in the comments if you've hit others like this one.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>4 Motion Design Bugs That Break Astro + GSAP + Lenis Sites in Production</title>
      <dc:creator>Keymel Gaston</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:04:20 +0000</pubDate>
      <link>https://dev.to/keymelgaston/4-motion-design-bugs-that-break-astro-gsap-lenis-sites-in-production-31ac</link>
      <guid>https://dev.to/keymelgaston/4-motion-design-bugs-that-break-astro-gsap-lenis-sites-in-production-31ac</guid>
      <description>&lt;p&gt;I got tired of debugging the same four things every time I built a motion-design landing page with Astro, GSAP, and Lenis — so I wrote them down. Here's each bug, what causes it, and how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scroll-triggered animations that never fire
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How to spot it:&lt;/strong&gt; you scroll to a section, and the animation that's supposed to play doesn't. No console error — the element just sits in its "before" state forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Lenis replaces native scrolling with a virtual, smoothed one. GSAP's ScrollTrigger has no built-in awareness that Lenis exists — it's still listening for native scroll behavior Lenis has effectively taken over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;lenis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lenis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lagSmoothing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this once, at setup. Do it per-section instead and you'll eventually forget one — which is exactly how this bug ships to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Images that fade in as blank boxes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How to spot it:&lt;/strong&gt; a smooth, well-eased fade-in animation... revealing nothing. The image pops in abruptly partway through, or right after the animation finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; the animation triggers on &lt;code&gt;DOMContentLoaded&lt;/code&gt;, component mount, or scroll-into-view — none of which guarantee the image has actually finished downloading and decoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;?.().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Await this before starting the fade. One helper function, one fewer confusing bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Buttons that stop responding for no visible reason
&lt;/h2&gt;

&lt;p&gt;Two separate causes here — worth checking both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3a — a decorative layer is eating the click.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An animated background shape or overlay ends up in a higher stacking context than an interactive element near it. The button looks normal. It just doesn't respond.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- z-index left to chance, decided per component
&lt;/span&gt;&lt;span class="gi"&gt;+ pointer-events: none on every purely decorative element, always
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3b — a React/Vue island hasn't hydrated yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With lazy hydration (&lt;code&gt;client:visible&lt;/code&gt;, &lt;code&gt;client:idle&lt;/code&gt;), there's a real window where an island's HTML is on the page but its listeners haven't attached. A click in that window isn't delayed — it's lost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hydrated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHydrated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setHydrated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hydrated&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hydrated&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Click me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't just hope the click lands after hydration — disable the element until hydration is confirmed. This race can pass casual manual testing and still fail reliably under real load — it's the kind of thing that's easy to miss until you have automated tests hitting it under real concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. SEO and Lighthouse scores that don't hold up
&lt;/h2&gt;

&lt;p&gt;The most common individual causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;og:image&lt;/code&gt; set to a relative URL — silently fails the Open Graph spec, which requires an absolute URL&lt;/li&gt;
&lt;li&gt;No base URL configured, so canonical links can't resolve&lt;/li&gt;
&lt;li&gt;Missing alt text, skipped heading levels, no sitemap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; centralize your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; in one layout every page uses, resolve image and canonical URLs to absolute paths explicitly, and generate a sitemap.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest note on Lighthouse scores
&lt;/h2&gt;

&lt;p&gt;If you're running Lighthouse on a site with real scroll-driven animation and you're not hitting a perfect 100 on Performance — that's expected. A 100 with heavy motion actually running is rare; it usually means the animation isn't doing much. A defensible 80s–90s score with genuine motion design running is more honest than a suspiciously perfect one.&lt;/p&gt;




&lt;p&gt;I put all four of these — tested, with automated regression tests, demonstrated live rather than just described — into a full Astro + GSAP + Lenis landing page template. &lt;a href="https://landing-premium-kit.vercel.app" rel="noopener noreferrer"&gt;The live demo is here&lt;/a&gt; if you want to see them in action, and the &lt;a href="https://keymelgaston.gumroad.com/l/inkukz" rel="noopener noreferrer"&gt;kit itself is on Gumroad&lt;/a&gt; if you'd rather start from a base that already has this solved.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about any of these in the comments — especially the hydration race in #3, that one surprised me too.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>css</category>
    </item>
  </channel>
</rss>
