feat: scroll-to-top button in estore; fix JS/CSS cache headers on main site

- Add ScrollToTop component matching main site's green Top button
  (appears after 130px scroll, same styling and font)
- Fix main-site server.js: JS/CSS now use max-age=3600 + must-revalidate
  instead of 30d immutable — changes reach users within 1 hour instead
  of being stuck in browser cache for a month
- Images/fonts keep 30d immutable (safe, as they are content-addressed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-15 14:41:42 -04:00
parent c6d5a0265f
commit 0576677523
3 changed files with 53 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import Footer from '@/components/Footer'
import CartDrawer from '@/components/CartDrawer'
import CartFab from '@/components/CartFab'
import { CartProvider } from '@/context/CartContext'
import ScrollToTop from '@/components/ScrollToTop'
export const metadata: Metadata = {
title: {
@ -51,6 +52,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<CartFab />
<main>{children}</main>
<Footer />
<ScrollToTop />
</CartProvider>
</body>
</html>

View File

@ -0,0 +1,45 @@
'use client'
import { useEffect, useState } from 'react'
export default function ScrollToTop() {
const [visible, setVisible] = useState(false)
useEffect(() => {
const onScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
setVisible(scrollTop > 130)
}
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
if (!visible) return null
return (
<button
aria-label="Back to top"
onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })}
style={{
position: 'fixed',
bottom: '12px',
right: '10px',
zIndex: 99,
border: '1px solid #363636',
outline: 'none',
background: '#94d601',
cursor: 'pointer',
padding: '15px',
borderRadius: '10px',
fontSize: '18px',
boxShadow: '3px 3px 3px #363636',
fontFamily: '"Autour One", serif',
lineHeight: 1,
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = '#aedad3' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = '#94d601' }}
>
Top
</button>
)
}

View File

@ -64,8 +64,13 @@ const staticCacheOptions = {
maxAge: process.env.NODE_ENV === 'production' ? '30d' : 0,
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html') || filePath.endsWith('update.json')) {
// Never cache HTML or live data
res.setHeader('Cache-Control', 'no-store');
} else if (/\.(js|css|svg|ico|png|jpg|jpeg|webp|avif|woff2?)$/i.test(filePath)) {
} else if (/\.(js|css)$/i.test(filePath)) {
// JS/CSS: 1 hour, must revalidate — allows updates to reach users quickly
res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate');
} else if (/\.(png|jpg|jpeg|webp|avif|svg|ico|woff2?)$/i.test(filePath)) {
// Images/fonts: 30 days immutable (these are named by content, rarely change)
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
}
}