@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.
- package/dependencies-matrix.json +32 -118
- package/dist/bin/commands/agent-setup.d.ts +6 -0
- package/dist/bin/commands/agent-setup.d.ts.map +1 -0
- package/dist/bin/commands/agent-setup.js +623 -0
- package/dist/bin/commands/agent-setup.js.map +1 -0
- package/dist/bin/commands/build.js +13 -12
- package/dist/bin/commands/bump.js +103 -35
- package/dist/bin/commands/cacheout.js +13 -12
- package/dist/bin/commands/create-app.js +53 -151
- package/dist/bin/commands/create-project.js +109 -167
- package/dist/bin/commands/deploy.js +7620 -30
- package/dist/bin/commands/dev.js +13 -12
- package/dist/bin/commands/emu.js +13 -12
- package/dist/bin/commands/firebase-setup.d.ts +6 -0
- package/dist/bin/commands/firebase-setup.d.ts.map +1 -0
- package/dist/bin/commands/firebase-setup.js +7 -0
- package/dist/bin/commands/firebase-setup.js.map +1 -0
- package/dist/bin/commands/format.js +13 -12
- package/dist/bin/commands/lint.js +13 -12
- package/dist/bin/commands/preview.js +13 -12
- package/dist/bin/commands/staging.d.ts +11 -0
- package/dist/bin/commands/staging.d.ts.map +1 -0
- package/dist/bin/commands/staging.js +12 -0
- package/dist/bin/commands/staging.js.map +1 -0
- package/dist/bin/commands/sync-secrets.js +13 -12
- package/dist/bin/commands/wai.js +7397 -11
- package/dist/bin/dndev.js +28 -3
- package/dist/bin/donotdev.js +28 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7760 -109
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/app-demo/src/pages/DetailPage.tsx.example +1 -1
- package/templates/app-demo/src/pages/FullPage.tsx.example +3 -3
- package/templates/app-demo/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +5 -5
- package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +3 -3
- package/templates/app-next/.env.example +2 -0
- package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-vite/.env.example +2 -0
- package/templates/app-vite/src/pages/HomePage.tsx.example +163 -73
- package/templates/functions-firebase/build.mjs.example +26 -10
- package/templates/functions-firebase/functions-firebase/build.mjs.example +26 -10
- package/templates/functions-firebase/functions.config.js.example +11 -15
- package/templates/github-consumer/.github/workflows/ci.yml.example +36 -0
- package/templates/root-consumer/.claude/agents/architect.md.example +2 -2
- package/templates/root-consumer/.claude/agents/builder.md.example +2 -2
- package/templates/root-consumer/.claude/agents/coder.md.example +2 -2
- package/templates/root-consumer/.claude/agents/extractor.md.example +2 -3
- package/templates/root-consumer/.claude/agents/polisher.md.example +67 -291
- package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +4 -4
- package/templates/root-consumer/.claude/commands/brainstorm.md.example +1 -1
- package/templates/root-consumer/.claude/commands/build.md.example +3 -3
- package/templates/root-consumer/.claude/commands/design.md.example +1 -1
- package/templates/root-consumer/.claude/commands/polish.md.example +66 -82
- package/templates/root-consumer/.dndev/args.json.example +6 -0
- package/templates/root-consumer/.env.example +13 -13
- package/templates/root-consumer/.gemini/settings.json.example +9 -0
- package/templates/root-consumer/.gitignore.example +3 -1
- package/templates/root-consumer/AI.md.example +150 -0
- package/templates/root-consumer/CLAUDE.md.example +19 -104
- package/templates/root-consumer/README.md.example +81 -255
- package/templates/root-consumer/entities/Contact.ts.example +126 -0
- package/templates/root-consumer/entities/index.ts.example +6 -3
- package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +59 -326
- package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +2 -1
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +144 -9
- package/templates/root-consumer/guides/dndev/GOTCHAS.md.example +186 -0
- package/templates/root-consumer/guides/dndev/INDEX.md.example +10 -0
- package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +13 -16
- package/templates/root-consumer/guides/dndev/SETUP_BLOG.md.example +263 -0
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +1 -1
- package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +168 -0
- package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +17 -19
- package/templates/root-consumer/guides/dndev/SETUP_TESTING.md.example +184 -0
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +134 -69
- package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +66 -44
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +18 -1
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +1 -0
- package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +2 -1
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +2 -1
- package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +180 -108
- package/templates/root-consumer/guides/wai-way/context_map.json.example +8 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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: 
|
|
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
|
+

|
|
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"
|
|
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.**
|