Priority: Medium Date: 2025-09-30 Affects: About page counter animations
Counter animations on the About page work perfectly in local development but fail completely in production (Netlify deployment).
- ✅ Works: Local development (
pnpm dev) - ❌ Fails: Production build on Netlify
- ❌ Fails: Even in incognito windows (not a caching issue)
- The counters remain at "0" and never animate to their target values
Counters should animate from 0 to their target values:
- Drawer Types: 0 → 4
- Cross-Device Support: 0 → 6
- Calculated Precision: 0 → 99.9
- File:
/src/components/CounterComponent.astro - Used in:
/src/content/about/index.mdx - Rendered via:
/src/pages/about.astro
The alfadev-astro-starter template uses:
<script is:inline>
document.addEventListener("astro:page-load", () => {
const counters = document.querySelectorAll(".count");
const speed = 500;
counters.forEach((counter) => {
const animate = () => {
const value = +counter.getAttribute("aria-valuenow");
const data = +counter.innerText;
const time = value / speed;
if (data < value) {
counter.innerText = Math.ceil(data + time);
setTimeout(animate, 1);
} else {
counter.innerText = value;
}
};
animate();
});
});
</script>-
astro:after-swap event (like dark mode fix)
- Added
astro:after-swaplistener - Result: No change
- Added
-
Immediate execution (like theme toggle)
- Removed event listeners, ran
initCounters()immediately - Result: No change
- Removed event listeners, ran
-
Named function approach
- Extracted logic into
initCounters()function - Result: No change
- Extracted logic into
-
DOMContentLoaded check
- Added
document.readyStatecheck - Added
DOMContentLoadedevent listener - Result: No change
- Added
-
Removed is:inline
- Let Astro bundle the script
- Result: TypeScript errors, had to revert
-
Hybrid approach
- Combined DOMContentLoaded + astro:after-swap + immediate execution
- Result: No change
<script is:inline>
const initCounters = () => {
const counters = document.querySelectorAll(".count");
if (counters.length === 0) return;
const speed = 500;
counters.forEach((counter) => {
const animate = () => {
const value = +counter.getAttribute("aria-valuenow");
const data = +counter.innerText;
const time = value / speed;
if (data < value) {
counter.innerText = Math.ceil(data + time);
setTimeout(animate, 1);
} else {
counter.innerText = value;
}
};
animate();
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCounters);
} else {
initCounters();
}
document.addEventListener("astro:after-swap", initCounters);
</script>- Works locally, fails in production - Classic timing/bundling issue
- Dark mode toggle works - Uses similar pattern but in
Base.astrohead - Theme script location - In
<head>, runs before body - Counter script location - In component body, after elements
- MDX rendering - Component is used inside MDX content
-
Script bundling differences
- Production minification might affect script execution
- Astro view transitions might handle inline scripts differently in production
-
MDX component rendering timing
- Script might run before MDX content is fully rendered
- Component scripts in MDX might have different lifecycle
-
Astro view transitions
- Production build might handle view transitions differently
- Script might not re-execute on client-side navigation
-
Script placement
- Theme script is in
<head>, counter script is in component - Might need to move script to a different location
- Theme script is in
-
is:inline directive
- Might be causing issues with Astro's script processing
- But removing it causes TypeScript errors
-
Move script to Base.astro
- Put counter initialization in
<head>like theme script - Use global function that checks for counter elements
- Put counter initialization in
-
Use client:load directive
- Convert to a React/Preact component with
client:load - Ensures proper hydration timing
- Convert to a React/Preact component with
-
Add console.log debugging
- Deploy with console logs to see what's happening in production
- Check if script runs, if elements exist, if animation starts
-
Check production HTML
- Download production HTML and compare with local
- See if script is being transformed/minified incorrectly
-
Try different event
- Use
window.onloadinstead of DOMContentLoaded - Try
requestAnimationFramefor timing
- Use
-
Intersection Observer
- Trigger animation when counters come into viewport
- More reliable for dynamically rendered content
Location: /src/layouts/Base.astro lines 271-298
<script is:inline>
const setTheme = () => { /* ... */ };
setTheme();
document.addEventListener("astro:after-swap", setTheme);
</script>Location: /src/layouts/partials/Header.astro lines 121-144
<script is:inline>
function initDropdownListeners() { /* ... */ }
document.addEventListener("astro:page-load", () => {
initDropdownListeners();
});
</script>37457a8- Initial fix attempt with astro:after-swap2b4ba18- Immediate execution approachfb51ee7- DOM ready handling651af4e- Demo links feature (unrelated but in same session)5fbbe09- Named function approach831f40f- Theme script patternf5cb546- DOMContentLoaded check (current)
- Original template: https://github.com/diego-alfadev/alfadev-astro-starter
- Dark mode fix memory: fbaadf34-02cd-4e6c-bb72-b14571b62b32
- Astro view transitions docs: https://docs.astro.build/en/guides/view-transitions/