@donotdev/cli 0.0.12 → 0.0.14

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 (85) hide show
  1. package/dependencies-matrix.json +32 -118
  2. package/dist/bin/commands/agent-setup.d.ts +6 -0
  3. package/dist/bin/commands/agent-setup.d.ts.map +1 -0
  4. package/dist/bin/commands/agent-setup.js +623 -0
  5. package/dist/bin/commands/agent-setup.js.map +1 -0
  6. package/dist/bin/commands/build.js +13 -12
  7. package/dist/bin/commands/bump.js +103 -35
  8. package/dist/bin/commands/cacheout.js +13 -12
  9. package/dist/bin/commands/create-app.js +53 -151
  10. package/dist/bin/commands/create-project.js +109 -167
  11. package/dist/bin/commands/deploy.js +7620 -30
  12. package/dist/bin/commands/dev.js +13 -12
  13. package/dist/bin/commands/emu.js +13 -12
  14. package/dist/bin/commands/firebase-setup.d.ts +6 -0
  15. package/dist/bin/commands/firebase-setup.d.ts.map +1 -0
  16. package/dist/bin/commands/firebase-setup.js +7 -0
  17. package/dist/bin/commands/firebase-setup.js.map +1 -0
  18. package/dist/bin/commands/format.js +13 -12
  19. package/dist/bin/commands/lint.js +13 -12
  20. package/dist/bin/commands/preview.js +13 -12
  21. package/dist/bin/commands/staging.d.ts +11 -0
  22. package/dist/bin/commands/staging.d.ts.map +1 -0
  23. package/dist/bin/commands/staging.js +12 -0
  24. package/dist/bin/commands/staging.js.map +1 -0
  25. package/dist/bin/commands/sync-secrets.js +13 -12
  26. package/dist/bin/commands/wai.js +7397 -11
  27. package/dist/bin/dndev.js +28 -3
  28. package/dist/bin/donotdev.js +28 -3
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +7760 -109
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/templates/app-demo/src/pages/DetailPage.tsx.example +1 -1
  35. package/templates/app-demo/src/pages/FullPage.tsx.example +3 -3
  36. package/templates/app-demo/src/pages/HomePage.tsx.example +1 -1
  37. package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +5 -5
  38. package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +3 -3
  39. package/templates/app-next/.env.example +2 -0
  40. package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
  41. package/templates/app-vite/.env.example +2 -0
  42. package/templates/app-vite/src/pages/HomePage.tsx.example +163 -73
  43. package/templates/functions-firebase/build.mjs.example +26 -10
  44. package/templates/functions-firebase/functions-firebase/build.mjs.example +26 -10
  45. package/templates/functions-firebase/functions.config.js.example +11 -15
  46. package/templates/github-consumer/.github/workflows/ci.yml.example +36 -0
  47. package/templates/root-consumer/.claude/agents/architect.md.example +2 -2
  48. package/templates/root-consumer/.claude/agents/builder.md.example +2 -2
  49. package/templates/root-consumer/.claude/agents/coder.md.example +2 -2
  50. package/templates/root-consumer/.claude/agents/extractor.md.example +2 -3
  51. package/templates/root-consumer/.claude/agents/polisher.md.example +67 -291
  52. package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +4 -4
  53. package/templates/root-consumer/.claude/commands/brainstorm.md.example +1 -1
  54. package/templates/root-consumer/.claude/commands/build.md.example +3 -3
  55. package/templates/root-consumer/.claude/commands/design.md.example +1 -1
  56. package/templates/root-consumer/.claude/commands/polish.md.example +66 -82
  57. package/templates/root-consumer/.dndev/args.json.example +6 -0
  58. package/templates/root-consumer/.env.example +13 -13
  59. package/templates/root-consumer/.gemini/settings.json.example +9 -0
  60. package/templates/root-consumer/.gitignore.example +3 -1
  61. package/templates/root-consumer/AI.md.example +150 -0
  62. package/templates/root-consumer/CLAUDE.md.example +19 -104
  63. package/templates/root-consumer/README.md.example +81 -255
  64. package/templates/root-consumer/entities/Contact.ts.example +126 -0
  65. package/templates/root-consumer/entities/index.ts.example +6 -3
  66. package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +59 -326
  67. package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +2 -1
  68. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +144 -9
  69. package/templates/root-consumer/guides/dndev/GOTCHAS.md.example +186 -0
  70. package/templates/root-consumer/guides/dndev/INDEX.md.example +10 -0
  71. package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +13 -16
  72. package/templates/root-consumer/guides/dndev/SETUP_BLOG.md.example +263 -0
  73. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +1 -1
  74. package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +168 -0
  75. package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +17 -19
  76. package/templates/root-consumer/guides/dndev/SETUP_TESTING.md.example +184 -0
  77. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +134 -69
  78. package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +66 -44
  79. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +18 -1
  80. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +1 -0
  81. package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +2 -1
  82. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +2 -1
  83. package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +180 -108
  84. package/templates/root-consumer/guides/wai-way/context_map.json.example +8 -7
  85. package/templates/root-consumer/guides/wai-way/page_patterns.md.example +4 -4
