saasaloy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/index.js +8455 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +69 -0
  4. package/schemas/manifest.schema.json +107 -0
  5. package/schemas/registry-item.schema.json +336 -0
  6. package/schemas/saasaloy-lock.schema.json +86 -0
  7. package/schemas/saasaloy.schema.json +42 -0
  8. package/templates/base/AGENTS.md +450 -0
  9. package/templates/base/CLAUDE.md +1 -0
  10. package/templates/base/DESIGN.md +209 -0
  11. package/templates/base/README.md +71 -0
  12. package/templates/base/_agents/skills/saasaloy-design/SKILL.md +198 -0
  13. package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +395 -0
  14. package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +273 -0
  15. package/templates/base/_gitignore +32 -0
  16. package/templates/base/_husky/commit-msg +1 -0
  17. package/templates/base/_husky/pre-commit +1 -0
  18. package/templates/base/_prettierignore +31 -0
  19. package/templates/base/_saasaloy-base.json +9 -0
  20. package/templates/base/apps/web/astro.config.mjs +61 -0
  21. package/templates/base/apps/web/package.json +30 -0
  22. package/templates/base/apps/web/public/favicon.svg +4 -0
  23. package/templates/base/apps/web/src/layouts/Layout.astro +52 -0
  24. package/templates/base/apps/web/src/pages/404.astro +24 -0
  25. package/templates/base/apps/web/src/pages/500.astro +33 -0
  26. package/templates/base/apps/web/src/pages/index.astro +61 -0
  27. package/templates/base/apps/web/src/pages/privacy.astro +15 -0
  28. package/templates/base/apps/web/src/pages/terms.astro +14 -0
  29. package/templates/base/apps/web/tsconfig.json +11 -0
  30. package/templates/base/apps/web/wrangler.jsonc +27 -0
  31. package/templates/base/commitlint.config.js +9 -0
  32. package/templates/base/lint-staged.config.js +17 -0
  33. package/templates/base/oxlint.config.mjs +155 -0
  34. package/templates/base/package.json +44 -0
  35. package/templates/base/packages/tsconfig/base.json +17 -0
  36. package/templates/base/packages/tsconfig/package.json +18 -0
  37. package/templates/base/packages/ui/components.json +19 -0
  38. package/templates/base/packages/ui/package.json +39 -0
  39. package/templates/base/packages/ui/src/blocks/cta.tsx +82 -0
  40. package/templates/base/packages/ui/src/blocks/error-state.tsx +144 -0
  41. package/templates/base/packages/ui/src/blocks/faq.tsx +64 -0
  42. package/templates/base/packages/ui/src/blocks/feature-grid.tsx +185 -0
  43. package/templates/base/packages/ui/src/blocks/footer.tsx +99 -0
  44. package/templates/base/packages/ui/src/blocks/hero.tsx +84 -0
  45. package/templates/base/packages/ui/src/blocks/navbar.tsx +159 -0
  46. package/templates/base/packages/ui/src/blocks/pricing-table.tsx +175 -0
  47. package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +51 -0
  48. package/templates/base/packages/ui/src/components/accordion.tsx +78 -0
  49. package/templates/base/packages/ui/src/components/badge.tsx +53 -0
  50. package/templates/base/packages/ui/src/components/button.tsx +59 -0
  51. package/templates/base/packages/ui/src/components/card.tsx +103 -0
  52. package/templates/base/packages/ui/src/components/input.tsx +20 -0
  53. package/templates/base/packages/ui/src/components/label.tsx +18 -0
  54. package/templates/base/packages/ui/src/components/separator.tsx +23 -0
  55. package/templates/base/packages/ui/src/containers/README.md +11 -0
  56. package/templates/base/packages/ui/src/content/errors.ts +58 -0
  57. package/templates/base/packages/ui/src/content/landing.ts +304 -0
  58. package/templates/base/packages/ui/src/index.ts +8 -0
  59. package/templates/base/packages/ui/src/lib/interpolate.ts +31 -0
  60. package/templates/base/packages/ui/src/lib/sentinel.ts +11 -0
  61. package/templates/base/packages/ui/src/lib/theme.ts +167 -0
  62. package/templates/base/packages/ui/src/lib/utils.ts +11 -0
  63. package/templates/base/packages/ui/src/styles/globals.css +164 -0
  64. package/templates/base/packages/ui/tsconfig.json +7 -0
  65. package/templates/base/pnpm-workspace.yaml +23 -0
  66. package/templates/base/prettier.config.js +10 -0
  67. package/templates/base/saasaloy.json +8 -0
  68. package/templates/base/stylelint.config.js +46 -0
  69. package/templates/base/turbo.json +20 -0
