ui-ux-consultant-cli 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/ui-ux-consultant/SKILL.md +844 -0
  2. package/assets/ui-ux-consultant/references/accessibility.md +175 -0
  3. package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
  4. package/assets/ui-ux-consultant/references/animations.md +448 -0
  5. package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
  6. package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
  7. package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
  8. package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
  9. package/assets/ui-ux-consultant/references/components.md +1116 -0
  10. package/assets/ui-ux-consultant/references/patterns.md +600 -0
  11. package/assets/ui-ux-consultant/references/performance.md +198 -0
  12. package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
  13. package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
  14. package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
  15. package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
  16. package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
  17. package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
  18. package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
  19. package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
  20. package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
  21. package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
  22. package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
  23. package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
  24. package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
  25. package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
  26. package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
  27. package/assets/ui-ux-consultant/references/theming.md +701 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +130 -0
  30. package/package.json +51 -0
@@ -0,0 +1,198 @@
1
+ # Angular Performance Patterns with UX Implications
2
+
3
+ ## Section 1: @defer — Deferred Loading Blocks (Angular 17+)
4
+
5
+ `@defer` is Angular's built-in lazy loading for template sections. It's a design decision as much as a technical one.
6
+
7
+ **Trigger types and when to use:**
8
+ ```html
9
+ <!-- Load when element enters viewport — use for below-fold sections -->
10
+ @defer (on viewport) {
11
+ <app-analytics-charts />
12
+ } @placeholder {
13
+ <div class="chart-skeleton" style="height: 300px;"></div>
14
+ }
15
+
16
+ <!-- Load on first user interaction — use for non-critical widgets -->
17
+ @defer (on interaction) {
18
+ <app-comments-section />
19
+ } @placeholder {
20
+ <button>Load comments</button>
21
+ }
22
+
23
+ <!-- Load after browser is idle — use for very low-priority content -->
24
+ @defer (on idle) {
25
+ <app-recommendations />
26
+ } @placeholder {
27
+ <div class="rec-placeholder">Loading recommendations...</div>
28
+ }
29
+
30
+ <!-- Load after a timer — use to prioritize above-fold paint -->
31
+ @defer (on timer(2s)) {
32
+ <app-chat-widget />
33
+ }
34
+
35
+ <!-- Conditional defer — load when a signal/condition is true -->
36
+ @defer (when isLoggedIn()) {
37
+ <app-user-panel />
38
+ }
39
+ ```
40
+
41
+ **Always provide @placeholder:** Shows while deferred block hasn't loaded. Keep placeholder lightweight (skeleton or simple div).
42
+
43
+ **@loading block** (shows during actual chunk fetch):
44
+ ```html
45
+ @defer (on viewport) {
46
+ <app-heavy-chart />
47
+ } @loading (minimum 200ms) {
48
+ <mat-spinner />
49
+ } @placeholder {
50
+ <div class="skeleton"></div>
51
+ } @error {
52
+ <p>Failed to load. <button (click)="retry()">Retry</button></p>
53
+ }
54
+ ```
55
+
56
+ `minimum 200ms` prevents flash of loading spinner for fast connections.
57
+
58
+ ---
59
+
60
+ ## Section 2: OnPush + Signals — Zero-Overhead Change Detection
61
+
62
+ **Why OnPush:** Default change detection checks every component on every browser event. OnPush checks only when:
63
+ - An `input()` reference changes
64
+ - An event originates from within the component
65
+ - `markForCheck()` is called
66
+ - An `async` pipe receives a new value
67
+ - A signal read in the template emits a new value
68
+
69
+ **With signals, OnPush is essentially free** — Angular's signal-based change detection only re-renders what changed.
70
+
71
+ ```typescript
72
+ @Component({
73
+ changeDetection: ChangeDetectionStrategy.OnPush, // ALWAYS
74
+ template: `<p>Count: {{ count() }}</p>`,
75
+ })
76
+ export class MyComponent {
77
+ count = signal(0); // template re-renders only when count changes
78
+ }
79
+ ```
80
+
81
+ **Rule:** Set `ChangeDetectionStrategy.OnPush` on every component. Make it a team standard.
82
+
83
+ ---
84
+
85
+ ## Section 3: trackBy in @for
86
+
87
+ Without `track`, Angular re-creates DOM nodes when list order changes. With `track`, it reuses nodes.
88
+
89
+ ```html
90
+ <!-- BAD — re-creates all DOM on any change -->
91
+ @for (item of items(); ) { ... }
92
+
93
+ <!-- GOOD — reuses DOM nodes by ID -->
94
+ @for (item of items(); track item.id) { ... }
95
+
96
+ <!-- For primitive arrays -->
97
+ @for (name of names(); track name) { ... }
98
+
99
+ <!-- For index (last resort — less efficient) -->
100
+ @for (item of items(); track $index) { ... }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Section 4: NgOptimizedImage
106
+
107
+ Replace every `<img>` with `NgOptimizedImage` directives:
108
+ ```html
109
+ <!-- OLD -->
110
+ <img src="/hero.jpg" alt="Hero" />
111
+
112
+ <!-- NEW — import NgOptimizedImage -->
113
+ <img ngSrc="/hero.jpg" alt="Hero" width="1200" height="600" />
114
+
115
+ <!-- LCP image (above fold) — add priority -->
116
+ <img ngSrc="/hero.jpg" alt="Hero" width="1200" height="600" priority />
117
+
118
+ <!-- Responsive image -->
119
+ <img ngSrc="/photo.jpg" alt="Photo" fill sizes="(max-width: 768px) 100vw, 50vw" />
120
+ ```
121
+
122
+ `NgOptimizedImage` automatically: sets `loading="lazy"` (except `priority`), generates `srcset`, adds `fetchpriority="high"` on priority images, warns on missing dimensions.
123
+
124
+ **Setup:**
125
+ ```typescript
126
+ import { NgOptimizedImage } from '@angular/common';
127
+ @Component({ imports: [NgOptimizedImage] })
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Section 5: Route-Level Code Splitting
133
+
134
+ Every lazy route = separate JS chunk = faster initial load:
135
+ ```typescript
136
+ // All routes should use loadComponent or loadChildren
137
+ { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent) }
138
+ { path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) }
139
+ ```
140
+
141
+ **Preloading strategy for UX** (load next likely routes after idle):
142
+ ```typescript
143
+ // app.config.ts
144
+ provideRouter(routes, withPreloading(PreloadAllModules))
145
+ // or selective: withPreloading(QuicklinkStrategy) from ngx-quicklink
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Section 6: SSR / Angular Universal UX Patterns
151
+
152
+ **Hydration (Angular 17+):** Enable for fast initial paint + SEO:
153
+ ```typescript
154
+ // app.config.ts
155
+ export const appConfig: ApplicationConfig = {
156
+ providers: [
157
+ provideClientHydration(), // add this
158
+ provideRouter(routes),
159
+ ],
160
+ };
161
+ ```
162
+
163
+ **Avoid SSR breaking patterns:**
164
+ ```typescript
165
+ // BAD — crashes on server (no window)
166
+ constructor() { window.addEventListener('scroll', ...) }
167
+
168
+ // GOOD — use afterRender() (client-only)
169
+ constructor() {
170
+ afterRender(() => { window.addEventListener('scroll', ...) });
171
+ }
172
+
173
+ // Or inject PLATFORM_ID
174
+ readonly isPlatformBrowser = isPlatformBrowser(inject(PLATFORM_ID));
175
+ ```
176
+
177
+ **Defer client-only UI:**
178
+ ```html
179
+ @defer (on idle) {
180
+ <app-chat-widget /> <!-- No SSR needed for this -->
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Section 7: Performance Checklist
187
+
188
+ | Check | Implementation |
189
+ |---|---|
190
+ | OnPush everywhere | `changeDetection: ChangeDetectionStrategy.OnPush` |
191
+ | trackBy in all @for | `track item.id` |
192
+ | Lazy routes | `loadComponent()` / `loadChildren()` |
193
+ | @defer for below-fold | `@defer (on viewport)` with `@placeholder` |
194
+ | NgOptimizedImage | Replace all `<img>` tags; `priority` on LCP |
195
+ | toSignal not subscribe | `toSignal(obs$)` in components |
196
+ | provideAnimationsAsync | Not `provideAnimations()` |
197
+ | Hydration enabled | `provideClientHydration()` in SSR apps |
198
+ | No window in constructor | Use `afterRender()` or `PLATFORM_ID` |
@@ -0,0 +1,382 @@
1
+ # Astro 4+ — UI/UX Reference
2
+
3
+ ## When to Read
4
+ Use this file when building with Astro. Covers islands architecture, hydration directives, Content Collections, View Transitions, data fetching, and UI library integration.
5
+
6
+ ---
7
+
8
+ ## Recommended Libraries
9
+
10
+ | Approach | Best for | Install |
11
+ |---|---|---|
12
+ | Tailwind CSS | Utility-first styling | `npx astro add tailwind` |
13
+ | shadcn/ui (React) | Copy-paste components | `npx astro add react` then shadcn |
14
+ | Starlight | Documentation sites | `npx create astro --template starlight` |
15
+ | DaisyUI | Tailwind component classes | `npm install daisyui` |
16
+ | Astro DB | SQLite-based database | `npx astro add db` |
17
+ | Nanostores | Cross-island state | `npm install nanostores` |
18
+ | Motion | Animations (islands) | `npm install motion` |
19
+
20
+ ---
21
+
22
+ ## Style Recommendations
23
+
24
+ - **Content / blog:** Minimalism + Swiss typographic style
25
+ - **Documentation:** Minimal + dark mode first (use Starlight)
26
+ - **Marketing / landing:** Aurora UI + Motion-Driven
27
+ - **Portfolio / creative:** Editorial, bold typography, large imagery
28
+ - **SaaS:** Flat Design + shadcn/ui React islands
29
+
30
+ ---
31
+
32
+ ## Core Concepts
33
+
34
+ - `.astro` files = **server-rendered by default** — zero JS shipped unless opted in
35
+ - **Islands:** interactive components get `client:*` directives to hydrate in browser
36
+ - **Content Collections:** type-safe content management for MDX/markdown
37
+ - **View Transitions:** `<ViewTransitions />` for SPA-like page transitions
38
+ - **Adapters:** SSR with Node, Vercel, Netlify, Cloudflare via `npx astro add <adapter>`
39
+
40
+ ---
41
+
42
+ ## Top UX Patterns with Code
43
+
44
+ ### 1. Islands Hydration Directives
45
+
46
+ ```astro
47
+ <!-- client:load — hydrate immediately (above fold, critical UI) -->
48
+ <ReactCounter client:load />
49
+
50
+ <!-- client:idle — hydrate when browser idle (non-critical, below fold) -->
51
+ <NewsletterForm client:idle />
52
+
53
+ <!-- client:visible — hydrate when scrolled into view (lazy hydration) -->
54
+ <CommentsSection client:visible />
55
+
56
+ <!-- client:media — hydrate only on matching media query -->
57
+ <MobileMenu client:media="(max-width: 768px)" />
58
+
59
+ <!-- client:only — skip SSR entirely (browser-API-dependent) -->
60
+ <ChatWidget client:only="react" />
61
+ <MapComponent client:only="svelte" />
62
+ ```
63
+
64
+ **Rule:** default to no directive (static HTML), escalate only when interaction is needed.
65
+
66
+ ### 2. Data Fetching in Frontmatter
67
+
68
+ ```astro
69
+ ---
70
+ // Runs server-side ONLY — never ships to browser
71
+ const [posts, featured] = await Promise.all([
72
+ fetch('https://api.example.com/posts').then(r => r.json()),
73
+ fetch('https://api.example.com/posts/featured').then(r => r.json()),
74
+ ]);
75
+
76
+ const { slug } = Astro.params;
77
+ const user = Astro.locals.user; // from middleware
78
+ const theme = Astro.cookies.get('theme'); // read cookies
79
+ ---
80
+
81
+ <ul>
82
+ {posts.map(post => (
83
+ <li>
84
+ <a href={`/posts/${post.slug}`}>{post.title}</a>
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ ```
89
+
90
+ ### 3. Content Collections
91
+
92
+ ```typescript
93
+ // src/content/config.ts
94
+ import { defineCollection, z } from 'astro:content';
95
+
96
+ const blog = defineCollection({
97
+ type: 'content', // MDX/markdown
98
+ schema: z.object({
99
+ title: z.string(),
100
+ publishDate: z.date(),
101
+ tags: z.array(z.string()).default([]),
102
+ draft: z.boolean().default(false),
103
+ heroImage: z.string().optional(),
104
+ }),
105
+ });
106
+
107
+ const authors = defineCollection({
108
+ type: 'data', // JSON/YAML
109
+ schema: z.object({
110
+ name: z.string(),
111
+ bio: z.string(),
112
+ avatar: z.string().url(),
113
+ }),
114
+ });
115
+
116
+ export const collections = { blog, authors };
117
+ ```
118
+
119
+ ```astro
120
+ ---
121
+ // src/pages/blog/index.astro
122
+ import { getCollection, getEntry } from 'astro:content';
123
+
124
+ // Get all non-draft posts, sorted by date
125
+ const posts = (await getCollection('blog', ({ data }) => !data.draft))
126
+ .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf());
127
+ ---
128
+
129
+ {posts.map(post => (
130
+ <article>
131
+ <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
132
+ <time>{post.data.publishDate.toLocaleDateString()}</time>
133
+ </article>
134
+ ))}
135
+ ```
136
+
137
+ ```astro
138
+ ---
139
+ // src/pages/blog/[slug].astro
140
+ import { getCollection } from 'astro:content';
141
+
142
+ export async function getStaticPaths() {
143
+ const posts = await getCollection('blog');
144
+ return posts.map(post => ({
145
+ params: { slug: post.slug },
146
+ props: { post },
147
+ }));
148
+ }
149
+
150
+ const { post } = Astro.props;
151
+ const { Content } = await post.render();
152
+ ---
153
+
154
+ <article>
155
+ <h1>{post.data.title}</h1>
156
+ <Content />
157
+ </article>
158
+ ```
159
+
160
+ ### 4. View Transitions
161
+
162
+ ```astro
163
+ ---
164
+ // src/layouts/Base.astro
165
+ import { ViewTransitions } from 'astro:transitions';
166
+ ---
167
+ <html>
168
+ <head>
169
+ <ViewTransitions />
170
+ </head>
171
+ <body>
172
+ <slot />
173
+ </body>
174
+ </html>
175
+ ```
176
+
177
+ ```astro
178
+ <!-- Shared element transitions with transition:name -->
179
+ <!-- In list page: -->
180
+ <img src={post.hero} transition:name={`hero-${post.slug}`} alt="" />
181
+ <h2 transition:name={`title-${post.slug}`}>{post.title}</h2>
182
+
183
+ <!-- In detail page — same transition:name creates morph animation -->
184
+ <img src={post.hero} transition:name={`hero-${post.slug}`} alt="" />
185
+ <h1 transition:name={`title-${post.slug}`}>{post.title}</h1>
186
+ ```
187
+
188
+ ```astro
189
+ <!-- Custom transition animations -->
190
+ <div transition:animate="slide">Slides between pages</div>
191
+ <div transition:animate="fade">Fades between pages</div>
192
+ <div transition:animate={{ old: fadeOut, new: fadeIn }}>Custom</div>
193
+ ```
194
+
195
+ ### 5. Cross-Island State with Nanostores
196
+
197
+ ```typescript
198
+ // src/stores/cart.ts
199
+ import { atom, computed } from 'nanostores';
200
+
201
+ export interface CartItem { id: string; qty: number; price: number; }
202
+
203
+ export const cartItems = atom<CartItem[]>([]);
204
+ export const cartTotal = computed(cartItems, items =>
205
+ items.reduce((sum, i) => sum + i.qty * i.price, 0)
206
+ );
207
+
208
+ export function addToCart(item: CartItem) {
209
+ const current = cartItems.get();
210
+ const existing = current.find(i => i.id === item.id);
211
+ if (existing) {
212
+ cartItems.set(current.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i));
213
+ } else {
214
+ cartItems.set([...current, item]);
215
+ }
216
+ }
217
+ ```
218
+
219
+ ```tsx
220
+ // React island — src/components/CartIcon.tsx
221
+ import { useStore } from '@nanostores/react';
222
+ import { cartItems } from '../stores/cart';
223
+
224
+ export function CartIcon() {
225
+ const items = useStore(cartItems);
226
+ return <button>Cart ({items.length})</button>;
227
+ }
228
+ ```
229
+
230
+ ```svelte
231
+ <!-- Svelte island — src/components/AddToCart.svelte -->
232
+ <script>
233
+ import { cartItems, addToCart } from '../stores/cart';
234
+ export let product;
235
+ </script>
236
+ <button on:click={() => addToCart(product)}>Add to Cart</button>
237
+ ```
238
+
239
+ ### 6. API Endpoints
240
+
241
+ ```typescript
242
+ // src/pages/api/subscribe.ts
243
+ import type { APIRoute } from 'astro';
244
+
245
+ export const POST: APIRoute = async ({ request }) => {
246
+ const { email } = await request.json();
247
+
248
+ if (!email) {
249
+ return new Response(JSON.stringify({ error: 'Email required' }), {
250
+ status: 400,
251
+ headers: { 'Content-Type': 'application/json' },
252
+ });
253
+ }
254
+
255
+ await addToMailingList(email);
256
+ return new Response(JSON.stringify({ success: true }), {
257
+ status: 200,
258
+ headers: { 'Content-Type': 'application/json' },
259
+ });
260
+ };
261
+ ```
262
+
263
+ ### 7. Middleware for Auth/Sessions
264
+
265
+ ```typescript
266
+ // src/middleware.ts
267
+ import { defineMiddleware } from 'astro:middleware';
268
+
269
+ export const onRequest = defineMiddleware(async (context, next) => {
270
+ const session = context.cookies.get('session')?.value;
271
+ if (session) {
272
+ context.locals.user = await validateSession(session);
273
+ }
274
+
275
+ // Protect routes
276
+ if (context.url.pathname.startsWith('/dashboard') && !context.locals.user) {
277
+ return context.redirect('/login');
278
+ }
279
+
280
+ return next();
281
+ });
282
+ ```
283
+
284
+ ### 8. Image Optimization
285
+
286
+ ```astro
287
+ ---
288
+ import { Image, Picture } from 'astro:assets';
289
+ import heroImage from '../assets/hero.jpg';
290
+ ---
291
+
292
+ <!-- Optimized, prevents layout shift -->
293
+ <Image src={heroImage} alt="Hero" width={800} height={400} />
294
+
295
+ <!-- Multiple formats + responsive -->
296
+ <Picture
297
+ src={heroImage}
298
+ formats={['avif', 'webp']}
299
+ sizes="(max-width: 640px) 100vw, 800px"
300
+ alt="Hero"
301
+ />
302
+
303
+ <!-- Remote images require domain allowlist in astro.config -->
304
+ <Image src="https://example.com/image.jpg" alt="Remote" width={400} height={300} inferSize />
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Best Practices by Category
310
+
311
+ ### Architecture
312
+ - Default to static rendering; add SSR adapter only when needed
313
+ - Use `getStaticPaths` for all dynamic routes in SSG mode
314
+ - Prefer Content Collections over manual file reads for structured content
315
+ - Co-locate component assets: put images/styles next to components that use them
316
+ - Use `src/layouts/` for page shells, `src/components/` for islands
317
+
318
+ ### Islands Strategy
319
+ - Start with zero islands; add only when user interaction requires it
320
+ - `client:visible` for any component that loads below the fold
321
+ - `client:only` for components using browser APIs (window, localStorage, WebGL)
322
+ - Keep island bundle sizes small — one island per logical widget, not per page
323
+ - Share state between islands via nanostores, not props (islands are isolated)
324
+
325
+ ### Content & SEO
326
+ - Use Content Collections for all structured content — never parse frontmatter manually
327
+ - Define schemas with zod — catch content errors at build time, not runtime
328
+ - Add `<meta>` tags in layouts with Astro.props passthrough
329
+ - Generate `sitemap.xml` with `@astrojs/sitemap` integration
330
+ - Use `canonical` meta tag for paginated collections
331
+
332
+ ### Performance
333
+ - Zero-JS by default — confirm with browser DevTools network tab
334
+ - `<Image>` from `astro:assets` for all images (auto-generates width/height)
335
+ - Preload critical assets: `<link rel="preload">` in `<head>`
336
+ - Prefetch internal links: `<a href="/page" data-astro-prefetch>`
337
+ - Enable compression: handled automatically on Vercel/Netlify adapters
338
+
339
+ ### Accessibility
340
+ - Use semantic HTML in `.astro` files — no framework overhead means no excuse for divs
341
+ - `<Image>` requires `alt` — enforced at build time
342
+ - View Transitions: respect `prefers-reduced-motion` (Astro handles automatically)
343
+ - Landmark regions: `<header>`, `<main>`, `<nav>`, `<footer>` in layouts
344
+
345
+ ---
346
+
347
+ ## Common Anti-Patterns
348
+
349
+ 1. **`client:load` on everything** — defeats Astro's zero-JS default. Most content is static; only interactive widgets need hydration.
350
+
351
+ 2. **Fetching in client components instead of frontmatter** — `fetch()` in a React island runs in the browser, missing SSG/SSR, caching, and auth context. Fetch in frontmatter and pass as props.
352
+
353
+ 3. **Not using Content Collections for typed content** — manual frontmatter parsing produces runtime errors from typos or missing fields. Collections validate at build time.
354
+
355
+ 4. **Missing `transition:name` on shared elements** — without matching names, View Transitions default to a cross-fade. Named transitions create the morphing/hero animation.
356
+
357
+ 5. **React islands for simple interactive UI** — Svelte/Vue/Lit islands are smaller. For minimal interactions (toggle, dropdown), use Alpine.js or plain `<script>` with a dataset.
358
+
359
+ 6. **Putting large JS libraries in frontmatter** — frontmatter runs server-side only; importing a 500KB charting lib just for data transform is wasteful. Use it in an island if really needed, or transform data server-side and pass numbers as props.
360
+
361
+ 7. **Skipping `getStaticPaths` for dynamic routes** — causes 404s in SSG mode. Every dynamic segment needs `getStaticPaths` to enumerate valid paths.
362
+
363
+ 8. **Remote images without domain allowlist** — `astro.config.mjs` requires `image.domains` or `image.remotePatterns` for `<Image>` to optimize remote sources.
364
+
365
+ 9. **Using `document` or `window` in `.astro` scripts** — `.astro` files run server-side. Use `<script>` tags (which run client-side) or islands for browser APIs.
366
+
367
+ 10. **One giant `.astro` page file** — extract repeated HTML into components in `src/components/`. Even static, non-interactive components benefit from extraction.
368
+
369
+ ---
370
+
371
+ ## Performance Checklist
372
+
373
+ - [ ] No `client:*` directive by default (pure HTML output)
374
+ - [ ] `client:visible` for all below-fold interactive islands
375
+ - [ ] `client:only` for browser-API-dependent widgets (maps, canvas, WebGL)
376
+ - [ ] Content Collections for all MDX/markdown content (type-safe at build)
377
+ - [ ] `<Image>` from `astro:assets` for all images (auto-optimize, prevent CLS)
378
+ - [ ] ViewTransitions enabled in base layout for SPA-like navigation
379
+ - [ ] `data-astro-prefetch` on key internal links
380
+ - [ ] `@astrojs/sitemap` for SEO
381
+ - [ ] SSR adapter (Vercel/Netlify) only when dynamic data requires it
382
+ - [ ] Audit bundle size: `npx astro build --verbose` shows per-island sizes