Skip to content

Commit a7c92a8

Browse files
authored
Merge pull request #666 from webstackdev/bugfix/theming-issues
Fix focus behavior on cookie modal
2 parents b5a5a09 + 5fa0208 commit a7c92a8

5 files changed

Lines changed: 325 additions & 30 deletions

File tree

_TODO.md

Lines changed: 270 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,283 @@ https://aws.plainenglish.io/how-to-build-a-chatbot-using-aws-lex-and-lambda-in-2
4343

4444
## Performance Issues
4545

46-
3. Audit the homepage hydration/chunk fan-out after prerendering. The 22 JS chunks suggest too much client code is shipping for a marketing landing page.
46+
### Home page size
4747

48-
### Cache Header
48+
- Audit the homepage hydration/chunk fan-out after prerendering. The 22 JS chunks suggest too much client code is shipping for a marketing landing page.
4949

50-
I addressed it by adding an explicit Vercel header rule in vercel.json for /_astro/(.*) with:
50+
No edits made. This is an audit of the current built homepage.
5151

52-
Cache-Control: public, max-age=31536000, immutable
53-
CDN-Cache-Control: public, max-age=31536000, immutable
54-
Vercel-CDN-Cache-Control: public, max-age=31536000, immutable
55-
That is the right fix surface for this issue, because the problem appears to be deployment/header behavior, not the asset build itself.
52+
The prerendered homepage still ships 22 direct module scripts from index.html:99, index.html:181, index.html:193, and index.html:230. The direct script total is 286,449 bytes before any imported shared chunks. That means the real network cost is higher, because these entries pull shared runtime pieces like Embla, Lit helpers, custom-element helpers, and GSAP.
5653

57-
One important detail from validation: the generated Astro build output in config.json:19 still shows Astro's own _astro header route after handle: filesystem, which is exactly why I didn't rely on that generated rule. The explicit vercel.json config is meant to be applied by Vercel at deploy time, outside that Astro route ordering.
54+
Roughly 165 KB of the direct JS is shell cost, not homepage content. The main driver is the always-on bootstrap from index.astro:64, which becomes the 101,517-byte top chunk and initializes consent, animation lifecycle, header search, and layout side effects from the bootstrap module. On top of that, the layout always includes BaseLayout.astro:134, BaseLayout.astro:144, BaseLayout.astro:147, BaseLayout.astro:190, and BaseLayout.astro:191. Header search and nav add more fixed cost through index.astro:62, index.astro:70, index.astro:151, and index.astro:179. The marketing homepage is paying for a full app shell.
5855

59-
So:
56+
The biggest homepage-specific cost is the Terraform hero animation, not the tiny hero text effect. The 80,510-byte entry is emitted before the hero markup in index.html:185, and the animation is registered from index.astro:605 with a GSAP-heavy client in index.ts:149. The actual hero custom element registration is separate and small in index.astro:95.
6057

61-
Code/config change: done
62-
Local build: passed
63-
Local proof of final deployed header behavior: not possible from astro build output alone
64-
Real verification step: after deploy, run curl -I https://www.webstackbuilders.com/_astro/<some-built-asset>.js
65-
What you should expect after deploy:
58+
The newsletter block fans out into three separate direct entries. The form itself registers in index.astro:83, while the nested form content also pulls index.astro:30 and index.astro:93. You can see the three adjacent newsletter-related script entries in index.html:193 and index.html:212. Combined direct cost there is about 28 KB before shared imports.
6659