@@ -0,0 +1,71 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A Cloudflare-native SaaS, scaffolded with [Saasaloy](https://github.com/mimukit/saasaloy). The base is a
4
+ near-inert marketing shell; everything churny (API, database, auth, admin, features) installs on demand.
5
+
6
+ ## Develop
7
+
8
+ ```sh
9
+ pnpm install
10
+ pnpm dev # astro dev on apps/web
11
+ ```
12
+
13
+ ## Deploy
14
+
15
+ ```sh
16
+ pnpm --filter @repo/web build
17
+ pnpm --filter @repo/web run deploy # wrangler deploy (Cloudflare Workers static assets)
18
+ ```
19
+
20
+ ## Add features
21
+
22
+ ```sh
23
+ saasaloy add waitlist # pulls api, logger, validators, database and a driver
24
+ saasaloy add auth # pulls api, database and a driver
25
+ saasaloy list # what the registry offers, and what you already have
26
+ ```
27
+
28
+ `saasaloy list` names every installable module and marks the ones this project already has.
29
+ `saasaloy add <name> --dry-run` shows exactly what one would write before it writes it.
30
+
31
+ ## UI components
32
+
33
+ `DESIGN.md` records the project's design tokens and UI rules for people and agents.
34
+
35
+ `packages/ui` ships a Tailwind 4 theme, a small set of [shadcn](https://ui.shadcn.com)
36
+ primitives, and the marketing blocks the landing page is built from. Import primitives and
37
+ blocks by subpath — neither is re-exported from the package root, which carries
38
+ project-wide constants only:
39
+
40
+ ```ts
41
+ import { siteName } from "@repo/ui";
42
+ import { Button } from "@repo/ui/components/button";
43
+ import { PricingTable } from "@repo/ui/blocks/pricing-table";
44
+ ```
45
+
46
+ To add a primitive the base doesn't include, run the pinned CLI in `packages/ui` — that's
47
+ where `components.json` lives:
48
+
49
+ ```sh
50
+ pnpm --filter @repo/ui exec shadcn add dialog
51
+ ```
52
+
53
+ Components land in `packages/ui/src/components/` as source you own and can edit.
54
+
55
+ ## Landing page
56
+
57
+ `apps/web/src/pages/index.astro` composes the blocks in `packages/ui/src/blocks/` —
58
+ `navbar`, `hero`, `feature-grid`, `pricing-table`, `faq`, `cta`, `footer`. Each is one
59
+ self-contained `.tsx` with its copy as in-file defaults, so editing the file is how you
60
+ change the page.
61
+
62
+ Blocks render to static HTML by default. Only the three that need browser state carry a
63
+ client directive (`Navbar` is `client:idle`; `PricingTable` and `Faq` are
64
+ `client:visible`) — the rest ship no JavaScript at all. Don't reach for `client:load`.
65
+
66
+ The page composes itself from explicit imports, and nothing discovers a file behind your
67
+ back. A module that ships UI writes its block into `packages/ui/src/blocks/` like every
68
+ block above, plus a small island under `apps/web/src/components/` that feeds the block
69
+ whatever it needs to talk to (blocks stay presentational and take behaviour as props).
70
+ `saasaloy add <module>` then prints a pointer to the module's skill, which carries the
71
+ import line and the suggested spot. Where it actually goes is your call.
@@ -0,0 +1,198 @@
1
+ ---
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 the design contract for token drift.
4
+ derived-from: "designkit (MIT), narrowed 2026-08-09"
5
+ ---
6
+
7
+ # saasaloy-design
8
+
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
+
11
+ The token home is `packages/ui/src/styles/globals.css`. The design contract is `DESIGN.md` at the project root.
12
+
13
+ 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
+
15
+ ## Modes
16
+
17
+ - `theme` reads the product brief, selects a registry style with the owner, applies it, updates `DESIGN.md`, writes a new fingerprint, and lints the result.
18
+ - `update` re-derives the contract after a design layer change, writes a new fingerprint, and lints the result.
19
+ - `audit` checks the fingerprint and the contract. It never writes.
20
+
21
+ If the user does not name a mode, ask which mode to use. Do not choose between a write and an audit.
22
+
23
+ ## Write surface
24
+
25
+ | Mode | Files |
26
+ |------|-------|
27
+ | `theme` | `packages/ui/src/styles/globals.css`, `DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
28
+ | `update` | `DESIGN.md` only |
29
+ | `audit` | none |
30
+
31
+ Do not edit `components.json` during a theme change. Do not add a dependency, script, component, page, or Tailwind class.
32
+
33
+ ## Shared rules
34
+
35
+ Read `DESIGN.md`, `packages/ui/src/styles/globals.css`, and all files under `packages/ui/src/` before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
36
+
37
+ Find the system in this order. Stop at the first match.
38
+
39
+ 1. Use the Tailwind 4 `@theme` block.
40
+ 2. Use the `:root` custom properties.
41
+ 3. Report that the expected Saasaloy token home is missing.
42
+
43
+ Read declarations and usage. Count color values, type utilities, spacing utilities, radii, shadows, interaction states, and dark mode variants. A declared value with no use is not enough evidence for a design token.
44
+
45
+ Classify each proposed token as `extracted`, `consolidated`, or `omitted`. List the source values for each consolidation. Use `omitted` when the project does not define a scale.
46
+
47
+ Keep spacing omitted while the project uses Tailwind's default scale unchanged. Use this exact reason in the front matter: `Tailwind's default scale is used unchanged`.
48
+
49
+ Write elevation as prose under `## Elevation & Depth`. Never add `elevation` to `omitted`. The official linter does not accept that omission name.
50
+
51
+ Write motion as prose under `## Motion`. The alpha schema has no motion token group and no component transition token.
52
+
53
+ Use only these component token keys: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, and `width`.
54
+
55
+ Keep the sections in this order: Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, Motion, Dark Mode.
56
+
57
+ The seed is a one-time base file. ADR 0022 means Saasaloy has no update path for it. The fingerprint detects drift, and this skill repairs the contract.
58
+
59
+ ## Fingerprint
60
+
61
+ The fingerprint covers only `packages/ui/src/styles/globals.css`. It does not cover components, blocks, or other files.
62
+
63
+ Compute the fingerprint from the file bytes. Use the first 12 lowercase hexadecimal characters of SHA-256.
64
+
65
+ ```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/globals.css')).digest('hex').slice(0, 12))"
67
+ ```
68
+
69
+ The scaffold seed ends with this stamp.
70
+
71
+ ```markdown
72
+ _Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
73
+ ```
74
+
75
+ After `theme` or `update`, replace the seed stamp with this stamp.
76
+
77
+ ```markdown
78
+ _Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
79
+ ```
80
+
81
+ Do not change the fingerprint during `audit`.
82
+
83
+ ## Official linter
84
+
85
+ Run the official linter after each write.
86
+
87
+ ```sh
88
+ pnpm dlx @google/design.md lint DESIGN.md
89
+ ```
90
+
91
+ The format is alpha. Read the current schema when a lint finding conflicts with this skill.
92
+
93
+ ```sh
94
+ pnpm dlx @google/design.md spec --format json
95
+ ```
96
+
97
+ Do not hide a warning. Fix it when the code supports the fix. Report it with the reason when the project intentionally keeps it.
98
+
99
+ If the network or `pnpm dlx` is unavailable, continue with the fingerprint and local extraction. State that structural lint did not run. Never call an unchecked file clean.
100
+
101
+ ## `theme`
102
+
103
+ ### 1. Read the product context
104
+
105
+ Read `docs/product-brief.md` first. Use its product, audience, differentiator, tone, and language answers. Do not ask those questions again.
106
+
107
+ If the brief is absent, offer to run `saasaloy-setup`:
108
+
109
+ > There's no `docs/product-brief.md`, so I'd be picking a theme for a product I know nothing about. `/saasaloy-setup` asks ten questions and leaves the brief behind; then this picks up where it stops. Want me to run it now?
110
+
111
+ Invoke that skill rather than reading its files. Do **not** improvise your own interview, and do not write `docs/product-brief.md` yourself. `saasaloy-setup` owns the brief and its format, and two skills asking overlapping questions is how an owner answers the same thing twice.
112
+
113
+ Ask only for design facts that the brief does not contain. Cover the color mood, the preset direction or registry URL, and the desired density.
114
+
115
+ ### 2. Choose the registry style
116
+
117
+ Accept a `registry:style` URL from `https://ui.shadcn.com/create`, `https://tweakcn.com`, or another compatible registry. Do not invent a Saasaloy palette catalogue.
118
+
119
+ Preview the URL and the files that the command can change. Get approval before you run the command.
120
+
121
+ ### 3. Apply the preset
122
+
123
+ Use the package-local executable from the project root.
124
+
125
+ ```sh
126
+ pnpm --filter @repo/ui exec shadcn add <registry-style-url>
127
+ ```
128
+
129
+ Confirm that the three `@source` rules, `@custom-variant dark`, and `@layer base` remain in `globals.css`. Confirm that `components.json` did not change.
130
+
131
+ 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
+
133
+ ### 4. Re-derive the contract
134
+
135
+ Run the shared extraction process against the merged `globals.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
136
+
137
+ 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
+
139
+ Recompute and replace the fingerprint stamp. Run the official linter.
140
+
141
+ ### 5. Report
142
+
143
+ Name the registry style URL. List the changed token groups and the new fingerprint. State the linter result. Tell the user to use `uikit` for component or page work.
144
+
145
+ ## `update`
146
+
147
+ ### 1. Find the change
148
+
149
+ Read `git status --short`. Read the working tree diff when it exists. Otherwise, compare the current branch with its base branch.
150
+
151
+ Identify changes under `packages/ui/`. State which DESIGN.md sections they affect. State which sections remain unchanged.
152
+
153
+ ### 2. Re-derive with restraint
154
+
155
+ Run the shared extraction process. Edit existing tokens and affected prose only.
156
+
157
+ Ask before you add or delete a section. Do not rewrite the full file because one token changed.
158
+
159
+ Recompute and replace the fingerprint stamp. Run the official linter.
160
+
161
+ ### 3. Report
162
+
163
+ List each changed token with its old and new value. Name the sections left unchanged. State the new fingerprint and linter result.
164
+
165
+ ## `audit`
166
+
167
+ Audit is read-only. Do not edit or format any file.
168
+
169
+ ### 1. Check the fingerprint offline
170
+
171
+ Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from `globals.css`.
172
+
173
+ - `current` means the two fingerprints match.
174
+ - `stale` means they differ.
175
+ - `unverified` means the stamp or source file is missing.
176
+
177
+ ### 2. Check token drift
178
+
179
+ Scan `packages/ui/src/` and the application usage. Report tokens whose values no longer exist. Report repeated values that no token covers.
180
+
181
+ Use `orphaned` for a documented token with no current source. Use `uncovered` for a repeated source value with no documented token.
182
+
183
+ ### 3. Lint when possible
184
+
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.
186
+
187
+ ### 4. Report
188
+
189
+ Open with the file count that the audit scanned. Give one verdict for the fingerprint and one verdict for token coverage. Name the worst drift and recommend `saasaloy-design update` when repair is needed.
190
+
191
+ ## Boundaries
192
+
193
+ - Never write a component or a page. Use `uikit` for that work.
194
+ - Never add a token value that the project does not contain.
195
+ - Never change `globals.css` in `update` or `audit`.
196
+ - Never write during `audit`.
197
+ - Never claim that lint passed when the linter did not run.
198
+ - Never treat a matching fingerprint as proof that component usage has no drift.
@@ -0,0 +1,395 @@
1
+ ---
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 says "Acme" or "The SaaS you meant to build", 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
+ ---
5
+
6
+ # saasaloy-landing-copy — draft it in markdown, then write the page
7
+
8
+ Copy is a chore an agent does well **if** it is first made to understand the product, and
9
+ badly if it guesses. Understanding the product is not this skill's job: `saasaloy-setup`
10
+ already interviewed the owner and left `docs/product-brief.md` behind. This skill turns
11
+ that brief into words.
12
+
13
+ It does it in two passes, and the order matters. **First a markdown draft** the owner can
14
+ read end to end in one screen and edit in place. **Then** the content module, once they say
15
+ yes. A page of thirty keys reviewed as thirty terminal messages is a page nobody actually
16
+ reviewed.
17
+
18
+ ## The write surface
19
+
20
+ | Path | What you change |
21
+ |------|-----------------|
22
+ | `docs/landing-copy-draft.md` | The draft. Written every run, deleted once the copy lands. |
23
+ | `packages/ui/src/content/landing.ts` | Everything under `landing.*`. Plus `ui.*`, **translated only**, and only when the page is not in English — see [Language](#language-and-the-ui-namespace). |
24
+ | `apps/web/src/pages/index.astro` | Only to drop a block, and only behind its own confirmation. |
25
+ | `docs/product-brief.md` | Only to append what you learned filling a gap the brief left, and only its **Known gaps** section otherwise. |
26
+
27
+ Nothing else — in particular not `siteName` and not the page's `lang` attribute. Those are
28
+ project identity, `saasaloy-setup` owns them, and [Step 0](#step-0--before-you-write-anything)
29
+ says what to do when they disagree with the brief.
30
+
31
+ ## Step 0 — before you write anything
32
+
33
+ 1. **Read `docs/product-brief.md`.** No brief, no copy. Say so and offer to run
34
+ `saasaloy-setup`:
35
+
36
+ > There's no `docs/product-brief.md`, so I'd be writing about a product I know nothing
37
+ > about. `/saasaloy-setup` asks ten questions and leaves the brief behind; then this
38
+ > takes about a minute. Want me to run it now?
39
+
40
+ Do **not** improvise your own interview. Two skills asking overlapping questions is how
41
+ an owner ends up answering the same thing twice and getting two different pages.
42
+ 2. **Read `packages/ui/src/content/landing.ts`.** You need the current copy to diff against,
43
+ and its comments carry the shape rules you have to keep.
44
+ 3. **Read the icon registry** at the top of `packages/ui/src/blocks/feature-grid.tsx`.
45
+ Reading a block is fine; editing one is not. Its keys are the whole vocabulary available
46
+ to `landing.features.items[].icon`, and they are the actual list rather than one copied
47
+ into this file that went stale.
48
+ 4. **Check the two facts you do not own.** If `siteName` in `packages/ui/src/index.ts` is
49
+ still the scaffold's directory slug, or `lang` in `apps/web/src/layouts/Layout.astro` is
50
+ `en` while the brief names another language, say so now and carry both into the draft's
51
+ **Not mine to fix** list. Writing Bangla copy into a page that declares itself English is
52
+ a real defect, and one attribute long. Do not fix it yourself.
53
+ 5. **Look at git, and treat it as advice.** `git status --short` if there is a repo. A dirty
54
+ tree or no repo is worth one sentence — "your changes aren't committed, so you can't undo
55
+ this with git; the draft is reviewed before anything is written anyway" — and then you
56
+ carry on. **Neither state is a reason to stop.**
57
+ 6. **If a draft is already on disk**, an earlier run was abandoned or is waiting on the
58
+ owner. Say it is there, summarise it in a line, and ask whether to build on it or start
59
+ over. Never silently overwrite someone's edits.
60
+ 7. **If the content module has already been rewritten by hand** and the brief does not
61
+ explain it, say so plainly: someone wrote this themselves. Ask whether to work from what
62
+ is there or start over.
63
+
64
+ ## Step 1 — fill only the gaps the brief left
65
+
66
+ The interview happened. Your questions are limited to what the brief genuinely does not
67
+ answer and the page genuinely needs — usually the FAQ, occasionally a feature that has no
68
+ evidence behind it. Ask in one batch, and carry **three sample answers plus "write your
69
+ own"** on every question, the same way `saasaloy-setup` does: derived from the brief rather
70
+ than generic, shapes rather than invented values wherever the answer becomes a claim the
71
+ page makes.
72
+
73
+ Every `weak:` tag in the brief is a standing question, and re-asking one is always fair:
74
+
75
+ > Last time "it's faster" had no number behind it, so the hero avoided the claim. Do you
76
+ > have a benchmark now?
77
+
78
+ If a gap stays a gap, write around it. Describe what the product does and leave the claim
79
+ out. A page that says less is recoverable; a page that says something false about the
80
+ product is not.
81
+
82
+ ## Step 2 — write the draft
83
+
84
+ `docs/landing-copy-draft.md`. Every key, current value beside proposed, in reading order —
85
+ so the owner reviews a page, not a data structure.
86
+
87
+ Three things belong in it besides the copy, because they are the ones an owner catches and
88
+ an agent does not:
89
+
90
+ - **The non-copy decisions.** Each feature's icon name, and where the two calls to action
91
+ point. A wrong icon is invisible in a list of sentences and obvious in a list of icon
92
+ names.
93
+ - **What you were working from.** One line per section tying the choice back to the brief.
94
+ This is what makes "no, that's not what I meant" a two-second correction.
95
+ - **What you are least sure about**, and **what is not yours to fix.**
96
+
97
+ ```md
98
+ # Landing copy draft — Ledgerly
99
+
100
+ Generated 2026-08-09 from `docs/product-brief.md`.
101
+ Edit anything here and tell me to apply it, or tell me what to change and I'll redo it.
102
+ This file is deleted once the copy lands in `packages/ui/src/content/landing.ts`.
103
+
104
+ ## Tab and search result
105
+ | Key | Now | Proposed |
106
+ |-----|-----|----------|
107
+ | `meta.title` | {siteName} — ship your SaaS, not your scaffolding | {siteName} — close the month in one place |
108
+
109
+ `meta.description` (155 char limit, currently 148):
110
+ > Ledgerly closes the month for bookkeepers carrying 5–20 client accounts, without four
111
+ > spreadsheets per client.
112
+
113
+ ## Hero
114
+ | Key | Now | Proposed |
115
+ |-----|-----|----------|
116
+ | `hero.eyebrow` | Now in early access | In beta with two firms |
117
+ | `hero.primaryActionLabel` | Get started | Join the beta |
118
+
119
+ `hero.title`:
120
+ > ~~The SaaS you meant to build, already scaffolded.~~
121
+ > **Close the month without four spreadsheets per client.**
122
+
123
+ *From the brief: the problem line, and "Excel plus a shared Dropbox folder" as the
124
+ alternative. The eyebrow uses the two named beta firms rather than a stage name.*
125
+
126
+ ## Features
127
+ | # | Icon | Title | About |
128
+ |---|------|-------|-------|
129
+ | 1 | `list-checks` (was `zap`) | One close, one checklist | … |
130
+
131
+ ## Where the buttons go
132
+ | Key | Now | Proposed |
133
+ |-----|-----|----------|
134
+ | `navbar.ctaHref` | `#cta` | https://ledgerly.com.bd/waitlist |
135
+ | `cta.primaryActionHref` | `/` | https://ledgerly.com.bd/waitlist |
136
+
137
+ ## Least sure about
138
+ - The eyebrow names your two beta firms. Fine to say publicly?
139
+ - FAQ 4 says data exports as CSV. The brief doesn't mention exports; I inferred it.
140
+
141
+ ## Not mine to fix
142
+ - `lang` in `apps/web/src/layouts/Layout.astro` is still `en` and the brief says Bangla.
143
+ Run `/saasaloy-setup`, or change the one attribute yourself.
144
+ ```
145
+
146
+ If the owner would rather skip the review — "just write it" — **still write the draft
147
+ file.** The draft is the record of what you propose, so it is not theirs to skip. Say once
148
+ that you are treating their go-ahead as approval, write `docs/landing-copy-draft.md`, name
149
+ the path, then go to [Step 4](#step-4--write-the-content-module) and show the diff there.
150
+
151
+ ## Step 3 — the owner reviews
152
+
153
+ Hand them the path and stop. Do not narrate the whole draft back into the terminal; the
154
+ point of the file is that it is not the terminal.
155
+
156
+ > Draft's at `docs/landing-copy-draft.md`. Read it, edit anything you'd rather word
157
+ > yourself, then tell me to apply it.
158
+
159
+ When they come back, **re-read the file from disk** before applying. They may have edited
160
+ it, and their wording wins over yours every time.
161
+
162
+ ## Step 4 — write the content module
163
+
164
+ Show the diff, key by key, old value then new. One confirmation for the file is enough; do
165
+ not ask per key. Then write.
166
+
167
+ Every key below is under `landing.` in `packages/ui/src/content/landing.ts`. Fill all of
168
+ them; the layout is built for copy of roughly the length already there, so match it rather
169
+ than doubling it.
170
+
171
+ | Key | What goes there |
172
+ |-----|-----------------|
173
+ | `meta.title` | Browser tab. `{siteName} — <what it does>`, under 60 characters. |
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. |
176
+ | `navbar.ctaLabel` `.ctaHref` | Two or three words, a verb first, and where it goes. See [Destinations](#destinations). |
177
+ | `hero.eyebrow` | The one-line status above the headline ("Now in early access"). Empty string hides it. |
178
+ | `hero.title` | The headline. Under ten words, one idea, concrete. This is the sentence the whole page is judged on. |
179
+ | `hero.description` | One or two sentences saying what it does for whom. |
180
+ | `hero.primaryActionLabel` `.secondaryActionLabel` | Button labels, a verb first. Both scroll down the page; their hrefs are anchors and stay in the block. |
181
+ | `features.title` `.description` | The section's heading and one supporting line. |
182
+ | `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). |
183
+ | `pricing.title` `.description` | Heading plus one line. |
184
+ | `pricing.annualNote` `.currencySymbol` | See [Pricing](#pricing). |
185
+ | `pricing.tiers[]` | `{ id, name, description, monthlyPrice, annualPrice, features, ctaLabel, ctaHref }`. Prices are whole units; `null` renders "Custom". Set `featured` on at most one. A tier's `ctaHref` stays `#cta` unless the brief gives that tier its own destination. |
186
+ | `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. |
187
+ | `cta.title` `.description` | The closing ask, plus one line removing a reason to hesitate. |
188
+ | `cta.primaryActionHref` `.secondaryActionHref` | See [Destinations](#destinations). |
189
+ | `footer.tagline` | One line under the brand. |
190
+ | `footer.groupProduct` `.groupLegal` `.link*` | Footer labels. One or two words each. |
191
+
192
+ Three mechanical rules the code depends on:
193
+
194
+ - **`{siteName}` is the only placeholder, and it works in exactly four strings**:
195
+ `meta.title`, `meta.description`, `hero.description`, `cta.description`. Anywhere else it
196
+ renders as the literal text `{siteName}`. Never a template literal — copy is data.
197
+ - **Feature and FAQ `id`s are stable keys**, not positions. Rewriting a `title` under an
198
+ existing id is free. Renaming an id is allowed but it is a new string to a translation
199
+ layer, so do it only when the item genuinely became a different item.
200
+ - **Deleting an item is fine**, in any of the three lists. Fewer than six features and fewer
201
+ than five questions both lay out correctly.
202
+
203
+ ### Icons
204
+
205
+ Each feature carries an `icon` naming a glyph from the registry at the top of
206
+ `packages/ui/src/blocks/feature-grid.tsx`. Rewrite what a feature is about and move its
207
+ icon in the same edit — that is the whole reason the name lives in content. A page whose
208
+ "practice all four modules" card renders a terminal prompt reads as machine-assembled,
209
+ because it was.
210
+
211
+ - Use a key the registry actually has. Read the map; do not guess from memory. An unknown
212
+ name silently renders the fallback sparkle, which looks like a choice and is not.
213
+ - 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 block, which is not
215
+ yours.
216
+
217
+ ### Destinations
218
+
219
+ Three of the page's hrefs are content, and they are the ones that leave the page:
220
+ `navbar.ctaHref`, `cta.primaryActionHref`, `cta.secondaryActionHref`. The brief's
221
+ **Where "sign up" goes** section is where the answer lives.
222
+
223
+ - **A URL in the brief.** Write it into `navbar.ctaHref` and `cta.primaryActionHref`.
224
+ - **Nothing yet.** Leave them as shipped (`#cta` and `/`) and write labels the page can
225
+ honestly satisfy — "See how it works" rather than "Start your free trial". Note it in the
226
+ draft and in the brief's **Known gaps**. A primary button that reloads the page is the
227
+ single most visible defect this workflow can leave behind, so it does not get to be
228
+ silent.
229
+ - **`cta.secondaryActionHref`** is usually docs or an about page. `/` is honest for a
230
+ one-page site; a label promising docs that do not exist is not.
231
+
232
+ Everything else stays put. `#features`, `#pricing`, `#faq` and `#cta` are anchors matching
233
+ section ids, they live in the blocks, and changing one breaks a link.
234
+
235
+ ### Pricing
236
+
237
+ The brief's **Pricing** section decides this, and it has already been through
238
+ `saasaloy-setup`'s "extracted, never invented" rule. Your job is to carry it across without
239
+ softening it.
240
+
241
+ - **Real prices** — write them, and set `currencySymbol` if it is not USD.
242
+ - **Placeholder** — leave the shipped tiers exactly as they are and say so in the draft.
243
+ - **None yet** — offer to drop the pricing block. That is a
244
+ [block removal](#dropping-a-block-is-its-own-confirmation), with its own confirmation.
245
+
246
+ `annualNote` is a claim about the two numbers beside it. If there is an annual discount, set
247
+ each tier's `annualPrice` to the effective monthly cost when billed annually and make the
248
+ note arithmetically true. If there is not, set `annualPrice` equal to `monthlyPrice` and
249
+ `annualNote` to `""`. The monthly/annual switch stays on the page — it is chrome, and it now
250
+ claims nothing untrue.
251
+
252
+ Never round, convert currencies, or fill in a typical number.
253
+
254
+ ### The bar this copy has to clear
255
+
256
+ You are writing without a copy editor, and the failure mode is not bad grammar. It is
257
+ fluent, confident, empty text that reads as machine-written. Refuse these, in the copy you
258
+ write and in the copy you keep:
259
+
260
+ - **Abstract benefit nouns as the subject.** "solutions", "experiences", "workflows",
261
+ "innovation", "efficiency". Name what the person does instead.
262
+ - **The AI vocabulary.** leverage, unlock, empower, seamless, robust, streamline, elevate,
263
+ cutting-edge, game-changing, effortless, revolutionary, harness, delve.
264
+ - **Rule-of-three cadence.** "Faster, simpler, smarter." Three-item lists everywhere is the
265
+ loudest tell there is. Use two, or four, or a sentence.
266
+ - **"Not just X — it's Y."** Also "more than just", "isn't only about".
267
+ - **Claims nobody could disagree with.** "Built for modern teams." "Designed for how you
268
+ work today." If the opposite is absurd, the sentence is empty.
269
+ - **Superlatives with no number behind them.** "Blazing fast", "enterprise-grade",
270
+ "world-class", "industry-leading".
271
+ - **Em-dash tic.** At most one em-dash per section. Full stops are free.
272
+ - **Every sentence the same length.** Vary it. Let one land in four words.
273
+ - **Invented proof.** No statistic, customer name, logo, award, rating or "trusted by
274
+ thousands" that did not come out of the brief. Not one.
275
+ - **Second-person promises the product cannot keep.** "You'll never worry about invoices
276
+ again."
277
+
278
+ Two habits carry more weight than that whole list: use the **owner's own nouns** from the
279
+ brief, and **name the current alternative**. "Close the month without four spreadsheets per
280
+ client" is a sentence no generator produces, because it required the interview.
281
+
282
+ ### Language, and the `ui.*` namespace
283
+
284
+ Write in the language the brief names, not English with a translation to follow.
285
+
286
+ When that language is not English, `ui.*` gets **translated** in the same pass. It is chrome
287
+ — "Monthly", "Most popular", "Close menu", the copyright line — and leaving it in English
288
+ under Bangla marketing copy ships a visibly broken page. No translation layer exists in the
289
+ base to come back for it later, so this is the only pass it gets.
290
+
291
+ Translated is not rewritten. Four rules:
292
+
293
+ - **Preserve every `{token}` exactly**: `{currencySymbol}`, `{price}`, `{year}`,
294
+ `{siteName}`. A dropped token is a silently broken price or copyright line.
295
+ - **Never rename, add or remove a key.**
296
+ - **Never reword a string that stays English.** In an English run, `ui.*` is untouched.
297
+ - **Leave `packages/ui/src/lib/theme.ts` alone** regardless. It is inlined verbatim into a
298
+ pre-paint `<script>`, is import-free on purpose, and its labels are a separate decision.
299
+
300
+ Then record in the brief's **Known gaps**, in one line, that `ui.*` was translated by a copy
301
+ agent rather than a translator and is worth a native speaker's read. Also record — once —
302
+ that the template loads **no webfont**: `font-sans` is the system stack, so a non-Latin
303
+ script renders in whatever face the visitor's device provides. Both are real gaps and
304
+ neither is a reason to write the page in English instead.
305
+
306
+ ## Step 5 — prove it, then clean up
307
+
308
+ A page that does not compile is not a deliverable:
309
+
310
+ ```sh
311
+ pnpm --filter @repo/ui typecheck
312
+ pnpm build
313
+ ```
314
+
315
+ Broken build, broken types: fix it, or revert the write and say what happened. Do not hand
316
+ back a red tree.
317
+
318
+ Then **delete `docs/landing-copy-draft.md`.** It has done its job, and a stale draft beside
319
+ a rewritten page is a second source of truth that will disagree within a week. The brief is
320
+ the one that persists.
321
+
322
+ Finally, tell the owner what to look at: `pnpm dev`, the two or three lines you are least
323
+ sure about, and anything still on the **Not mine to fix** list.
324
+
325
+ ### Dropping a block is its own confirmation
326
+
327
+ When the brief yields nothing a block can honestly carry — no pricing, no proof for the FAQ
328
+ — you may propose removing that block. **Separately.** Never bundled into the copy write,
329
+ never silent.
330
+
331
+ It is three edits, and all three are allowed:
332
+
333
+ 1. Delete that block's line from `apps/web/src/pages/index.astro`.
334
+ 2. Blank the matching label in content (`landing.navbar.linkPricing = ""`,
335
+ `landing.footer.linkPricing = ""`). A blank label drops the link, so the page keeps no
336
+ nav entry pointing at a section that is gone.
337
+ 3. **Retarget or drop any hero action pointing at the removed section.** A blank label
338
+ does not save you here: the hero's `href` lives in the block, so it survives an empty
339
+ label and the button still points at nothing. The hero's secondary action ships as
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.
343
+
344
+ Removing a block is not redesigning one. You are editing lines in a page, not editing
345
+ `packages/ui/src/blocks/`.
346
+
347
+ ## Re-running
348
+
349
+ Copy on disk plus a brief means this has run before.
350
+
351
+ 1. **Read the brief and the current copy**, and say in a few lines what the page claims
352
+ today.
353
+ 2. **Ask what changed**, and ask about every `weak:` tag.
354
+ 3. **Draft again.** The draft's "Now" column is the current copy, so a re-run shows the
355
+ owner exactly what a second pass would move — which is the whole value of the second
356
+ pass.
357
+ 4. If the copy on disk does not match what the brief would produce, someone edited it by
358
+ hand: say so, show the difference, and ask which wins.
359
+
360
+ ## Boundaries to honor
361
+
362
+ - **[The write surface](#the-write-surface) is the whole list of files you may touch** —
363
+ nothing outside that table. Two of them carry copy: `packages/ui/src/content/landing.ts`
364
+ (`landing.*` always, `ui.*` translated on a non-English run) and
365
+ `docs/landing-copy-draft.md`, which you write on every run and delete at the end. The
366
+ other two are a consented block removal in `apps/web/src/pages/index.astro` and appended
367
+ gaps in `docs/product-brief.md`.
368
+ - **`siteName` and `lang` are not yours.** `packages/ui/src/index.ts` and
369
+ `apps/web/src/layouts/Layout.astro` belong to `saasaloy-setup`. Report a mismatch; do not
370
+ fix it.
371
+ - **Never edit a block.** `packages/ui/src/blocks/*.tsx` is off limits: no markup, no
372
+ classes, no structure, no widening the icon registry. Read them freely. If copy will not
373
+ fit a block, say so and let the owner decide.
374
+ - **Never touch the design layer** — `packages/ui/src/styles/globals.css`,
375
+ `components.json`, `packages/ui/src/components/*`, or any Tailwind class anywhere.
376
+ - **Never rewrite `ui.*`**, in any language. Translate it or leave it.
377
+ - **Leave anchors alone.** `#features`, `#pricing`, `#faq`, `#cta` and a tier's default
378
+ `ctaHref` match section ids. The three outbound destinations in
379
+ [Destinations](#destinations) are the exception, and they are the only one.
380
+ - **Leave `apps/web/src/pages/terms.astro` and `privacy.astro` alone.** Boilerplate legal
381
+ text is not a copywriting job.
382
+ - **No images, logos, icons or screenshots.** Not generated, not sourced, not referenced.
383
+ Choosing an existing icon name is not this.
384
+ - **No dependencies, no i18n machinery, no webfonts.** Report the gap; do not close it.
385
+ - **Never invent pricing or proof**, and never quietly leave the shipped demo numbers in
386
+ place as if they were real.
387
+ - **No repeated setup interview.** The threshold is presence, not depth. Hand back to
388
+ `saasaloy-setup` only when `docs/product-brief.md` is absent, or names no product at
389
+ all. A brief that exists and names the product is usable however thin it is: proceed
390
+ under the next bullet, and for a page-specific gap ask only the questions
391
+ [Step 1](#step-1--fill-only-the-gaps-the-brief-left) allows. Never ask the setup
392
+ interview's own questions yourself.
393
+ - **A dirty tree, a missing repo, or a thin brief is not a blocker.** Warn, write less, and
394
+ say what you left out. (This is about those three conditions only — anything unsafe or
395
+ outside this skill's scope you decline as you normally would.)