saasaloy 0.5.2 → 0.6.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.
- package/README.md +7 -7
- package/dist/index.js +637 -522
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/registry-item.schema.json +31 -1
- package/templates/base/AGENTS.md +204 -89
- package/templates/base/README.md +20 -8
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +38 -17
- package/templates/base/_agents/skills/saasaloy-env/SKILL.md +130 -0
- package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +63 -46
- package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +1 -1
- package/templates/base/_gitignore +0 -3
- package/templates/base/_saasaloy-base.json +8 -1
- package/templates/base/{DESIGN.md → apps/web/DESIGN.md} +36 -12
- package/templates/base/apps/web/astro.config.mjs +36 -6
- package/templates/base/apps/web/package.json +2 -1
- package/templates/base/apps/web/src/env.ts +41 -0
- package/templates/base/apps/web/src/layouts/Layout.astro +39 -11
- package/templates/base/apps/web/src/pages/index.astro +33 -24
- package/templates/base/apps/web/src/pages/privacy.astro +10 -2
- package/templates/base/apps/web/src/pages/terms.astro +10 -2
- package/templates/base/oxlint.config.mjs +125 -4
- package/templates/base/package.json +7 -3
- package/templates/base/packages/env/_env.example +54 -0
- package/templates/base/packages/env/package.json +35 -0
- package/templates/base/packages/env/scripts/env-setup.ts +87 -0
- package/templates/base/packages/env/src/check.test.ts +41 -0
- package/templates/base/packages/env/src/check.ts +50 -0
- package/templates/base/packages/env/src/define.test.ts +206 -0
- package/templates/base/packages/env/src/define.ts +315 -0
- package/templates/base/packages/env/src/deploy.test.ts +438 -0
- package/templates/base/packages/env/src/deploy.ts +518 -0
- package/templates/base/packages/env/src/dotenv.test.ts +179 -0
- package/templates/base/packages/env/src/dotenv.ts +173 -0
- package/templates/base/packages/env/src/env-file.ts +51 -0
- package/templates/base/packages/env/src/error.ts +38 -0
- package/templates/base/packages/env/src/example.test.ts +86 -0
- package/templates/base/packages/env/src/example.ts +90 -0
- package/templates/base/packages/env/src/index.ts +15 -0
- package/templates/base/packages/env/src/schema.ts +63 -0
- package/templates/base/packages/env/src/services.test.ts +83 -0
- package/templates/base/packages/env/src/services.ts +110 -0
- package/templates/base/packages/env/src/setup.test.ts +254 -0
- package/templates/base/packages/env/src/setup.ts +177 -0
- package/templates/base/packages/env/src/source.ts +20 -0
- package/templates/base/packages/env/src/sources.test.ts +137 -0
- package/templates/base/packages/env/src/sources.ts +93 -0
- package/templates/base/packages/env/src/zod.ts +8 -0
- package/templates/base/packages/env/tsconfig.json +13 -0
- package/templates/base/packages/tsconfig/base.json +7 -0
- package/templates/base/packages/ui/components.json +1 -1
- package/templates/base/packages/ui/package.json +4 -2
- package/templates/base/packages/ui/src/blocks/audience.tsx +73 -0
- package/templates/base/packages/ui/src/blocks/cta.tsx +41 -30
- package/templates/base/packages/ui/src/blocks/error-state.tsx +2 -0
- package/templates/base/packages/ui/src/blocks/faq.tsx +64 -36
- package/templates/base/packages/ui/src/blocks/feature-grid.tsx +48 -146
- package/templates/base/packages/ui/src/blocks/footer.tsx +129 -70
- package/templates/base/packages/ui/src/blocks/hero.tsx +183 -26
- package/templates/base/packages/ui/src/blocks/how-it-works.tsx +75 -0
- package/templates/base/packages/ui/src/blocks/logo-cloud.tsx +118 -0
- package/templates/base/packages/ui/src/blocks/navbar.tsx +204 -88
- package/templates/base/packages/ui/src/blocks/pain-points.tsx +131 -0
- package/templates/base/packages/ui/src/blocks/pricing-table.tsx +112 -94
- package/templates/base/packages/ui/src/blocks/showcase.tsx +185 -0
- package/templates/base/packages/ui/src/blocks/testimonials.tsx +85 -0
- package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +16 -12
- package/templates/base/packages/ui/src/components/badge.tsx +1 -1
- package/templates/base/packages/ui/src/components/button.tsx +2 -2
- package/templates/base/packages/ui/src/components/tabs.tsx +1 -1
- package/templates/base/packages/ui/src/containers/README.md +6 -5
- package/templates/base/packages/ui/src/content/landing.ts +590 -125
- package/templates/base/packages/ui/src/index.ts +6 -2
- package/templates/base/packages/ui/src/lib/external-link.ts +36 -0
- package/templates/base/packages/ui/src/lib/icons.ts +131 -0
- package/templates/base/packages/ui/src/lib/inline-code.ts +29 -0
- package/templates/base/packages/ui/src/lib/theme.ts +140 -56
- package/templates/base/packages/ui/src/styles/globals.css +47 -125
- package/templates/base/packages/ui/src/styles/web.css +359 -0
- package/templates/base/pnpm-workspace.yaml +0 -6
- package/templates/base/saasaloy.json +1 -0
- package/templates/base/stylelint.config.js +3 -3
- package/templates/base/turbo.json +13 -0
- package/templates/base/vitest.config.ts +21 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: saasaloy-design
|
|
3
|
-
description: Keep DESIGN.md true of the Saasaloy design layer. Use theme to choose and apply a registry style, update after packages/ui changes, or audit
|
|
3
|
+
description: Keep apps/web/DESIGN.md, and apps/admin/DESIGN.md when the admin app is installed, true of the Saasaloy design layer. Use theme to choose and apply a registry style to the web app, update after packages/ui changes, or audit a design contract for token drift.
|
|
4
4
|
derived-from: "designkit (MIT), narrowed 2026-08-09"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,7 +8,16 @@ derived-from: "designkit (MIT), narrowed 2026-08-09"
|
|
|
8
8
|
|
|
9
9
|
This skill records the design system that the project already uses. It writes no components and no pages. Use `uikit` for that work.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Each app has its own token home and its own contract:
|
|
12
|
+
|
|
13
|
+
| App | Token home | Contract |
|
|
14
|
+
|-----|------------|----------|
|
|
15
|
+
| web | `packages/ui/src/styles/web.css` | `apps/web/DESIGN.md` |
|
|
16
|
+
| admin, when installed | `packages/ui/src/styles/admin.css` | `apps/admin/DESIGN.md` |
|
|
17
|
+
|
|
18
|
+
Both theme files import `packages/ui/src/styles/globals.css`, which holds the shared Tailwind setup and no token values. Ask which app when the user does not say, and work on one contract at a time. Never read one app's tokens into the other app's contract.
|
|
19
|
+
|
|
20
|
+
The admin contract also covers the admin primitive set in `packages/ui/src/components/admin/`, its blocks in `packages/ui/src/blocks/admin/` and its containers in `packages/ui/src/containers/admin/`. The web contract covers the base `components/`, the other `blocks/` and the web app.
|
|
12
21
|
|
|
13
22
|
The grounding rule controls every mode. Every token value must exist in the project code or in the Tailwind utility that the project uses. Do not choose a nearby value because it looks cleaner.
|
|
14
23
|
|
|
@@ -24,15 +33,17 @@ If the user does not name a mode, ask which mode to use. Do not choose between a
|
|
|
24
33
|
|
|
25
34
|
| Mode | Files |
|
|
26
35
|
|------|-------|
|
|
27
|
-
| `theme` | `packages/ui/src/styles/
|
|
28
|
-
| `update` | `DESIGN.md` only |
|
|
36
|
+
| `theme` | web only: `packages/ui/src/styles/web.css`, `apps/web/DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
|
|
37
|
+
| `update` | the chosen app's `DESIGN.md` only |
|
|
29
38
|
| `audit` | none |
|
|
30
39
|
|
|
40
|
+
`theme` never runs for the admin app. A preset targets `web.css` through `components.json`, and nothing writes to `admin.css` but a person. When the user asks to restyle the admin app, say so, and offer `update` after they edit `admin.css`.
|
|
41
|
+
|
|
31
42
|
Do not edit `components.json` during a theme change. Do not add a dependency, script, component, page, or Tailwind class.
|
|
32
43
|
|
|
33
44
|
## Shared rules
|
|
34
45
|
|
|
35
|
-
Read `DESIGN.md
|
|
46
|
+
Read the chosen app's `DESIGN.md` and theme file, `packages/ui/src/styles/globals.css`, and the files under `packages/ui/src/` that the app renders before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
|
|
36
47
|
|
|
37
48
|
Find the system in this order. Stop at the first match.
|
|
38
49
|
|
|
@@ -50,42 +61,52 @@ Write elevation as prose under `## Elevation & Depth`. Never add `elevation` to
|
|
|
50
61
|
|
|
51
62
|
Write motion as prose under `## Motion`. The alpha schema has no motion token group and no component transition token.
|
|
52
63
|
|
|
64
|
+
The landing page's layout, type and motion vocabulary lives at the end of `web.css`, after the colour tokens, and `DESIGN.md` must name all of it:
|
|
65
|
+
|
|
66
|
+
- The type ramp in `@theme`: `--text-display`, `--text-section-title` and `--text-lead`. Record the largest size of each fluid `clamp()` step as the token's `fontSize`, and describe the fluid range in the Typography prose.
|
|
67
|
+
- The container widths in `@theme` (`--container-page`, `--container-narrow`, `--container-wide`) and the `section-container*` and `section-y` utilities. Describe them under `## Layout`. Spacing stays omitted, because these are named widths and one section rhythm, not a spacing scale.
|
|
68
|
+
- The motion utilities: `blur-in`, `beam-in`, `reveal`, `marquee`, `shine-text`, `nav-float` and `hover-lift`. Describe each under `## Motion`, and say that each one stops under reduced motion.
|
|
69
|
+
- The decoration utility `grid-pattern`. Describe it under `## Components` with the landing blocks it decorates.
|
|
70
|
+
|
|
71
|
+
A theme preset replaces the colour tokens and leaves this vocabulary alone. After a `theme` run, check that the vocabulary survived and that `DESIGN.md` still describes it.
|
|
72
|
+
|
|
53
73
|
Use only these component token keys: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, and `width`.
|
|
54
74
|
|
|
55
75
|
Keep the sections in this order: Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, Motion, Dark Mode.
|
|
56
76
|
|
|
57
|
-
The
|
|
77
|
+
The web contract is a one-time base seed. ADR 0022 means Saasaloy has no update path for it. The admin contract is an admin module file, so `saasaloy update` lists an edited copy in its merge plan. In both cases the fingerprint detects drift, and this skill repairs the contract.
|
|
58
78
|
|
|
59
79
|
## Fingerprint
|
|
60
80
|
|
|
61
|
-
The fingerprint covers only `packages/ui/src/styles/
|
|
81
|
+
The fingerprint covers only the chosen app's theme file: `packages/ui/src/styles/web.css` for the web contract, `packages/ui/src/styles/admin.css` for the admin contract. It does not cover `globals.css`, components, blocks, or other files. The commands and stamps below name `web.css`; substitute `admin.css` for the admin contract.
|
|
62
82
|
|
|
63
83
|
Compute the fingerprint from the file bytes. Use the first 12 lowercase hexadecimal characters of SHA-256.
|
|
64
84
|
|
|
65
85
|
```sh
|
|
66
|
-
node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/
|
|
86
|
+
node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/web.css')).digest('hex').slice(0, 12))"
|
|
67
87
|
```
|
|
68
88
|
|
|
69
89
|
The scaffold seed ends with this stamp.
|
|
70
90
|
|
|
71
91
|
```markdown
|
|
72
|
-
_Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/
|
|
92
|
+
_Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
|
|
73
93
|
```
|
|
74
94
|
|
|
75
95
|
After `theme` or `update`, replace the seed stamp with this stamp.
|
|
76
96
|
|
|
77
97
|
```markdown
|
|
78
|
-
_Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/
|
|
98
|
+
_Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
|
|
79
99
|
```
|
|
80
100
|
|
|
81
101
|
Do not change the fingerprint during `audit`.
|
|
82
102
|
|
|
83
103
|
## Official linter
|
|
84
104
|
|
|
85
|
-
Run the official linter after each write.
|
|
105
|
+
Run the official linter after each write, on the contract you wrote.
|
|
86
106
|
|
|
87
107
|
```sh
|
|
88
|
-
pnpm dlx @google/design.md lint DESIGN.md
|
|
108
|
+
pnpm dlx @google/design.md lint apps/web/DESIGN.md
|
|
109
|
+
pnpm dlx @google/design.md lint apps/admin/DESIGN.md
|
|
89
110
|
```
|
|
90
111
|
|
|
91
112
|
The format is alpha. Read the current schema when a lint finding conflicts with this skill.
|
|
@@ -126,13 +147,13 @@ Use the package-local executable from the project root.
|
|
|
126
147
|
pnpm --filter @repo/ui exec shadcn add <registry-style-url>
|
|
127
148
|
```
|
|
128
149
|
|
|
129
|
-
Confirm that the `@source` rules
|
|
150
|
+
`components.json` points shadcn at `web.css`, so the preset merges into the `:root`, `.dark` and `@theme inline` blocks there. Confirm that `globals.css` did not change and still holds the `@source` rules and `@layer base`. Confirm that `web.css` and `globals.css` hold one `@theme inline` and one `@custom-variant dark` between them. Confirm that `components.json` did not change.
|
|
130
151
|
|
|
131
152
|
Stop and report the failure when the preset removes a required rule or changes `components.json`. Do not repair an unknown preset merge without approval.
|
|
132
153
|
|
|
133
154
|
### 4. Re-derive the contract
|
|
134
155
|
|
|
135
|
-
Run the shared extraction process against the merged `
|
|
156
|
+
Run the shared extraction process against the merged `web.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
|
|
136
157
|
|
|
137
158
|
Use the product brief to update Overview and Do's and Don'ts. Do not add product claims that the brief does not support.
|
|
138
159
|
|
|
@@ -168,7 +189,7 @@ Audit is read-only. Do not edit or format any file.
|
|
|
168
189
|
|
|
169
190
|
### 1. Check the fingerprint offline
|
|
170
191
|
|
|
171
|
-
Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from `
|
|
192
|
+
Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from the chosen app's theme file, `web.css` or `admin.css`.
|
|
172
193
|
|
|
173
194
|
- `current` means the two fingerprints match.
|
|
174
195
|
- `stale` means they differ.
|
|
@@ -182,7 +203,7 @@ Use `orphaned` for a documented token with no current source. Use `uncovered` fo
|
|
|
182
203
|
|
|
183
204
|
### 3. Lint when possible
|
|
184
205
|
|
|
185
|
-
Run `pnpm dlx @google/design.md lint DESIGN.md` when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
|
|
206
|
+
Run `pnpm dlx @google/design.md lint apps/<app>/DESIGN.md` for the chosen app when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
|
|
186
207
|
|
|
187
208
|
### 4. Report
|
|
188
209
|
|
|
@@ -192,7 +213,7 @@ Open with the file count that the audit scanned. Give one verdict for the finger
|
|
|
192
213
|
|
|
193
214
|
- Never write a component or a page. Use `uikit` for that work.
|
|
194
215
|
- Never add a token value that the project does not contain.
|
|
195
|
-
- Never change `globals.css` in `update` or `audit`.
|
|
216
|
+
- Never change `globals.css` in any mode, or `web.css` in `update` or `audit`.
|
|
196
217
|
- Never write during `audit`.
|
|
197
218
|
- Never claim that lint passed when the linter did not run.
|
|
198
219
|
- Never treat a matching fingerprint as proof that component usage has no drift.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saasaloy-env
|
|
3
|
+
description: Runbook for the env capability in packages/env — the one tracked key list, the pnpm env:setup distribution command, the pnpm deploy:production deploy with env-deploy, and the typed createEnv gate each service reads its values through. Use when deploying to Cloudflare, when a deploy stops naming a blank production key, adding an environment key, when a build or a request fails naming an unset key, when choosing which services read a key, when a value must stay blank, when a key is checkout-owned, when picking a value source, or when a leftover .dev.vars is hiding .env from wrangler.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# env — one key list, one command, one typed gate
|
|
7
|
+
|
|
8
|
+
`packages/env` (`@repo/env`) owns both halves of this project's environment story.
|
|
9
|
+
|
|
10
|
+
The **node half** is a tracked key list (`packages/env/.env.example`), a gitignored value file (`packages/env/.env`), and `pnpm env:setup`, which writes every service's own `.env` from the two. The **runtime half** is `createEnv`, which validates the values a service actually got and hands back typed ones.
|
|
11
|
+
|
|
12
|
+
A *service* is a workspace that reads a `.env`: `api`, `web`, `admin`. The name is the workspace directory. `packages/env/src/services.ts` holds the table, and `saasaloy add api` appends a row to it. One row has no workspace and no file: `deploy` holds `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`, which a deploy loads in memory.
|
|
13
|
+
|
|
14
|
+
## Adding a key, in two steps
|
|
15
|
+
|
|
16
|
+
**1. Add its line to `packages/env/.env.example`**, under the `# @services` section naming every service that reads it. Write a comment above it saying where the value comes from and what unset looks like. Put the local development default on the line.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
# @services api
|
|
20
|
+
|
|
21
|
+
# Sender address every outbound message uses. The domain must be one the selected
|
|
22
|
+
# provider may send from. Unset, the first send throws naming this key.
|
|
23
|
+
EMAIL_FROM=dev@localhost
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A key with no `# @services` line above it fails `pnpm env:setup`. That is deliberate: a key that reaches no service is a key the Worker throws on, and failing here is the cheaper place to find out.
|
|
27
|
+
|
|
28
|
+
**2. Declare it in the preset** of the package that reads it, so `createEnv` validates it and the type reaches the calling code.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// packages/email/src/env-preset.ts
|
|
32
|
+
export const emailPreset = definePreset({
|
|
33
|
+
module: "@repo/email",
|
|
34
|
+
extends: [],
|
|
35
|
+
server: {
|
|
36
|
+
EMAIL_FROM: z.string().min(1).describe("Sender address every message uses."),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `.describe()` text is what the aggregate error prints, so write it for the person reading a failed deploy.
|
|
42
|
+
|
|
43
|
+
## Reading a key
|
|
44
|
+
|
|
45
|
+
On the server, pass the Worker's `env`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { apiEnv } from "@api/env";
|
|
49
|
+
|
|
50
|
+
app.get("/x", (c) => {
|
|
51
|
+
const { EMAIL_FROM } = apiEnv(c.env);
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In a frontend, call it with nothing — the values are inlined at build time:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { webEnv } from "@web/env";
|
|
59
|
+
|
|
60
|
+
const base = webEnv().PUBLIC_API_URL;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`createEnv` validates on the **first call per `env` object** and memoizes the result in a `WeakMap`. A Worker cannot see its `env` at module scope, so the first request is the earliest honest point to check. One throw then names every failing key at once, with the module that declared it and that module's own wording, so a deploy with three unset keys takes one round trip to fix.
|
|
64
|
+
|
|
65
|
+
## Server, client, shared
|
|
66
|
+
|
|
67
|
+
- `server` — read only where `isServer` is true. A Worker, a build script.
|
|
68
|
+
- `client` — inlined into a public bundle. **Must** start with `PUBLIC_`, and the type system says so: a mis-prefixed key does not compile.
|
|
69
|
+
- `shared` — read on both sides.
|
|
70
|
+
|
|
71
|
+
`apps/web/src/env.ts` and `apps/admin/src/env.ts` set `isServer: false`, which arms a Proxy: reading a server key from client code throws by name. `typeof window === "undefined"` is not used, because it is true in a Worker *and* in a browser bundle before hydration.
|
|
72
|
+
|
|
73
|
+
The `runtimeEnvStrict` map in those files is hand-written, one literal `import.meta.env.PUBLIC_X` per key. Vite inlines a literal member expression and nothing else, so a spread, a loop or a computed key compiles to `undefined` in the built bundle and fails only in production. **Add the line by hand when you add a client key.**
|
|
74
|
+
|
|
75
|
+
## Blank on purpose
|
|
76
|
+
|
|
77
|
+
`pnpm env:setup` fails on a key the list carries with no value. When a key is meant to stay empty, say so in its comment block:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
# Blank on purpose: unset means no proxy origin, and a link is root-relative.
|
|
81
|
+
STORAGE_PROXY_URL=
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Checkout-owned keys
|
|
85
|
+
|
|
86
|
+
`CHECKOUT_OWNED_KEYS` in `packages/env/src/services.ts` lists keys this checkout sets for itself. A key named there keeps whatever value is already on disk, whoever wrote it, so a per-branch `DATABASE_URL` survives every `pnpm env:setup`. `packages/env` never learns what a database is — only that some key is the checkout's to set.
|
|
87
|
+
|
|
88
|
+
## Value sources
|
|
89
|
+
|
|
90
|
+
`pnpm env:setup` reads its values from a source, selected by `ENV_SOURCE`:
|
|
91
|
+
|
|
92
|
+
- **`local`** (the default, always installed) reads the gitignored `packages/env/.env`. `saasaloy env` prompts for each unset key and writes that file, so a project develops with no vendor account and no network.
|
|
93
|
+
- **`infisical`** arrives with `saasaloy add env-infisical`.
|
|
94
|
+
|
|
95
|
+
A source that does not answer at all — no CLI, no network — is treated as offline: `env:setup` keeps every service file that is already complete and warns, and fails writing nothing when one is incomplete. A refusal the source *answered* with, such as no access, always fails.
|
|
96
|
+
|
|
97
|
+
## Production and `--env prod`
|
|
98
|
+
|
|
99
|
+
Each service row declares `dev` or `prod`. `pnpm env:setup --env prod` refuses to act without `--only <service>`, and refuses outright to write production values into a workspace under `apps/`. Two independent guards on the one command that can put a live credential on disk.
|
|
100
|
+
|
|
101
|
+
`saasaloy env` prints the `wrangler secret put` lines for a deploy. It never runs them, and `pnpm deploy:production` makes them unnecessary.
|
|
102
|
+
|
|
103
|
+
## Deploying: `pnpm deploy:production` and `env-deploy`
|
|
104
|
+
|
|
105
|
+
`pnpm deploy:production` deploys every Worker in the project with wrangler. It runs in two steps:
|
|
106
|
+
|
|
107
|
+
1. `node packages/env/src/deploy.ts --check` loads every service's `prod` values from the selected source and exits non-zero, naming each blank key, when one is missing. It also stops when `CLOUDFLARE_API_TOKEN` is blank and wrangler holds no `wrangler login` session. Nothing builds before this passes.
|
|
108
|
+
2. `turbo run deploy` runs each workspace's `deploy` script, which is `env-deploy <service>`. A workspace whose package depends on `@repo/db` waits for `db:migrate:prod` first.
|
|
109
|
+
|
|
110
|
+
`env-deploy <service>` loads that service's `prod` values and the `deploy` credentials, runs `pnpm run build` with them in the environment, and runs `wrangler deploy --secrets-file <file>`. Code and secrets land in one Worker version, so a Worker that throws at module scope on a missing secret deploys cleanly the first time. The secrets file is JSON, mode `0600`, under the OS temp directory, and deleted when the deploy ends, passed or failed. It holds the service's values minus every `CLOUDFLARE_*` key, minus each key the Worker's `wrangler.jsonc` already sets in `vars`, minus empty values. An assets-only Worker, one with no `main`, gets no secrets file.
|
|
111
|
+
|
|
112
|
+
**Where the production values come from.** The `local` source reads the gitignored `packages/env/.env.prod`, never `packages/env/.env`, so a deploy cannot ship a `localhost` URL by accident. Copy the keys from `packages/env/.env.example` into it and set each one. `env-infisical` reads its `prod` environment. A variable the shell already exports wins over both, which is how CI passes values. The base `turbo.json` sets `passThroughEnv` on `deploy`, because turbo's strict env mode would drop the exported variables otherwise.
|
|
113
|
+
|
|
114
|
+
**Why the dev `.env` stays out of the build.** Vite gives the environment priority over a `.env` file, but the Cloudflare adapters prerender inside workerd, and wrangler hands workerd the `.env` beside the workspace as its vars. So `env-deploy` builds with `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV=false`, and it sets a key the source leaves out to an empty string rather than leaving it unset. `apps/web/.env` keeps its dev values, and nothing writes a production value under `apps/`.
|
|
115
|
+
|
|
116
|
+
Three more forms:
|
|
117
|
+
|
|
118
|
+
- `pnpm --filter @repo/api run deploy` deploys one Worker. Pass wrangler flags after `--`: `pnpm --filter @repo/web run deploy -- --dry-run` builds with the production values and uploads nothing.
|
|
119
|
+
- `env-deploy --run <service> -- <command> [args...]` runs one command with that service's `prod` values and the credentials. `database-d1`'s `db:migrate:prod` uses it.
|
|
120
|
+
- `env-deploy --check` is step 1 on its own.
|
|
121
|
+
|
|
122
|
+
Two things the flow does not do. `--secrets-file` only adds: a secret you removed from the value source stays on the Worker until you run `wrangler secret delete <KEY>` in its workspace. And a key marked `# Blank on purpose` passes the check even where production needs it, such as `BETTER_AUTH_SECRET`; the Worker's own `createEnv` gate is what refuses it.
|
|
123
|
+
|
|
124
|
+
## `.dev.vars` is gone
|
|
125
|
+
|
|
126
|
+
This project writes `.env` and nothing else. A leftover `.dev.vars` **hides** `.env` from wrangler — wrangler loads `.env` only while no `.dev.vars` sits beside it, and it does not warn — so `saasaloy doctor` reports one and `pnpm env:setup` carries its values across and deletes it.
|
|
127
|
+
|
|
128
|
+
## `process.env`
|
|
129
|
+
|
|
130
|
+
No file a Worker imports reads it. The Worker's `env` is the only source, and it arrives through `createEnv`. Node tooling — `scripts/`, `env-deploy`, a value source — reads it freely.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: saasaloy-landing-copy
|
|
3
|
-
description: Write the scaffolded landing page's copy from docs/product-brief.md — as a markdown draft the owner reviews first, then into the content module. Use when the landing page still
|
|
3
|
+
description: Write the scaffolded landing page's copy from docs/product-brief.md — as a markdown draft the owner reviews first, then into the content module. Use when the landing page still describes Saasaloy ("Ship the SaaS, not the scaffolding"), when the owner asks to write, rewrite, improve or update the landing copy, headline, features, pricing or FAQ, and when re-running after the brief changed. Needs docs/product-brief.md; run saasaloy-setup first if there isn't one.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# saasaloy-landing-copy — draft it in markdown, then write the page
|
|
@@ -41,12 +41,12 @@ says what to do when they disagree with the brief.
|
|
|
41
41
|
an owner ends up answering the same thing twice and getting two different pages.
|
|
42
42
|
2. **Read `packages/ui/src/content/landing.ts`.** You need the current copy to diff against,
|
|
43
43
|
and its comments carry the shape rules you have to keep.
|
|
44
|
-
3. **Read the icon registry**
|
|
44
|
+
3. **Read the icon registry** in `packages/ui/src/lib/icons.ts`.
|
|
45
45
|
Reading a block is fine; editing one is not. Its keys are the whole vocabulary available
|
|
46
46
|
to `landing.features.items[].icon`, and they are the actual list rather than one copied
|
|
47
47
|
into this file that went stale.
|
|
48
48
|
4. **Check the two facts you do not own.** If `siteName` in `packages/ui/src/index.ts` is
|
|
49
|
-
still the
|
|
49
|
+
still the shipped `"Saasaloy"` or a directory slug, or `lang` in `apps/web/src/layouts/Layout.astro` is
|
|
50
50
|
`en` while the brief names another language, say so now and carry both into the draft's
|
|
51
51
|
**Not mine to fix** list. Writing Bangla copy into a page that declares itself English is
|
|
52
52
|
a real defect, and one attribute long. Do not fix it yourself.
|
|
@@ -113,29 +113,29 @@ This file is deleted once the copy lands in `packages/ui/src/content/landing.ts`
|
|
|
113
113
|
## Hero
|
|
114
114
|
| Key | Now | Proposed |
|
|
115
115
|
|-----|-----|----------|
|
|
116
|
-
| `hero.
|
|
116
|
+
| `hero.announcement` | See what's new | In beta with two firms |
|
|
117
117
|
| `hero.primaryActionLabel` | Get started | Join the beta |
|
|
118
118
|
|
|
119
119
|
`hero.title`:
|
|
120
|
-
> ~~
|
|
120
|
+
> ~~Ship the SaaS, not the scaffolding.~~
|
|
121
121
|
> **Close the month without four spreadsheets per client.**
|
|
122
122
|
|
|
123
123
|
*From the brief: the problem line, and "Excel plus a shared Dropbox folder" as the
|
|
124
|
-
alternative. The
|
|
124
|
+
alternative. The announcement uses the two named beta firms rather than a stage name.*
|
|
125
125
|
|
|
126
126
|
## Features
|
|
127
127
|
| # | Icon | Title | About |
|
|
128
128
|
|---|------|-------|-------|
|
|
129
|
-
| 1 | `list-checks` (was `
|
|
129
|
+
| 1 | `list-checks` (was `terminal`) | One close, one checklist | … |
|
|
130
130
|
|
|
131
131
|
## Where the buttons go
|
|
132
132
|
| Key | Now | Proposed |
|
|
133
133
|
|-----|-----|----------|
|
|
134
|
-
| `navbar.ctaHref` |
|
|
135
|
-
| `cta.primaryActionHref` |
|
|
134
|
+
| `navbar.ctaHref` | Saasaloy's getting started guide | https://ledgerly.com.bd/waitlist |
|
|
135
|
+
| `cta.primaryActionHref` | Saasaloy's getting started guide | https://ledgerly.com.bd/waitlist |
|
|
136
136
|
|
|
137
137
|
## Least sure about
|
|
138
|
-
- The
|
|
138
|
+
- The announcement names your two beta firms. Fine to say publicly?
|
|
139
139
|
- FAQ 4 says data exports as CSV. The brief doesn't mention exports; I inferred it.
|
|
140
140
|
|
|
141
141
|
## Not mine to fix
|
|
@@ -170,57 +170,70 @@ than doubling it.
|
|
|
170
170
|
|
|
171
171
|
| Key | What goes there |
|
|
172
172
|
|-----|-----------------|
|
|
173
|
-
| `meta.title` | Browser tab. `{siteName}
|
|
173
|
+
| `meta.title` | Browser tab and link preview. `{siteName}: <what it does>`, under 60 characters. |
|
|
174
174
|
| `meta.description` | Search result and link preview. One sentence, under 155 characters. |
|
|
175
|
-
| `navbar.linkFeatures` `.linkPricing` `.linkFaq` | Nav labels. One or two words. They point at the sections of the same name — keep the meaning, or the anchor lies. |
|
|
175
|
+
| `navbar.linkFeatures` `.linkHowItWorks` `.linkShowcase` `.linkPricing` `.linkFaq` | Nav labels. One or two words. They point at the sections of the same name — keep the meaning, or the anchor lies. An empty label hides the link. |
|
|
176
|
+
| `navbar.secondaryLabel` `.secondaryHref` | The outline button beside the main one, usually docs, source or sign-in. Empty `secondaryHref` hides it. See [Destinations](#destinations). |
|
|
176
177
|
| `navbar.ctaLabel` `.ctaHref` | Two or three words, a verb first, and where it goes. See [Destinations](#destinations). |
|
|
177
|
-
| `hero.
|
|
178
|
+
| `hero.announcement` `.announcementHref` | The pill above the headline ("Now in early access") and where it links, usually a changelog or launch post. Empty `announcement` hides it. |
|
|
178
179
|
| `hero.title` | The headline. Under ten words, one idea, concrete. This is the sentence the whole page is judged on. |
|
|
179
180
|
| `hero.description` | One or two sentences saying what it does for whom. |
|
|
180
|
-
| `hero.primaryActionLabel` `.secondaryActionLabel` | Button labels, a verb first
|
|
181
|
+
| `hero.primaryActionLabel` `.primaryActionHref` `.secondaryActionLabel` `.secondaryActionHref` | Button labels, a verb first, and where each goes. See [Destinations](#destinations). |
|
|
182
|
+
| `hero.terminalLabel` `.terminalTitle` `.terminalLines[]` `.treeTitle` `.tree[]` | The hero's product visual: a terminal and a file tree. For a product that is not a developer tool, keep the shipped lines and tell the owner in the draft to replace the visual with a screenshot (the block's header comment shows how). Command lines and paths are code, not prose. |
|
|
183
|
+
| `logos.label` `.items[]` | One line beside the logo row, and `{ id, name, mark }` per logo. `name` is the accessible label; `mark` names an SVG the owner adds to `logo-cloud.tsx`, and an empty `mark` renders `name` as text. Never invent a customer: no logos in the brief means offer to drop the block. |
|
|
184
|
+
| `painPoints.title` `.description` `.intro` `.items[]` | The problem the product solves. Four items by default: `{ id, icon, title, description }`, each a pain the brief's customer actually has. See [Icons](#icons). |
|
|
185
|
+
| `howItWorks.title` `.description` `.steps[]` | Three ordered steps: `{ id, title, description }`. Only for a real sequence a customer follows. |
|
|
181
186
|
| `features.title` `.description` | The section's heading and one supporting line. |
|
|
182
187
|
| `features.items[]` | Six by default: `{ id, icon, title, description }`. Title 2–4 words; description one sentence about what the owner can *do*, not about the technology. See [Icons](#icons). |
|
|
188
|
+
| `showcase.title` `.description` `.rows[]` | Alternating rows: `{ id, title, description, points[], visual, imageSrc, imageAlt }`. With real screenshots in `apps/web/public`, set `imageSrc` and a real `imageAlt`; otherwise keep `visual` and its lists, or offer to drop the block. |
|
|
189
|
+
| `audience.title` `.description` `.items[]` | Who it is for. Four items: `{ id, icon, title, description }`, from the brief's customer section. |
|
|
190
|
+
| `testimonials.title` `.description` `.items[]` | `{ id, quote, name, role }`. **Real quotes only**, with the person's permission. With none in the brief, keep the shipped placeholders and offer to drop the block. Never write a quote. |
|
|
183
191
|
| `pricing.title` `.description` | Heading plus one line. |
|
|
184
192
|
| `pricing.annualNote` `.currencySymbol` | See [Pricing](#pricing). |
|
|
185
|
-
| `pricing.tiers[]` | `{ id, name, description, monthlyPrice, annualPrice, features, ctaLabel, ctaHref }
|
|
193
|
+
| `pricing.tiers[]` | `{ id, name, description, monthlyPrice, annualPrice, priceSuffix, features, ctaLabel, ctaHref }`, plus `featured` and `badge` on at most one. Prices are whole units; `null` renders "Custom". An empty `priceSuffix` shows "/month". |
|
|
186
194
|
| `faq.items[]` | Five by default: `{ id, question, answer }`. Write the questions a buyer asks before paying — pricing, migration, lock-in, data, what happens when they outgrow it. One to three sentences, and answer the question. |
|
|
195
|
+
| `faq.moreLabel` `.moreLinkLabel` `.moreLinkHref` | The line under the list. Empty `moreLinkHref` hides it. |
|
|
187
196
|
| `cta.title` `.description` | The closing ask, plus one line removing a reason to hesitate. |
|
|
188
|
-
| `cta.
|
|
197
|
+
| `cta.command` | A command shown as a code chip. Empty for a product that is not installed from a terminal. |
|
|
198
|
+
| `cta.primaryActionLabel` `.primaryActionHref` `.secondaryActionLabel` `.secondaryActionHref` | See [Destinations](#destinations). |
|
|
189
199
|
| `footer.tagline` | One line under the brand. |
|
|
190
|
-
| `footer.
|
|
200
|
+
| `footer.links[]` `.linkTerms` `.linkPrivacy` | `{ id, label, href }` per outbound link, then the two legal labels. Empty drops a link. |
|
|
201
|
+
| `footer.githubHref` `.xHref` | Social profiles. Empty hides the icon. |
|
|
202
|
+
| `footer.copyright` | Keep `{year}` and `{siteName}` in it. |
|
|
191
203
|
|
|
192
204
|
Three mechanical rules the code depends on:
|
|
193
205
|
|
|
194
|
-
- **`{siteName}` is the
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- **
|
|
206
|
+
- **`{siteName}` is the placeholder**, and the blocks fill it in titles, descriptions,
|
|
207
|
+
answers and tier names. Never a template literal — copy is data. `{year}` works only in
|
|
208
|
+
`footer.copyright`. A pair of backticks in a description or answer renders as `<code>`.
|
|
209
|
+
- **Every list item's `id` is a stable key**, not a position. Rewriting a `title` under an
|
|
198
210
|
existing id is free. Renaming an id is allowed but it is a new string to a translation
|
|
199
211
|
layer, so do it only when the item genuinely became a different item.
|
|
200
|
-
- **Deleting an item is fine**, in any
|
|
201
|
-
|
|
212
|
+
- **Deleting an item is fine**, in any list. Fewer features, questions, steps or quotes all
|
|
213
|
+
lay out correctly.
|
|
202
214
|
|
|
203
215
|
### Icons
|
|
204
216
|
|
|
205
|
-
|
|
206
|
-
`packages/ui/src/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
because it was.
|
|
217
|
+
Feature, pain point and audience items carry an `icon` naming a glyph from the registry in
|
|
218
|
+
`packages/ui/src/lib/icons.ts`. Rewrite what an item is about and move its icon in the same
|
|
219
|
+
edit — that is the whole reason the name lives in content. A page whose "practice all four
|
|
220
|
+
modules" card renders a terminal prompt reads as machine-assembled, because it was.
|
|
210
221
|
|
|
211
222
|
- Use a key the registry actually has. Read the map; do not guess from memory. An unknown
|
|
212
223
|
name silently renders the fallback sparkle, which looks like a choice and is not.
|
|
213
224
|
- If nothing in the registry fits, say so in the draft and name the lucide icon you would
|
|
214
|
-
want. Widening the map is a two-line edit **the owner makes**, in a
|
|
215
|
-
yours.
|
|
225
|
+
want. Widening the map is a two-line edit **the owner makes**, in a library file, which is
|
|
226
|
+
not yours.
|
|
216
227
|
|
|
217
228
|
### Destinations
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
230
|
+
Every href that leaves the page is content: the navbar's two buttons, the hero's two
|
|
231
|
+
actions and its announcement, the CTA's two actions, each tier's `ctaHref`, the FAQ's
|
|
232
|
+
contact link and the footer's links and social profiles. The brief's **Where "sign up"
|
|
233
|
+
goes** section is where the main answer lives.
|
|
222
234
|
|
|
223
|
-
- **A URL in the brief.** Write it into `navbar.ctaHref
|
|
235
|
+
- **A URL in the brief.** Write it into `navbar.ctaHref`, `hero.primaryActionHref` and
|
|
236
|
+
`cta.primaryActionHref`.
|
|
224
237
|
- **Nothing yet.** Leave them as shipped (`#cta` and `/`) and write labels the page can
|
|
225
238
|
honestly satisfy — "See how it works" rather than "Start your free trial". Note it in the
|
|
226
239
|
draft and in the brief's **Known gaps**. A primary button that reloads the page is the
|
|
@@ -229,8 +242,12 @@ Three of the page's hrefs are content, and they are the ones that leave the page
|
|
|
229
242
|
- **`cta.secondaryActionHref`** is usually docs or an about page. `/` is honest for a
|
|
230
243
|
one-page site; a label promising docs that do not exist is not.
|
|
231
244
|
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
- **The shipped values point at Saasaloy's own repo and docs.** Replace every one. An href
|
|
246
|
+
you cannot fill gets an empty string where the key allows it, which hides the link.
|
|
247
|
+
|
|
248
|
+
Everything else stays put. `#features`, `#how-it-works`, `#showcase`, `#pricing` and
|
|
249
|
+
`#faq` are anchors matching section ids, they live in the blocks, and changing one breaks a
|
|
250
|
+
link.
|
|
234
251
|
|
|
235
252
|
### Pricing
|
|
236
253
|
|
|
@@ -324,22 +341,22 @@ sure about, and anything still on the **Not mine to fix** list.
|
|
|
324
341
|
|
|
325
342
|
### Dropping a block is its own confirmation
|
|
326
343
|
|
|
327
|
-
When the brief yields nothing a block can honestly carry — no pricing, no
|
|
328
|
-
|
|
344
|
+
When the brief yields nothing a block can honestly carry — no pricing, no customer quotes,
|
|
345
|
+
no customer logos, no screenshots for the showcase, no real sequence for how it works — you
|
|
346
|
+
may propose removing that block. **Separately.** Never bundled into the copy write,
|
|
329
347
|
never silent.
|
|
330
348
|
|
|
331
349
|
It is three edits, and all three are allowed:
|
|
332
350
|
|
|
333
|
-
1. Delete that block's line from `apps/web/src/pages/index.astro`.
|
|
351
|
+
1. Delete that block's line and its import from `apps/web/src/pages/index.astro`. Tell
|
|
352
|
+
the owner they can delete the block file in `packages/ui/src/blocks/` too; that file is
|
|
353
|
+
not yours to delete.
|
|
334
354
|
2. Blank the matching label in content (`landing.navbar.linkPricing = ""`,
|
|
335
355
|
`landing.footer.linkPricing = ""`). A blank label drops the link, so the page keeps no
|
|
336
356
|
nav entry pointing at a section that is gone.
|
|
337
|
-
3. **Retarget
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
`href: "#pricing"`, so when you remove the pricing block, pass `secondaryAction={null}`
|
|
341
|
-
to `<Hero>` in `index.astro` to drop the button, or pass a whole action pointing at a
|
|
342
|
-
section that still exists. Confirm which one with the owner along with the removal.
|
|
357
|
+
3. **Retarget any action pointing at the removed section.** The hero's and the CTA's
|
|
358
|
+
hrefs are content, so check each one in `landing.ts` and point it somewhere that still
|
|
359
|
+
exists. Confirm the new target with the owner along with the removal.
|
|
343
360
|
|
|
344
361
|
Removing a block is not redesigning one. You are editing lines in a page, not editing
|
|
345
362
|
`packages/ui/src/blocks/`.
|
|
@@ -371,7 +388,7 @@ Copy on disk plus a brief means this has run before.
|
|
|
371
388
|
- **Never edit a block.** `packages/ui/src/blocks/*.tsx` is off limits: no markup, no
|
|
372
389
|
classes, no structure, no widening the icon registry. Read them freely. If copy will not
|
|
373
390
|
fit a block, say so and let the owner decide.
|
|
374
|
-
- **Never touch the design layer** — `packages/ui/src/styles
|
|
391
|
+
- **Never touch the design layer** — `packages/ui/src/styles/*`,
|
|
375
392
|
`components.json`, `packages/ui/src/components/*`, or any Tailwind class anywhere.
|
|
376
393
|
- **Never rewrite `ui.*`**, in any language. Translate it or leave it.
|
|
377
394
|
- **Leave anchors alone.** `#features`, `#pricing`, `#faq`, `#cta` and a tier's default
|
|
@@ -262,7 +262,7 @@ can see:
|
|
|
262
262
|
- **Never write landing copy.** Not into `packages/ui/src/content/landing.ts`, not into a
|
|
263
263
|
block, not "just the headline while we're here". The brief is your output.
|
|
264
264
|
- **Never edit a block or the design layer** — `packages/ui/src/blocks/*`,
|
|
265
|
-
`packages/ui/src/styles
|
|
265
|
+
`packages/ui/src/styles/*`, `packages/ui/src/components/*`, `components.json`, or a Tailwind class
|
|
266
266
|
anywhere.
|
|
267
267
|
- **Never invent pricing, proof, a customer name, or a URL.** Not in the brief, and not in
|
|
268
268
|
a sample answer.
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"seedFiles": [
|
|
3
|
-
"DESIGN.md",
|
|
3
|
+
"apps/web/DESIGN.md",
|
|
4
4
|
"README.md",
|
|
5
5
|
"saasaloy.json",
|
|
6
6
|
"apps/web/src/pages/index.astro",
|
|
7
7
|
"packages/ui/src/content/landing.ts"
|
|
8
8
|
],
|
|
9
|
+
"retiredSeedFiles": ["DESIGN.md"],
|
|
9
10
|
"ownedFiles": [
|
|
11
|
+
"packages/env/.env.example",
|
|
12
|
+
"packages/env/src/services.ts",
|
|
13
|
+
"packages/env/src/sources.ts",
|
|
14
|
+
"apps/web/src/env.ts",
|
|
10
15
|
"packages/ui/src/styles/globals.css",
|
|
16
|
+
"packages/ui/src/styles/web.css",
|
|
11
17
|
"packages/ui/src/blocks/**",
|
|
12
18
|
"packages/ui/src/components/**",
|
|
13
19
|
"packages/ui/src/containers/**",
|
|
14
20
|
"packages/ui/src/content/**",
|
|
15
21
|
"packages/ui/src/index.ts",
|
|
22
|
+
"packages/ui/src/lib/icons.ts",
|
|
16
23
|
"apps/web/src/layouts/Layout.astro",
|
|
17
24
|
"apps/web/public/favicon.*"
|
|
18
25
|
]
|