@@ -0,0 +1,186 @@
1
+ # Framework Gotchas
2
+
3
+ **Common mistakes. Read before coding. Phase tags show when each matters most.**
4
+
5
+ ---
6
+
7
+ ## Imports [Phase 1, 2, 3]
8
+
9
+ **Server code MUST use `/server` imports — client imports crash on deploy.**
10
+
11
+ ```typescript
12
+ // ✅ CORRECT
13
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
14
+ import { handleError } from '@donotdev/core/server';
15
+
16
+ // ❌ WRONG - crashes on deploy
17
+ import { getFirestore } from '@donotdev/firebase';
18
+ import { handleError } from '@donotdev/core';
19
+ ```
20
+
21
+ **Rule:** Always use `/server` suffix for `@donotdev/firebase/server`, `@donotdev/core/server`, `@donotdev/utils/server`.
22
+
23
+ **ESM only — never use `require()`.**
24
+
25
+ ```typescript
26
+ // ❌ WRONG
27
+ const { something } = require('@donotdev/package');
28
+
29
+ // ✅ CORRECT
30
+ import { something } from '@donotdev/package';
31
+ ```
32
+
33
+ **Import ordering is mandatory:**
34
+ 1. React (values, then types)
35
+ 2. Other vendors (values, then types)
36
+ 3. `@donotdev/*` packages (values, then types)
37
+ 4. Relative imports (values, then types)
38
+
39
+ One line for values, one line for types. Blank line between categories.
40
+
41
+ ---
42
+
43
+ ## Routing [Phase 1, 3]
44
+
45
+ **Never import from `react-router-dom` — use framework routing.**
46
+
47
+ ```tsx
48
+ // ❌ WRONG - breaks framework features
49
+ import { Link, useNavigate, useParams } from 'react-router-dom';
50
+
51
+ // ✅ CORRECT
52
+ import { Link, useNavigate, useParams } from '@donotdev/ui/routing';
53
+ ```
54
+
55
+ **Routes are auto-discovered** from `src/pages/*Page.tsx` with `pageMeta`. Don't use `<Routes>`, `<Route>`, or manual `<Outlet />`.
56
+
57
+ **Use `useRouteParam('id')` for typed route params** — not `useParams()` from react-router-dom.
58
+
59
+ **Navigation is auto-built.** Use `<DnDevNavigationMenu>` or `useNavigationItems()`. Don't build nav manually — you lose auth filtering and route discovery.
60
+
61
+ ---
62
+
63
+ ## Styling [Phase 3, 4]
64
+
65
+ **No inline `fontSize` or `font-size` — use `Text` level prop.**
66
+
67
+ ```tsx
68
+ // ❌ WRONG
69
+ <Text style={{ fontSize: '18px' }}>Title</Text>
70
+
71
+ // ✅ CORRECT
72
+ <Text level="h2">Title</Text>
73
+ ```
74
+
75
+ Levels: `h1`, `h2`, `h3`, `h4`, `body`, `small`, `caption`. Limit to 2–3 levels per page.
76
+
77
+ **RTL: Always use `start`/`end` — never `left`/`right`.**
78
+
79
+ ```css
80
+ /* ❌ WRONG - breaks RTL */
81
+ text-align: left;
82
+
83
+ /* ✅ CORRECT - works in both LTR and RTL */
84
+ text-align: start;
85
+ ```
86
+
87
+ Same for inline styles: `textAlign: 'start'` not `textAlign: 'left'`. Same for data attributes: `data-text-align="start|center|end"`.
88
+
89
+ ---
90
+
91
+ ## Components [Phase 3]
92
+
93
+ **Always `lookup_symbol` before using any `@donotdev` component.** Never guess props.
94
+
95
+ Common wrong props:
96
+ - `Button`: no `size`, `tone`, `gap` — use `variant`, `display`
97
+ - `Text`: no `size`, `tone`, `color` — use `level`, `variant`
98
+ - `Stack`: no `spacing`, `size` — use `gap`
99
+ - `Card`: no `padding`, `margin`, `size` — use `title`, `subtitle`, `content`, `footer`
100
+ - `Grid`: no `columns` — use `cols`
101
+
102
+ **If you can't do it with framework components:** Stop. Tell the user what's missing. Don't invent custom workarounds.
103
+
104
+ ---
105
+
106
+ ## CRUD [Phase 2, 3]
107
+
108
+ **Hidden fields are auto-added by `defineEntity()` — don't define them manually:**
109
+ - `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status`
110
+
111
+ **Scope field is auto-added** when `scope` is configured. Don't manually define the scope field (e.g., `companyId`).
112
+
113
+ **Custom form fields MUST use framework's `useController`:**
114
+
115
+ ```typescript
116
+ // ❌ WRONG
117
+ import { useController } from 'react-hook-form';
118
+
119
+ // ✅ CORRECT
120
+ import { useController } from '@donotdev/crud';
121
+ ```
122
+
123
+ **Form components receive `control` prop, not `field` prop.**
124
+
125
+ **Price field is structured:** `{ amount, currency, vatIncluded, discountPercent }`. Don't store computed discount amounts.
126
+
127
+ **File uploads are deferred** — files upload on form submit, not on selection. Images show optimistic blob URLs.
128
+
129
+ **Entity namespace defaults to `entity-{name}`** (lowercase). Translation files must match: `locales/entity-product_en.json`.
130
+
131
+ ---
132
+
133
+ ## Functions [Phase 3, 4]
134
+
135
+ **Use `createFunction` for custom functions — 3 params, everything included:**
136
+
137
+ ```typescript
138
+ import { createFunction } from '@donotdev/functions/firebase';
139
+
140
+ export const myFunction = createFunction(schema, 'operation_name', async (data, { uid }) => {
141
+ // Your logic
142
+ });
143
+ ```
144
+
145
+ **Use `createBaseFunction` only when you need custom config** (memory, timeout, region override).
146
+
147
+ **Deploy with `dndev deploy`** — not `firebase deploy`. Manual deploy causes CORS 403 on preflight because Cloud Run blocks unauthenticated OPTIONS by default.
148
+
149
+ **Naming:** Export in camelCase (`getDashboardMetrics`), operation ID in snake_case (`get_dashboard_metrics`).
150
+
151
+ **CRUD functions are one-liner:** `export const crud = createCrudFunctions(entities);` — generates all CRUD endpoints per entity. Access controlled via `entity.access`.
152
+
153
+ ---
154
+
155
+ ## i18n [Phase 3, 4]
156
+
157
+ **Phase 3: hardcode strings. Phase 4: add translations.** Don't i18n too early.
158
+
159
+ **Two loading strategies:**
160
+ - **Eager** (always loaded): `src/locales/common_en.json` — navigation, buttons, common UI
161
+ - **Lazy** (loaded per page): `src/pages/locales/home_en.json` — page-specific content
162
+
163
+ **Status field translations** fall back: `entity-{name}` namespace → `crud` namespace.
164
+
165
+ **Rich text uses `<Trans>` component** with supported tags only: `<accent>`, `<primary>`, `<muted>`, `<success>`, `<warning>`, `<error>`, `<bold>`, `<code>`.
166
+
167
+ **Array translations use `tList`:**
168
+ ```tsx
169
+ import { tList } from '@donotdev/ui';
170
+ <Card content={tList(t, 'features.items', 4)} />
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Dependencies [Phase 1]
176
+
177
+ **Apps don't declare bundled deps.** Framework packages (`@donotdev/*`) provide everything. Don't add `react-router-dom`, `react-hook-form`, `valibot`, etc. to app's `package.json` — they come through framework deps.
178
+
179
+ **Environment variables:**
180
+ - Client: `apps/my-app/.env` (prefix with `VITE_*`)
181
+ - Server: `functions/.env` (secrets: `STRIPE_*`, OAuth tokens)
182
+ - Local overrides: `.env.local` (gitignored)
183
+
184
+ ---
185
+
186
+ **When in doubt: search the codebase first, ask the user second, never guess.**
@@ -6,6 +6,15 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## Getting Started
10
+
11
+ - [ENV_SETUP.md](./ENV_SETUP.md) - **START HERE** — Full onboarding flow (install → firebase → deploy)
12
+ - [GOTCHAS.md](./GOTCHAS.md) - **Common mistakes & pitfalls** (phase-tagged, read before coding)
13
+ - [SETUP_FIREBASE.md](./SETUP_FIREBASE.md) - Firebase project setup (`dndev firebase:setup`)
14
+ - [SETUP_TESTING.md](./SETUP_TESTING.md) - Test generation (Phase 4)
15
+
16
+ ---
17
+
9
18
  ## Core Setup
10
19
 
11
20
  - [SETUP_PAGES.md](./SETUP_PAGES.md) - Pages & routing (pre-configured)
@@ -23,6 +32,7 @@
23
32
  - [SETUP_AUTH.md](./SETUP_AUTH.md) - Authentication (pre-configured)
24
33
  - [SETUP_OAUTH.md](./SETUP_OAUTH.md) - OAuth connections (pre-configured)
25
34
  - [SETUP_BILLING.md](./SETUP_BILLING.md) - Stripe subscriptions (pre-configured)
35
+ - [SETUP_BLOG.md](./SETUP_BLOG.md) - Convention-based markdown blog with i18n
26
36
  - [SETUP_PWA.md](./SETUP_PWA.md) - Progressive Web App setup
27
37
  - [SETUP_FUNCTIONS.md](./SETUP_FUNCTIONS.md) - Firebase Functions (pre-configured)
28
38
 
@@ -107,29 +107,26 @@ export default defineViteConfig({
107
107
 
108
108
  ## 4. Environment Variables
109
109
 
110
- ```bash
111
- # .env
112
- VITE_APP_NAME="My App"
113
- VITE_APP_URL=http://localhost:5173
110
+ **Vite loads `.env` from the app directory only.** Not the repo root.
114
111
 
115
- # Firebase
116
- VITE_FIREBASE_API_KEY=your-api-key
117
- VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
118
- VITE_FIREBASE_PROJECT_ID=your-project-id
119
- VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
120
- VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
121
- VITE_FIREBASE_APP_ID=1:123456789:web:abcdef
112
+ Run `dndev firebase:setup` to auto-populate Firebase config. See [SETUP_FIREBASE.md](./SETUP_FIREBASE.md).
122
113
 
123
- # Auth providers
124
- VITE_AUTH_PARTNERS=password,emailLink,google,github
125
-
126
- # Stripe
114
+ ```bash
115
+ # apps/<your-app>/.env — client-side variables (exposed to browser)
116
+ VITE_APP_URL=http://localhost:5173
117
+ VITE_DONOTDEV_LICENSE_KEY=dndev_your_key_here
118
+ VITE_FIREBASE_API_KEY=... # Written by dndev firebase:setup
119
+ VITE_FIREBASE_PROJECT_ID=... # Written by dndev firebase:setup
120
+ VITE_AUTH_PARTNERS=github,google
127
121
  VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
122
+
123
+ # functions/.env — server-side secrets (never exposed to browser)
128
124
  STRIPE_SECRET_KEY=sk_test_...
129
125
  STRIPE_WEBHOOK_SECRET=whsec_...
130
- STRIPE_API_VERSION=2025-08-27.basil
131
126
  ```
132
127
 
128
+ Push server secrets to Firebase: `dndev sync-secrets`
129
+
133
130
  ---
134
131
 
135
132
  **Zero config. Override when needed.**
@@ -0,0 +1,263 @@
1
+ # Setup: Blog
2
+
3
+ **Convention-based markdown blog.** Drop `slug_lang.md` files in `src/content/blog/`, get a fully functional blog with i18n, reading time, tags, RSS, and sitemap.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Create the content directory
10
+
11
+ ```
12
+ src/content/blog/
13
+ ├── my-first-post_en.md
14
+ ├── my-first-post_fr.md
15
+ └── another-post_en.md
16
+ ```
17
+
18
+ ### 2. Write a post with frontmatter
19
+
20
+ ```markdown
21
+ ---
22
+ title: My First Post
23
+ description: A short summary for listings and SEO
24
+ date: 2025-06-01
25
+ tags: tutorial, getting-started
26
+ image: /blog/my-first-post.png
27
+ ---
28
+
29
+ Your markdown content here. Supports:
30
+ - **Bold**, *italic*, ~~strikethrough~~
31
+ - [Internal links](/faq) and [external links](https://example.com)
32
+ - Images: ![alt text](/blog/my-image.png)
33
+ - Code blocks with syntax highlighting
34
+ ```
35
+
36
+ ### 3. Create the data loader
37
+
38
+ ```typescript
39
+ // src/data/blog/index.ts
40
+ import { createBlogLoader } from '@donotdev/templates';
41
+ import type { BlogLoader } from '@donotdev/templates';
42
+
43
+ const markdownFiles = import.meta.glob('../../content/blog/*.md', {
44
+ query: '?raw',
45
+ import: 'default',
46
+ eager: true,
47
+ });
48
+
49
+ export function getBlogLoader(lang: string = 'en'): BlogLoader {
50
+ return createBlogLoader(markdownFiles as Record<string, string>, lang);
51
+ }
52
+ ```
53
+
54
+ ### 4. Create the listing page
55
+
56
+ ```typescript
57
+ // src/pages/BlogPage.tsx
58
+ import { BookOpen } from 'lucide-react';
59
+ import { HeroSection } from '@donotdev/components';
60
+ import { useTranslation, type PageMeta } from '@donotdev/core';
61
+ import { BlogList } from '@donotdev/templates';
62
+ import { PageContainer } from '@donotdev/ui';
63
+ import { getBlogLoader } from '../data/blog';
64
+
65
+ export const NAMESPACE = 'blog';
66
+ export const meta: PageMeta = { namespace: NAMESPACE, icon: <BookOpen /> };
67
+
68
+ export default function BlogPage() {
69
+ const { t, i18n } = useTranslation([NAMESPACE]);
70
+ const posts = getBlogLoader(i18n?.language).getAllPosts();
71
+
72
+ return (
73
+ <PageContainer variant="docs">
74
+ <HeroSection title={t('title')} subtitle={t('subtitle')} />
75
+ <BlogList posts={posts} />
76
+ </PageContainer>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ### 5. Create the post page (dynamic route)
82
+
83
+ ```typescript
84
+ // src/pages/BlogPostPage.tsx
85
+ import { BookOpen } from 'lucide-react';
86
+ import { useTranslation, type PageMeta } from '@donotdev/core';
87
+ import { BlogPostView } from '@donotdev/templates';
88
+ import { PageContainer, useRouteParam } from '@donotdev/ui';
89
+ import { getBlogLoader } from '../data/blog';
90
+
91
+ export const NAMESPACE = 'blog';
92
+ export const meta: PageMeta = {
93
+ namespace: NAMESPACE,
94
+ icon: <BookOpen />,
95
+ route: '/blog/:slug',
96
+ hideFromMenu: true,
97
+ };
98
+
99
+ export default function BlogPostPage() {
100
+ const { i18n } = useTranslation([NAMESPACE]);
101
+ const slug = useRouteParam('slug');
102
+ const post = slug ? getBlogLoader(i18n?.language).getPostBySlug(slug) : null;
103
+
104
+ return (
105
+ <PageContainer variant="docs">
106
+ <BlogPostView post={post} />
107
+ </PageContainer>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ### 6. Add locale files
113
+
114
+ Blog UI labels (`readMore`, `backToBlog`, `publishedOn`, etc.) are provided by the framework via `blog_*.json` in `@donotdev/core`. Override any key by creating the same file in your app.
115
+
116
+ Page-specific strings (title, subtitle) go in your app locale:
117
+
118
+ ```json
119
+ // src/pages/locales/blog_en.json
120
+ {
121
+ "title": "Blog",
122
+ "subtitle": "Your subtitle here"
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## File Convention
129
+
130
+ **Pattern:** `slug_lang.md`
131
+
132
+ | File | Slug | Language |
133
+ |------|------|----------|
134
+ | `my-post_en.md` | `my-post` | English |
135
+ | `my-post_fr.md` | `my-post` | French |
136
+ | `my-post_de.md` | `my-post` | German |
137
+ | `solo-post_en.md` | `solo-post` | English (no FR = falls back to EN) |
138
+
139
+ **Rules:**
140
+ - Slug = everything before the last `_xx` suffix
141
+ - Language suffix must be 2-5 characters (ISO codes)
142
+ - English (`_en`) is the fallback language
143
+ - If a post only exists in `_en`, it's shown for all languages
144
+
145
+ ---
146
+
147
+ ## Frontmatter Fields
148
+
149
+ | Field | Required | Description |
150
+ |-------|----------|-------------|
151
+ | `title` | Yes | Post title |
152
+ | `description` | Yes | Summary for listings and SEO |
153
+ | `date` | Yes | Publication date (YYYY-MM-DD) |
154
+ | `tags` | No | Comma-separated tags |
155
+ | `image` | No | Hero image path — must be in `public/blog/` (see Images section) |
156
+
157
+ Custom fields are preserved in `meta[key]`.
158
+
159
+ ---
160
+
161
+ ## Images
162
+
163
+ ### Hero Images (frontmatter `image` field)
164
+
165
+ One image per post. Used in blog listing cards, article page, RSS feed, and OG/social sharing.
166
+
167
+ **Convention:**
168
+ - **Location:** `public/blog/[slug].png` (matches the post slug)
169
+ - **Size:** 1200×630px minimum (standard OG ratio, sharp on retina)
170
+ - **Format:** PNG or JPG
171
+ - **Reference:** `image: /blog/my-post.png` in frontmatter
172
+
173
+ ```
174
+ public/
175
+ └── blog/
176
+ ├── my-first-post.png ← matches my-first-post_en.md
177
+ ├── another-post.png ← matches another-post_en.md
178
+ └── inline-diagram.svg ← used in markdown body
179
+ ```
180
+
181
+ **Where it shows:**
182
+ | Context | Sizing |
183
+ |---------|--------|
184
+ | Blog listing (featured card) | `width: 100%`, `max-height: 360px`, `object-fit: cover` |
185
+ | Blog listing (grid cards) | `width: 100%`, `max-height: 180px`, `object-fit: cover` |
186
+ | Article page | `width: 100%`, `max-height: 480px`, `object-fit: cover` |
187
+ | RSS feed | `<enclosure>` with full URL |
188
+
189
+ ### Inline Images (in markdown body)
190
+
191
+ Images referenced in markdown content also go in `public/blog/`:
192
+
193
+ ```markdown
194
+ ![Architecture diagram](/blog/inline-diagram.svg)
195
+ ```
196
+
197
+ Rendered with `loading="lazy"` and `decoding="async"`.
198
+
199
+ **Co-located images (next to .md files) do NOT work** — `?raw` imports treat markdown as text strings, not processed modules.
200
+
201
+ ---
202
+
203
+ ## Tags & Filtering
204
+
205
+ ```typescript
206
+ const blog = getBlogLoader('en');
207
+ const allTags = blog.getAllTags(); // ['react', 'typescript', ...]
208
+ const filtered = blog.getPostsByTag('react'); // Posts tagged 'react'
209
+ ```
210
+
211
+ ---
212
+
213
+ ## RSS & Sitemap
214
+
215
+ **Auto-generated at build time.** The SEO pipeline (Vite `SEOPlugin` / Next.js `SEOHandler`) automatically:
216
+
217
+ 1. Discovers `src/content/blog/*_en.md` files
218
+ 2. Appends blog post URLs to `sitemap.xml`
219
+ 3. Generates `rss.xml` with all posts
220
+
221
+ No manual scripts needed. Just drop `.md` files and build.
222
+
223
+ ---
224
+
225
+ ## Scaling (50+ Posts)
226
+
227
+ Default uses `eager: true` (all posts bundled into JS). For large blogs, switch to lazy loading:
228
+
229
+ ```typescript
230
+ // Lazy: posts loaded on demand, not bundled
231
+ const markdownFiles = import.meta.glob('../../content/blog/*.md', {
232
+ query: '?raw',
233
+ import: 'default',
234
+ eager: false, // ← Change this
235
+ });
236
+
237
+ // Now each value is () => Promise<string> instead of string
238
+ // You'll need to await them before passing to createBlogLoader
239
+ ```
240
+
241
+ For most apps (< 50 posts), eager loading is optimal.
242
+
243
+ ---
244
+
245
+ ## Auto-Deploy
246
+
247
+ Push a new `.md` file → CI builds → `import.meta.glob` picks it up at build time → deployed. No CMS, no API.
248
+
249
+ ---
250
+
251
+ ## API Reference
252
+
253
+ From `@donotdev/templates`:
254
+
255
+ | Export | Type | Description |
256
+ |--------|------|-------------|
257
+ | `createBlogLoader` | Function | Create loader from glob results + language |
258
+ | `parseFrontmatter` | Function | Parse `---` frontmatter from raw markdown |
259
+ | `BlogList` | Component | Renders post listing with cards |
260
+ | `BlogPostView` | Component | Renders single post with MarkdownViewer |
261
+ | `BlogPost` | Type | Post with slug, meta, content, readingTime, tags |
262
+ | `BlogMeta` | Type | Frontmatter metadata |
263
+ | `BlogLoader` | Type | Loader interface |
@@ -366,7 +366,7 @@ export default function ProductDetailPage() {
366
366
  return (
367
367
  <PageContainer>
368
368
  <Section>
369
- <Stack direction="row" gap="medium" align="center">
369
+ <Stack direction="row" align="center">
370
370
  <h1>{data.name}</h1>
371
371
  <Button
372
372
  variant={isFavorite(id) ? 'primary' : 'outline'}
@@ -0,0 +1,168 @@
1
+ # Setup: Firebase
2
+
3
+ **From zero to deployed in 5 steps.**
4
+
5
+ ---
6
+
7
+ ## Step 1: Run Firebase Setup
8
+
9
+ ```bash
10
+ dndev firebase:setup
11
+ ```
12
+
13
+ This command:
14
+ - Checks Firebase CLI is installed and you're logged in
15
+ - Lists your Firebase projects (or creates a new one)
16
+ - Creates a web app if the project doesn't have one
17
+ - Gets the SDK config and writes it to your app's `.env`
18
+ - Updates `.firebaserc` with the project ID
19
+ - Updates `firebase.json` placeholders
20
+
21
+ **After running, it will prompt you for 2 manual steps (below).**
22
+
23
+ ---
24
+
25
+ ## Step 2: Download Service Account Key
26
+
27
+ The CLI gives you a direct link. In short:
28
+
29
+ 1. Firebase Console → Project Settings → Service Accounts
30
+ 2. Click "Generate new private key"
31
+ 3. Save the file as `service-account-key.json` in your app root
32
+ 4. This file is `.gitignored` — never commit it
33
+
34
+ **Why?** The service account key authenticates `dndev deploy` and Cloud Functions to your Firebase project. Without it, deployment fails.
35
+
36
+ ---
37
+
38
+ ## Step 3: Enable Firebase Services
39
+
40
+ Go to the Firebase Console and enable:
41
+
42
+ **Authentication** (required):
43
+ - Get Started → Enable Email/Password
44
+ - Add OAuth providers if needed (Google, GitHub, etc.)
45
+
46
+ **Cloud Firestore** (required):
47
+ - Create Database → Start in test mode → Select region (e.g., europe-west1)
48
+ - Test mode rules expire after 30 days — Phase 4 generates proper rules
49
+
50
+ **Storage** (optional — only if your app uploads files):
51
+ - Get Started → Select region
52
+
53
+ ---
54
+
55
+ ## Step 4: Test Locally
56
+
57
+ ```bash
58
+ dndev emu start
59
+ ```
60
+
61
+ Select services: Auth + Firestore + Functions. This starts Firebase emulators so you can develop without touching production.
62
+
63
+ **Verify:** Open the app (`bun dev`), sign up, create some data. Everything runs locally.
64
+
65
+ ---
66
+
67
+ ## Step 5: Deploy
68
+
69
+ ```bash
70
+ dndev deploy
71
+ ```
72
+
73
+ This handles everything:
74
+ - Builds the app
75
+ - Deploys hosting (your frontend)
76
+ - Deploys Cloud Functions (your backend)
77
+ - Configures Cloud Run IAM automatically (fixes CORS preflight on 2nd gen functions)
78
+ - Deploys Firestore rules (if `firestore.rules` exists)
79
+
80
+ **After deploy:** Your app is live at `https://<project-id>.web.app`.
81
+
82
+ ---
83
+
84
+ ## Environment Variables
85
+
86
+ **Vite loads `.env` from the app directory only. NOT from the repo root.**
87
+
88
+ | File | What Goes Here | Loaded By |
89
+ |------|---------------|-----------|
90
+ | `apps/<app>/.env` | Firebase config, license key, Stripe publishable key | Vite (at dev + build) |
91
+ | `apps/<app>/.env.local` | Local overrides (gitignored) | Vite (overrides .env) |
92
+ | `apps/<app>/.env.production` | Production overrides | Vite (at build --mode production) |
93
+ | `apps/<app>/.env.staging` | Staging config | Vite (via `dndev staging`) |
94
+ | `functions/.env` | Server secrets: STRIPE_SECRET_KEY, OAuth secrets | Cloud Functions runtime |
95
+ | Root `.env` | **Not read by Vite.** Reference only. | Nothing |
96
+
97
+ **`dndev firebase:setup` writes Firebase vars to `apps/<app>/.env` automatically.**
98
+
99
+ **Custom domains:** Framework uses `APP_URL` hostname as `authDomain` in production (not Firebase's `projectId.firebaseapp.com`). Copy/paste `FIREBASE_AUTH_DOMAIN` from Firebase Console in both `.env.local` and `.env.production` — framework handles the rest.
100
+
101
+ ---
102
+
103
+ ## Staging Environment (Optional)
104
+
105
+ 1. Create a second Firebase project (e.g., `my-app-staging`)
106
+ 2. Run `dndev firebase:setup` again and select the staging project
107
+ 3. Add to `.firebaserc`: `{ "projects": { "staging": "my-app-staging" } }`
108
+ 4. Create `service-account-key.staging.json` (same steps as production)
109
+ 5. Deploy: `dndev staging`
110
+
111
+ ---
112
+
113
+ ## Secrets (Stripe, OAuth, etc.)
114
+
115
+ Server-side secrets go in `functions/.env`, not the app `.env`:
116
+
117
+ ```bash
118
+ # functions/.env
119
+ STRIPE_SECRET_KEY=sk_live_...
120
+ STRIPE_WEBHOOK_SECRET=whsec_...
121
+ GITHUB_CLIENT_SECRET=...
122
+ ```
123
+
124
+ Push to Firebase Secret Manager:
125
+
126
+ ```bash
127
+ dndev sync-secrets
128
+ ```
129
+
130
+ Secrets are auto-loaded by Cloud Functions at runtime. Never put server secrets in `VITE_*` variables — those are exposed to the browser.
131
+
132
+ ---
133
+
134
+ ## Cloud Run IAM (Technical Detail)
135
+
136
+ Firebase 2nd gen Cloud Functions run on Cloud Run. By default, Cloud Run blocks unauthenticated OPTIONS requests (CORS preflight). This breaks browser calls to your functions.
137
+
138
+ `dndev deploy` automatically runs `gcloud run services update --no-invoker-iam-check` on all deployed functions. Your functions still validate Firebase Auth in code — this only allows the CORS preflight to pass.
139
+
140
+ If you deploy manually with `firebase deploy`, you'll need to run this yourself. Use `dndev deploy` instead.
141
+
142
+ ---
143
+
144
+ ## Troubleshooting
145
+
146
+ **"Service account key not found"**
147
+ → Download from Firebase Console → Service Accounts → Generate new private key
148
+ → Save as `service-account-key.json` in your app root
149
+
150
+ **"Permission denied" / 401 on deploy**
151
+ → Check service account key is valid JSON with `project_id`, `private_key`, `client_email`
152
+ → Re-download if corrupted
153
+
154
+ **"CORS error" on function calls**
155
+ → Use `dndev deploy` instead of `firebase deploy` (handles IAM automatically)
156
+ → Or run: `gcloud run services update <function-name> --region=<region> --no-invoker-iam-check --project=<project-id>`
157
+
158
+ **".env values not loading"**
159
+ → Check the `.env` file is in your **app directory** (`apps/<app>/.env`), not the repo root
160
+ → All Vite vars must start with `VITE_`
161
+
162
+ **"License key not working"**
163
+ → Must be in `apps/<app>/.env` as `VITE_DONOTDEV_LICENSE_KEY=dndev_...`
164
+ → Must start with `dndev_` and be at least 20 characters
165
+
166
+ ---
167
+
168
+ **`dndev firebase:setup` → download service account key → enable Auth + Firestore → `dndev emu start` → `dndev deploy`. That's it.**