saasaloy 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +180 -53
- package/templates/base/README.md +20 -8
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +26 -15
- 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 +91 -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 +1 -0
- 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/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
|
@@ -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
|
]
|
|
@@ -32,10 +32,16 @@ colors:
|
|
|
32
32
|
typography:
|
|
33
33
|
headline-display:
|
|
34
34
|
fontFamily: "ui-sans-serif, system-ui, sans-serif"
|
|
35
|
-
fontSize:
|
|
35
|
+
fontSize: 4.5rem
|
|
36
|
+
fontWeight: 500
|
|
37
|
+
lineHeight: 1.05
|
|
38
|
+
letterSpacing: -0.035em
|
|
39
|
+
headline-section:
|
|
40
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif"
|
|
41
|
+
fontSize: 3rem
|
|
36
42
|
fontWeight: 600
|
|
37
|
-
lineHeight: 1
|
|
38
|
-
letterSpacing: -0.
|
|
43
|
+
lineHeight: 1.1
|
|
44
|
+
letterSpacing: -0.03em
|
|
39
45
|
headline-lg:
|
|
40
46
|
fontFamily: "ui-sans-serif, system-ui, sans-serif"
|
|
41
47
|
fontSize: 2.25rem
|
|
@@ -53,6 +59,11 @@ typography:
|
|
|
53
59
|
fontSize: 1.125rem
|
|
54
60
|
fontWeight: 400
|
|
55
61
|
lineHeight: 1.75rem
|
|
62
|
+
lead:
|
|
63
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif"
|
|
64
|
+
fontSize: 1.125rem
|
|
65
|
+
fontWeight: 400
|
|
66
|
+
lineHeight: 1.75rem
|
|
56
67
|
body-md:
|
|
57
68
|
fontFamily: "ui-sans-serif, system-ui, sans-serif"
|
|
58
69
|
fontSize: 1rem
|
|
@@ -93,7 +104,10 @@ components:
|
|
|
93
104
|
typography: "{typography.headline-display}"
|
|
94
105
|
section-title:
|
|
95
106
|
textColor: "{colors.foreground}"
|
|
96
|
-
typography: "{typography.headline-
|
|
107
|
+
typography: "{typography.headline-section}"
|
|
108
|
+
section-lead:
|
|
109
|
+
textColor: "{colors.muted-foreground}"
|
|
110
|
+
typography: "{typography.lead}"
|
|
97
111
|
legal-title:
|
|
98
112
|
textColor: "{colors.foreground}"
|
|
99
113
|
typography: "{typography.headline-md}"
|
|
@@ -162,25 +176,25 @@ The base uses a neutral, content-first system. It gives a new product a clear st
|
|
|
162
176
|
|
|
163
177
|
## Apps
|
|
164
178
|
|
|
165
|
-
|
|
179
|
+
This file covers the web app. Each app has its own theme file in `packages/ui/src/styles/` and its own `DESIGN.md` in its own folder. `web.css` holds the tokens recorded here, and `globals.css` holds the shared Tailwind setup with no token values. Another app's theme file imports `globals.css` and declares its own tokens, mapping and radius scale, so a change here never reaches it.
|
|
166
180
|
|
|
167
181
|
## Colors
|
|
168
182
|
|
|
169
183
|
The light theme uses white surfaces, near-black text, and a dark neutral primary action. The dark theme reverses that relationship with near-black surfaces and near-white text. Muted neutrals separate supporting content. The destructive color is the only chromatic semantic token in the seed.
|
|
170
184
|
|
|
171
|
-
Use the semantic custom properties in `packages/ui/src/styles/
|
|
185
|
+
Use the semantic custom properties in `packages/ui/src/styles/web.css`. Do not copy their current `oklch()` values into components.
|
|
172
186
|
|
|
173
187
|
## Typography
|
|
174
188
|
|
|
175
|
-
The template uses Tailwind's system sans stack.
|
|
189
|
+
The template uses Tailwind's system sans stack, with no web font. Three named sizes in `web.css` carry the landing page's ramp. `text-display` is the hero's one `h1`: medium weight, very tight tracking, and fluid from `2.5rem` to `4.5rem`. `text-section-title` is every section's `h2`: semibold, tight, fluid from `2rem` to `3rem`. `text-lead` is the paragraph under either. The token map records the largest size of each fluid step. Body copy uses the `text-sm` through `text-lg` scale. Controls use medium weight at `text-xs` or `text-sm`. Code the page quotes (commands, module names, paths) uses the mono face at `text-xs` or the surrounding size.
|
|
176
190
|
|
|
177
191
|
## Layout
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
Three container widths live in `web.css`. `section-container` is every section's frame at `64rem`. `section-container-narrow` is a deliberate `48rem` reading measure for the FAQ and the closing CTA. `section-container-wide` is `80rem`, for the navbar and the hero's product frame. All three share a `1.5rem` gutter. The sticky navbar floats: a gap holds it off the top of the viewport, and it overlays the first section instead of pushing it down. The hero's background, grid and light beams start at the top of the page, behind the bar, so no band of plain background shows above the hero. `section-y` is the one vertical rhythm between sections: `5rem` on small screens and `8rem` from `48rem` up. A block uses these names and sets no width or section padding of its own, so sections can be added, removed and reordered without the spacing drifting. Components use Tailwind's default spacing scale, which stays omitted from the token map because the project does not define a custom scale.
|
|
180
194
|
|
|
181
195
|
## Elevation & Depth
|
|
182
196
|
|
|
183
|
-
Surfaces use borders, rings, and tonal contrast instead of a shadow scale. The small shadow on the input is a component detail. Do not infer a project shadow scale from
|
|
197
|
+
Surfaces use borders, rings, and tonal contrast instead of a shadow scale. Landing cards add a hairline `shadow-xs`. Two elements carry a soft drop shadow tinted from `foreground`: the hero's product frame and the featured pricing tier. Those are the page's only raised surfaces. The small shadow on the input is a component detail. Do not infer a project shadow scale from any of them.
|
|
184
198
|
|
|
185
199
|
## Shapes
|
|
186
200
|
|
|
@@ -188,26 +202,36 @@ The base radius is `0.625rem`. Controls use the `lg` radius. Cards use `xl`. Lar
|
|
|
188
202
|
|
|
189
203
|
## Components
|
|
190
204
|
|
|
205
|
+
The landing page is thirteen blocks in `packages/ui/src/blocks/`, composed by explicit imports in `apps/web/src/pages/index.astro`: navbar, hero, logo-cloud, pain-points, how-it-works, feature-grid, showcase, audience, testimonials, pricing-table, faq, cta and footer. They share one vocabulary. A section opens with a centred `section-title` and `lead`. Cards are bordered at the `xl` radius. A product visual sits in a double frame: an outer bordered shell at `1.25rem` and an inner surface at `0.875rem`. Primary calls to action sit in a thin ringed wrapper beside a ghost button. Decoration is drawn from tokens: the `grid-pattern` utility under a radial mask, and a dashed rule for dividers inside a card and above the footer.
|
|
206
|
+
|
|
191
207
|
Primary buttons use the primary pair. Secondary buttons use the secondary pair. Destructive actions use the destructive token with restrained tint states. Inputs and buttons share a `2rem` default height and the `lg` radius. Cards use the card pair and a border-strength ring.
|
|
192
208
|
|
|
209
|
+
Cards in one row share one height. The grid stretches its cards, so it carries no `items-start`, and sibling cards hold the same number of one-line items. The pricing tiers are the worked example: five one-line bullets each. A card's primary action sits inside that card, on the primary button, never below or beside it.
|
|
210
|
+
|
|
211
|
+
A value that waits on a request shows the `Skeleton` primitive in its place, at the size the value will take, until the response arrives. It never shows a blank gap or a spinner. Add the primitive with `pnpm --filter @repo/ui exec shadcn add skeleton` the first time a page needs it.
|
|
212
|
+
|
|
193
213
|
The `error-state` block is the one screen for a failure: a wrong path, a failed render, a server error. It is a single centered card holding a muted icon, a status label in the mono face at the smallest size, a title at `body-lg`, a description in `muted-foreground`, and one or two actions on the button scale. It carries no destructive token and no alert tint, because a mistyped address is not a danger state. Every app renders this block rather than its own error markup, so a theme change reaches all three at once.
|
|
194
214
|
|
|
195
215
|
## Do's and Don'ts
|
|
196
216
|
|
|
197
|
-
- Read this file and `packages/ui/src/styles/
|
|
217
|
+
- Read this file and `packages/ui/src/styles/web.css` before you write UI.
|
|
198
218
|
- Use semantic token utilities such as `bg-primary` and `text-muted-foreground`.
|
|
199
219
|
- Keep light and dark token pairs together when you change a semantic role.
|
|
200
220
|
- Keep new pages within the established type scale and radius vocabulary.
|
|
201
221
|
- Do not add a color, radius, shadow, or type size that the code does not define.
|
|
202
222
|
- Do not use a shadow when a border or tonal surface provides the needed separation.
|
|
203
223
|
- Keep UI copy direct and specific. Use the product brief for product language.
|
|
224
|
+
- Do not put a value in copy that goes stale: a version number, a release date, a count. Say something general and link to where the current value lives, the way the hero pill says "See what's new" and links to the changelog.
|
|
225
|
+
- Open every link that leaves the site in a new tab. Spread `externalLinkProps(href)` from `@repo/ui/lib/external-link` onto the anchor and render `newTabHint(href)` inside it. A path on this site or a same-page anchor stays in the tab.
|
|
204
226
|
|
|
205
227
|
## Motion
|
|
206
228
|
|
|
207
|
-
|
|
229
|
+
Motion is CSS only and additive. With reduced motion requested, or in a browser that lacks the feature, every element sits in its final state. The named utilities in `web.css` are the whole vocabulary: `blur-in` (the hero's load-time cascade, staggered with `--blur-in-delay`), `beam-in` (the hero's two light beams, which show from their top-left end and widen just after the cascade starts, staggered with `--beam-in-delay`), `reveal` (a scroll-tied entrance below the fold), `marquee` (the logo row), `shine-text` (the announcement pill), `nav-float` (the navbar frosting once the page scrolls) and `hover-lift`. Blocks use these names and write no keyframes. Color and state changes use short Tailwind transitions. Motion stays in prose because the DESIGN.md alpha schema has no motion token group. A theme toggle press reveals the new theme as a circle that grows from the button, through a view transition whose animation lives in `globals.css`. The script only starts it, and reduced motion skips it.
|
|
208
230
|
|
|
209
231
|
## Dark Mode
|
|
210
232
|
|
|
211
233
|
The `.dark` class selects the dark token set. The pre-paint theme script applies the class before rendering and records the resolved mode in `data-theme`. Components must use semantic tokens so both themes stay aligned.
|
|
212
234
|
|
|
213
|
-
|
|
235
|
+
The theme toggle has two states, light and dark. `system` is only the starting state: a visitor who never pressed the toggle follows the OS. A press flips the resolved mode and stores the choice, and no press steps back to `system`. Do not add a third state or a light/dark/system menu to the toggle.
|
|
236
|
+
|
|
237
|
+
_Seeded from the saasaloy base template · CLI {{CLI_VERSION}} · tokens sha256:e25d78b57596 of packages/ui/src/styles/web.css_
|
|
@@ -5,6 +5,31 @@ import react from "@astrojs/react";
|
|
|
5
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
6
|
import { defineConfig } from "astro/config";
|
|
7
7
|
|
|
8
|
+
// `astro build` gets its own Vite cache. Both commands keep pre-bundled dependencies under
|
|
9
|
+
// node_modules/.vite, each stamped with a hash of the config that made them. The two
|
|
10
|
+
// hashes always differ, and Vite deletes a cache whose hash does not match. So a build
|
|
11
|
+
// run beside a live `astro dev` deletes the dev server's deps_ssr folder, and the next
|
|
12
|
+
// module the dev server loads from it fails with "The file does not exist at
|
|
13
|
+
// .../deps_ssr/<dep>.js which is in the optimize deps directory". A separate folder for
|
|
14
|
+
// the build keeps the two apart; dev keeps the default.
|
|
15
|
+
/** @returns {import("astro").AstroIntegration} the integration that moves the build cache */
|
|
16
|
+
function separateBuildCache() {
|
|
17
|
+
return {
|
|
18
|
+
name: "separate-build-cache",
|
|
19
|
+
hooks: {
|
|
20
|
+
"astro:config:setup": ({ command, updateConfig }) => {
|
|
21
|
+
if (command === "build") {
|
|
22
|
+
updateConfig({ vite: { cacheDir: "node_modules/.vite-build" } });
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Every React component the dev server can render, for Vite's dependency scan. See the
|
|
30
|
+
// `optimizeDeps` notes in `vite` below.
|
|
31
|
+
const COMPONENT_ENTRIES = ["src/**/*.tsx", "../../packages/ui/src/**/*.tsx"];
|
|
32
|
+
|
|
8
33
|
// Static output — the marketing site is content-first, and every page here is
|
|
9
34
|
// prerendered at build time. `output: "static"` is written out rather than left to the
|
|
10
35
|
// default so the next person changing this file has to mean it.
|
|
@@ -44,7 +69,7 @@ export default defineConfig({
|
|
|
44
69
|
// makes pnpm install the same plugin for the adapter. Move it and `wrangler` together:
|
|
45
70
|
// they release as a pair, and apps/api (once added) pins the same two versions.
|
|
46
71
|
adapter: cloudflare({ imageService: "compile" }),
|
|
47
|
-
integrations: [react()],
|
|
72
|
+
integrations: [react(), separateBuildCache()],
|
|
48
73
|
// Fixed dev port. Every cross-origin consumer in this repo — the api Worker's CORS
|
|
49
74
|
// allowlist, auth's `trustedOrigins`, the waitlist form's `PUBLIC_API_URL` fallback —
|
|
50
75
|
// hardcodes the localhost dev origins, so the port cannot be allowed to drift.
|
|
@@ -53,8 +78,9 @@ export default defineConfig({
|
|
|
53
78
|
server: { port: 3000 },
|
|
54
79
|
vite: {
|
|
55
80
|
// Tailwind 4 is a Vite plugin, not an Astro integration — `@astrojs/tailwind` is EOL
|
|
56
|
-
// and never supported v4. The theme
|
|
57
|
-
//
|
|
81
|
+
// and never supported v4. The theme lives in packages/ui: the @source globs in
|
|
82
|
+
// src/styles/globals.css, the tokens in src/styles/web.css, which Layout.astro
|
|
83
|
+
// imports via `@repo/ui/styles/web.css`.
|
|
58
84
|
plugins: [tailwindcss()],
|
|
59
85
|
server: { strictPort: true },
|
|
60
86
|
// Dev only: pre-bundle every dependency the React components import before the
|
|
@@ -67,10 +93,14 @@ export default defineConfig({
|
|
|
67
93
|
// A glob, not an include list, so a component a module adds is covered with no edit
|
|
68
94
|
// here.
|
|
69
95
|
ssr: {
|
|
70
|
-
optimizeDeps: {
|
|
71
|
-
entries: ["src/**/*.tsx", "../../packages/ui/src/**/*.tsx"],
|
|
72
|
-
},
|
|
96
|
+
optimizeDeps: { entries: COMPONENT_ENTRIES },
|
|
73
97
|
},
|
|
98
|
+
// The browser needs the same entries. Its scan reads an .astro page's <script> tags but
|
|
99
|
+
// not the frontmatter that imports an island, so the islands' own dependencies arrive
|
|
100
|
+
// mid-request. Vite re-bundles, renames the shared chunks, and deletes the ones the
|
|
101
|
+
// page already asked for: "The file does not exist at .../deps/react-dom-<hash>.js
|
|
102
|
+
// which is in the optimize deps directory", then a full reload.
|
|
103
|
+
optimizeDeps: { entries: COMPONENT_ENTRIES },
|
|
74
104
|
resolve: {
|
|
75
105
|
// `@web` mirrors saasaloy.json's alias of the same name (apps/web/src) — that
|
|
76
106
|
// alias only drives the CLI's file-placement when a module's files[] target
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "astro build",
|
|
9
9
|
"clean": "rimraf dist .astro .wrangler",
|
|
10
10
|
"preview": "astro preview",
|
|
11
|
-
"deploy": "
|
|
11
|
+
"deploy": "env-deploy web"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"astro": "7.3.5",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@astrojs/react": "7.0.0",
|
|
17
17
|
"react": "19.3.0",
|
|
18
18
|
"react-dom": "19.3.0",
|
|
19
|
+
"@repo/env": "workspace:*",
|
|
19
20
|
"@repo/ui": "workspace:*"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|