ui-ux-consultant-cli 1.0.0
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.
- package/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- 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
|