67-
Cache-Control: public, max-age=31536000, immutable
68-
x-vercel-cache behavior should improve for those assets
69-
Vercel-CDN-Cache-Control may be consumed by Vercel and not echoed back
70-
If you want, once this deploy finishes I can verify a live _astro asset header and confirm whether this actually fixed production caching.
60+
The carousel family is the next meaningful page-local contributor. Skills, latest insights, and testimonials each register separate custom elements in index.astro:125, index.astro:279, and index.astro:186. Their direct entry files are not individually huge, but they multiply the request graph and pull shared Embla infrastructure.
61+
62+
The current use of whenIdle is not reducing chunk fan-out. It delays registration work, but it does not stop Astro from emitting direct module script tags into the prerendered HTML. That is visible for index.astro:88, index.astro:605, index.astro:279, and index.astro:186, and those scripts still show up in index.html:181 and index.html:193. So it helps execution timing, not shipped JS count.
63+
64+
The highest-leverage reductions are clear. First, remove or aggressively defer the Terraform animation. Second, split the marketing homepage off the full app shell so it does not inherit ClientRouter, search, theme picker, bug reporter, and other global interactive infrastructure by default. Third, flatten the newsletter block into a simpler home-page variant without confetti and custom checkbox logic unless the user actually interacts.
65+
66+
1. I can turn this into a concrete implementation plan with estimated byte savings per change.
67+
68+
2. I can make the highest-value first cut now: remove or defer the Terraform animation and rebuild to remeasure.
69+
70+
3. I can create a lighter marketing layout for the homepage and remeasure the shell cost.
71+
72+
I would not start from PageLayout.astro:1, because it still inherits BaseLayout.astro:1. The clean version is a new marketing-only layout that sits much closer to PrintLayout.astro:1: same CSS pipeline and meta surface, but without the app shell.
73+
74+
What that would look like in practice:
75+
76+
- Keep the global CSS imports and the document skeleton from PrintLayout.astro:1.
77+
- Keep the meta tags from index.astro:1, but split them so the homepage does not automatically pull index.astro:64.
78+
- Drop BaseLayout.astro:134, BaseLayout.astro:144, BaseLayout.astro:147, BaseLayout.astro:194, BaseLayout.astro:190, BaseLayout.astro:191, and BaseLayout.astro:192 for the first pass.
79+
- Replace the current interactive header and footer with static marketing variants, because the current header/footer bring search, theme toggle, footer custom-element registration, and bug reporter behavior through index.astro:1 and index.astro:1.
80+
- Leave the homepage content in index.astro:1 unchanged at first, so the measurement isolates layout-shell savings before you touch hero, carousels, or newsletter behavior.
81+
82+
Conceptually, it would be closer to this:
83+
84+
```markdown
85+
---
86+
import '@styles/index.css'
87+
import '@styles/icons.css'
88+
import Meta from '@components/Head/Meta.astro'
89+
import ThemeInit from '@components/Head/ThemeInit.astro'
90+
import MarketingHeader from '@components/Marketing/Header.astro'
91+
import MarketingFooter from '@components/Marketing/Footer.astro'
92+
---
93+
94+
<!doctype html>
95+
<html lang="en" data-theme="light">
96+
<head>
97+
<meta charset="utf-8" />
98+
<ThemeInit />
99+
<Meta ... />
100+
</head>
101+
<body class="invisible">
102+
<a href="#main" class="sr-only">Skip to main content</a>
103+
<MarketingHeader />
104+
<main id="main">
105+
<slot />
106+
</main>
107+
<MarketingFooter />
108+
</body>
109+
</html>
110+
```
111+
112+
The important part is what is missing, not what is added: no bootstrap script, no view transitions router, no shell web components, no PWA plumbing, no analytics, no footer bug reporter, no search, no theme-picker island.
113+
114+
For remeasuring shell cost, I would do it in two passes:
115+
116+
1. Build the homepage against that stripped marketing layout and rerun the same index.html script extraction you already used. That gives you "current homepage content + lighter shell".
117+
118+
2. Build one temporary blank marketing page with the same layout and almost no body content. That gives you the shell baseline by itself.
119+
120+
That second number is the one you actually want if the question is "what does the homepage pay just for the shell?" Right now your current measurement is a mix of shell cost and page-local cost. A lighter layout lets you separate them cleanly.
121+
122+
The existing repo precedent for this is PrintLayout.astro:1. The structural lesson from that file is correct: a truly light layout is a separate layout, not a flag on BaseLayout.astro:1. Once you start making BaseLayout conditional, you usually keep most of the bundle edges anyway.
123+
124+
If you want, I can next turn this into a concrete change proposal with:
125+
126+
1. the exact components I would remove from the homepage shell first,
127+
2. the likely script entries that would disappear,
128+
3. the order I would test them in so the deltas stay attributable.
129+
130+
I'm narrowing this to the behavioral decision: which shell features should be absent on the homepage versus deferred behind explicit triggers, so the answer stays operational instead of theoretical.Not by default. The point of a lighter homepage layout is that most of those features should simply be absent on /, not secretly reintroduced through another global loader.
131+
132+
The right split is:
133+
134+
- Features that can just not exist on the homepage: BaseLayout.astro:134, footer bug reporter, header search, tooltip shell, and the full bootstrap from index.astro:64. If they are not rendered, nothing is broken; the homepage just behaves like a normal static marketing page.
135+
136+
- Features that can be deferred only if the homepage still needs them: analytics, consent UI, theme picker, and maybe service worker registration. Those should be loaded by explicit need, not by a generic site bootstrap.
137+
138+
A good rule is: if a user cannot discover the missing behavior on the homepage, do not lazy load it there. Just omit it.
139+
140+
What that means feature by feature:
141+
142+
- View transitions router: do not lazy load it on the homepage. If BaseLayout.astro:134 is absent, links from home still work as normal full-page navigations. Then once the user lands on an interior page that uses BaseLayout.astro:1, view transitions can resume there. Nothing is broken; you just give up SPA-style transitions on the landing page.
143+
144+
- Bootstrap script: do not replace one big bootstrap with a lazy bootstrap. That just moves the cost later and keeps the coupling. The better move is to stop treating index.astro:64 as mandatory for every page, and only load the specific feature modules that a given page actually needs.
145+
146+
- Search: unless homepage search is a product requirement, it should not work there because it should not exist there. If you want a search affordance on home, that is a good candidate for interaction-triggered loading.
147+
148+
- Theme picker: separate "theme is applied" from "theme picker UI exists." The tiny theme-init path can still run so the page honors stored theme without FOUC, but the picker island itself can be absent on home. If you keep a theme control on home, then load that UI on click.
149+
150+
- Consent banner: this is the one I would not hand-wave away. If your policy requires consent controls on first entry, keep the minimal consent affordance on the homepage. But even here, I would aim for a narrow consent-specific loader, not the whole shell.
151+
152+
- Analytics: if you need marketing attribution on the homepage, defer it independently after consent or after initial render. If you are measuring shell cost, it should not be bundled through the shared page shell.
153+
154+
- PWA plumbing: for a marketing homepage, I would usually omit service worker registration and offline prefetch on /. Those features can begin on later app-like pages if they still matter.
155+
156+
So the short answer is: some things should intentionally not work on the homepage because they are not part of the homepage experience. Only lazy load the subset that is both valuable on / and user- or policy-driven.
157+
158+
The mistake to avoid is "minimal layout plus hidden global rehydration." If you do that, you keep the same architecture and mostly just delay the pain.
159+
160+
If you want, I can map these into three buckets next:
161+
162+
1. omit entirely on /
163+
2. keep but defer on /
164+
3. keep synchronous on / because they are required for correctness or compliance
165+
166+
I'm going to turn the shell features into three decision buckets, then separate "hidden global rehydration" from legitimate targeted deferral so the tradeoff is explicit.This is the mapping I would use for the homepage.
167+
168+
#### Bucket 1
169+
170+
Omit entirely on the homepage.
171+
172+
- View transitions router. If BaseLayout.astro:134 is not used on the homepage, links still work through normal navigations. You lose transition polish on entry, but not functionality.
173+
174+
- Global bootstrap. The large boot path from index.astro:64 should not exist on a marketing landing page unless a concrete homepage feature needs it.
175+
176+
- Header search. If search is not a primary homepage action, do not ship it there through Header.
177+
178+
- Theme picker UI. Keep theme application if needed, but omit the picker island itself from BaseLayout.astro:147.
179+
180+
- Footer bug reporter and footer enhancement logic from Footer.
181+
182+
- Tooltip shell. That is app chrome, not marketing-page core.
183+
- PWA offline prefetch. The homepage does not need to pay for PrefetchOfflinePage.
184+
185+
#### Bucket 2
186+
187+
Keep only if needed, and defer behind an explicit trigger or policy boundary.
188+
189+
- Analytics. If you need attribution on first entry, load analytics independently after consent or after first paint. Do not inherit it from the shared shell in BaseLayout.astro:190.
190+
191+
- Consent UI. This is the strongest candidate for "keep, but narrow." If the site needs consent handling on first visit, load only the consent path from Consent/Banner, not the whole app shell.
192+
193+
- Theme picker, but only if you expose a theme control on the homepage. If there is no control, do not load the picker.
194+
195+
- Search, but only if you make search a visible homepage action. Then load it on click or open, not on page load.
196+
Service worker registration, but only if there is a real homepage product reason. Usually I would start with "off on home" and justify it back in later.
197+
198+
#### Bucket 3
199+
200+
Keep synchronously because it is required for correctness, presentation, or compliance.
201+
202+
- Critical metadata and head tags from src/components/Head/index.astro, but split away from the bootstrap entry.
203+
Theme initialization only, if you need to avoid flash-of-wrong-theme. That is different from keeping the theme picker.
204+
205+
- Basic skip link and static header/footer structure.
206+
207+
- Any legally required consent placeholder if policy requires that users can immediately see or access privacy controls on first entry.
208+
209+
#### "Minimal layout plus hidden global rehydration" is not a synonym for deferred loading.
210+
211+
They look similar from far away because both reduce some upfront execution, but architecturally they are different.
212+
213+
**Deferred loading, used well, means:**
214+
215+
- the homepage starts with a genuinely smaller feature surface
216+
217+
- only a specific feature bundle is loaded later
218+
219+
- the trigger is explicit, like consent accepted, user clicked search, user opened theme control, or a clearly bounded idle task
220+
221+
- the deferred bundle is local to that feature
222+
223+
**Hidden global rehydration means:**
224+
225+
- you render a minimal-looking layout on the server
226+
227+
- but you still attach a general-purpose client bootstrap later
228+
229+
- that bootstrap scans the page, wires up many shell behaviors, and starts recreating the same global runtime you thought you removed
230+
231+
- the coupling is still global, so the homepage remains architecturally tied to the app shell even if the work happens later
232+
233+
So the difference is not timing alone. It is scope and coupling.
234+
235+
A good smell test is this:
236+
237+
- If the later-loaded code says "initialize homepage search," that is targeted deferral.
238+
239+
- If the later-loaded code says "initialize the site" or "bootstrap all shell behaviors," that is hidden global rehydration.
240+
241+
**Why hidden global rehydration is usually the wrong move here:**
242+
243+
- It often preserves most of the total JS cost, just shifted later.
244+
245+
- It keeps shared dependencies sticky, so the homepage still drags the app shell architecture behind it.
246+
247+
- It can improve some Lighthouse timings while still hurting real-user experience on slower devices once idle work kicks in.
248+
249+
- It makes attribution harder, because the homepage no longer has a clean boundary between what belongs to marketing and what belongs to the application shell.
250+
251+
**Why targeted deferral is still useful:**
252+
253+
- It lets the homepage stay mostly static.
254+
255+
- It preserves specific high-value behaviors.
256+
257+
- It makes the bundle graph legible: this feature exists because the user did something that asked for it.
258+
259+
**So my recommendation is not "defer everything." It is:**
260+
261+
- Remove whole homepage-irrelevant shell features.
262+
263+
- Keep only the smallest synchronous primitives needed for correctness.
264+
265+
- Defer a small number of user-discoverable or compliance-driven features with feature-specific loaders.
266+
267+
- Avoid any replacement that effectively recreates index.astro:64 under a different name.
268+
269+
If we evaluate this tomorrow, the practical decision framework is simple:
270+
271+
- If a homepage visitor would never miss it, omit it.
272+
- If they only need it after an action, defer it specifically.
273+
- If the page is broken or non-compliant without it, keep it synchronous.
274+
275+
If you want tomorrow, I can turn this into a concrete scorecard for each current homepage shell feature with columns for user value, compliance risk, likely byte impact, and recommended bucket.
276+
277+
#### Chunk weight
278+
279+
Vercel supports both gzip and brotli, and if the client supports Brotli, Vercel prefers Brotli over gzip for eligible text assets like HTML, CSS, and JavaScript.
280+
281+
From the current built homepage, the JS breaks down like this:
282+
283+
Direct entries: about 279.7 KB raw, 100.0 KB gzip
284+
Shared chunks: about 45.7 KB raw, 17.5 KB gzip
285+
286+
Total JS graph for the homepage: about 325.5 KB raw, 117.4 KB gzip
287+
288+
Homepage CSS only:
289+
290+
Total CSS raw: 220,895 bytes, about 215.7 KB
291+
Total CSS gzip: 34,691 bytes, about 33.9 KB
292+
Total CSS Brotli estimate: 28,242 bytes, about 27.6 KB
293+
294+
And for the Brotli side of the JS audit:
295+
296+
Total reachable homepage JS Brotli estimate: 106,493 bytes, about 104.0 KB
297+
298+
If you want the full first-load asset picture from the current build:
299+
300+
JS + CSS raw: 554,167 bytes, about 541.2 KB
301+
JS + CSS gzip: 154,920 bytes, about 151.3 KB
302+
JS + CSS Brotli estimate: 134,735 bytes, about 131.6 KB
303+
304+
What that means in practice:
305+
306+
The 22 entry chunks are not a strict A -> B -> C -> D chain. They should mostly look like a stack.
307+
But there is still a second-wave dependency step for shared imports, so it is not a perfectly flat stack either.
308+
In the current build, the shared JS layer is only 3 chunks deep and the max dependency depth is 1, so this is a one-step staircase, not a long waterfall.
309+
The three shared chunks currently causing that extra step are:
310+
311+
- embla-carousel.esm
312+
- focus-trap.esm
313+
- workbox-window.prod.es5
314+
315+
So the honest answer is:
316+
317+
- No, you do not appear to have a severe multi-hop dependency waterfall.
318+
- Yes, you do have some waterfall behavior because shared chunks are not preloaded and must be discovered from the entry modules.
319+
320+
The bigger problem on this homepage is still total shipped JS and chunk fan-out, not a deep chained graph. If you opened DevTools, I would expect "mostly stacked direct requests, plus a smaller second wave" rather than a long staircase.
321+
322+
If you want, I can next turn that into a plain-English takeaway for your _TODO.md, like: "fan-out is the main issue; dependency waterfall is present but shallow."
71323

72324
### Search page
73325

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Consent/Banner/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import CookieSvg from '@assets/images/site/cookie.svg'
88
<consent-banner class="print:hidden!">
99
<div
1010
id="consent-modal-id"
11-
class="bg-page-offset bottom-0 left-0 fixed right-0 w-full z-(--z-content-floating)"
11+
class="bg-page-offset bottom-0 left-0 fixed right-0 w-full z-(--z-content-floating) focus:outline-none focus-visible:outline-none"
1212
style="display:none;"
1313
role="dialog"
1414
aria-labelledby="consent-modal__title"

0 commit comments

Comments
 (0)