Why a vibe coded site crashes on a link click but not a refresh
I clicked the first link in my own nav and watched the site kill itself. White screen, one line in the console about a node that could not be removed because it no longer belonged to the node doing the removing. I hit refresh and everything came back perfect. Clicked the link again, dead again. Same build, same commit, and the site worked flawlessly right up until someone tried to move around inside it instead of just landing on it.
If your vibe coded site crashes when you click an internal link but a full refresh is clean, the cause is almost always that something moved a piece of the page out from under the framework, and the framework tried to clean up a node that had quietly changed parents. The single most useful thing I did was stop treating the crash as the bug and start treating the difference between the two navigations as the clue. A refresh throws the whole document away. A link click asks the framework to tear down the old page by hand, and that hand reached for a node that was no longer where it had filed it.
why does it crash on a link click but not on a refresh
A hard refresh and a soft navigation are two completely different teardown paths, and only one of them runs your framework's cleanup. When you refresh, the browser discards the entire document and starts over. Nothing in your app code runs to dismantle the old page, so nothing can trip over the old page's state. When you click an in-app link, there is no reload. The framework keeps the tab alive and does the teardown itself, walking its own record of what it rendered and calling removeChild on each node's parent to pull it out.
That second path is where a vibe coded site gets bitten, because the framework is removing nodes based on a map it drew at render time. If anything rearranged the real page after that map was drawn, the map is now a lie, and the teardown follows the lie straight into a crash the refresh path never even attempts.
what actually moved the node
The culprit was the scroll animation. To pin a section while the page scrolls past it, the animation library wraps that section in a spacer element and physically relocates your node inside the wrapper. It does this in the live DOM, after render, as a side effect. The framework never saw it happen. As far as the framework's virtual tree was concerned, my pinned section was still nested directly inside the container it originally rendered into.
So on a soft navigation the unmount walked its tree, found the pinned section, asked the original container to remove it, and the browser said no, that node lives inside a spacer now, it is not yours to remove. That is the exact meaning of "the node to be removed does not belong to this node." Nobody wrote a bug. Two layers each behaved correctly against a different picture of the same page, and the disagreement only became fatal on the one navigation path that makes the framework do the demolition itself.
why the first three fixes each missed
I did not see any of that at first, so I did what the error tempted me to do and treated it as a framework problem. First guess was routing, so I rewrote how links mounted and unmounted. The crash moved but stayed. Second guess was a missing key on a list nearby, because that is the folklore answer to anything reconciliation-shaped. No change. Third guess was strict mode double-invoking an effect, so I chased phantom double-mounts for an hour. Each fix was aimed at whatever looked broken last, and each one relocated the symptom without touching the cause, which is what debugging looks like when you are guessing instead of diagnosing.
The turn came from the clue I had ignored on the first click. Refresh clean, navigation fatal. That difference does not point at routing or keys or strict mode, all of which behave the same across both paths. It points at teardown, at something that only matters when the framework dismantles the page by hand. Once I named the real cause, an outside layer mutating the DOM after render, the fix stopped being a guess. Naming the cause before writing the fix is the whole discipline, and I had skipped it three times because the error message was pointing at the wrong layer and I let it.
the contained fix, and the real fix I scoped separately
The immediate fix was small once the cause had a name. Before the component unmounts, tear down the animation instances in the cleanup step, which makes the animation library put my node back where it found it and restore the original nesting. Now when the framework walks its tree to remove the section, the node is exactly where the map says it should be, and the teardown completes. One click, clean navigation, no white screen. I shipped that as the contained fix and confirmed it on the actual click path, not just on a reload.
I deliberately did not bundle the bigger change into that hotfix. The real fix is structural: wrap every animation setup in a scope that tracks everything it touches and reverts all of it automatically on cleanup, so no animated component can ever leave the DOM rearranged behind it. That is a refactor across every animated section, and stapling it onto an urgent one-line hotfix is how you turn a fix you can verify into a change you have to pray about. Contained fix now to stop the bleeding, structural fix scoped as its own work with its own verification. Isolating the two is the same reason fixing one bug does not have to break another: the smallest change that provably stops the crash is worth more than the elegant change that ships three risks at once.
This is a cousin of the mistake in deleting the duplicate your AI wrote. There the load-bearing thing looked redundant and got cut. Here the load-bearing assumption, that the DOM still matches what the framework rendered, got silently violated by a layer nobody was watching. Both crashes come from the same blind spot: the surface says one thing, the running system is doing another, and only measurement tells you which.
If you're a vibe coder scaling personal site work into client builds, and you keep hitting crashes that vanish on refresh and come back the moment someone actually navigates, and you want a sparring partner on the production jump, /work-with-us.
questions that keep coming up
Is this a bug in my framework or my animation library? Neither, and that is why it is so hard to search for. Both layers are behaving correctly against their own view of the page. The animation library really did move the node, and the framework really is removing nodes from where it last recorded them. The bug lives in the gap between the two, so the fix has to live there too: make the animation layer restore the DOM before the framework tears it down.
Why did it look completely fine while I was building it? Because you were reloading. Every time you save and the page refreshes, you take the hard-navigation path that never runs the framework's teardown, so you never hit the crash. Real visitors click through your site instead of reloading it, so they take the soft path you almost never exercise while vibe coding. That is the same perception gap behind the mobile win that broke scroll: the environment you build in flatters you.
How do I catch this before shipping? Click through your own site the way a visitor would, following internal links instead of refreshing between pages, and do it before you call anything done. A crash that only shows up on in-app navigation is invisible to the reload-heavy way we all build, and it will find your users on their first click even though it never found you on your hundredth save.
The site that felt finished was only ever finished for the one person who kept refreshing it. The product is whatever survives a stranger clicking through it, and the difference between the two lived in a teardown path I was not testing and a node I did not know had move
// part of the ai websites topic
// grab the free starter kit that makes your AI stop forgetting and stop guessing: get it →
// building with AI? the field manual has the structured lessons.
// hitting this on a real build? this is what I fix →