@se-studio/skills 1.0.6 → 1.0.11
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/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/skills/site-workflows-apply-design-snapshot/SKILL.md +285 -0
- package/skills/site-workflows-contentful-vercel-setup/SKILL.md +300 -0
- package/skills/site-workflows-copy-doc-snapshot/SKILL.md +222 -0
- package/skills/site-workflows-figma-design-snapshot/SKILL.md +390 -0
- package/skills/site-workflows-new-project/SKILL.md +278 -0
- package/skills/site-workflows-project-cleanup/SKILL.md +362 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @se-studio/skills
|
|
2
2
|
|
|
3
|
+
## 1.0.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Version bump: patch for changed packages
|
|
8
|
+
|
|
9
|
+
## 1.0.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Add site-workflows-contentful-vercel-setup skill
|
|
14
|
+
|
|
15
|
+
## 1.0.9
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Add site-workflows-copy-doc-snapshot skill
|
|
20
|
+
- Add site-workflows-new-project skill
|
|
21
|
+
- Add site-workflows-project-cleanup skill
|
|
22
|
+
|
|
23
|
+
## 1.0.8
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Add site-workflows-apply-design-snapshot skill
|
|
28
|
+
|
|
29
|
+
## 1.0.7
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Add site-workflows-figma-design-snapshot skill
|
|
34
|
+
|
|
3
35
|
## 1.0.6
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: site-workflows-apply-design-snapshot
|
|
3
|
+
description: Reads design.md and applies the design tokens to the project — updating tailwind.config.json (colours + typography), handling custom fonts, and regenerating all downstream files via pnpm codegen. Run after /figma-design-snapshot to sync the codebase with the latest Figma changes.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-studio
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Apply Design Snapshot
|
|
11
|
+
|
|
12
|
+
Reads `design.md` and applies its design tokens to the project. Run this after `/figma-design-snapshot` to keep the codebase in sync with Figma.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/apply-design-snapshot
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No arguments needed. Reads `design.md` and `.design-snapshot.json` from the project root, updates `tailwind.config.json` and `globals.css`, then runs `pnpm codegen` to regenerate all typed token files.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## CRITICAL: The token pipeline
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
tailwind.config.json
|
|
28
|
+
↓ pnpm codegen
|
|
29
|
+
src/generated/colors.ts ← typed colour lookups (auto-generated)
|
|
30
|
+
src/generated/textStyles.ts ← typed type-scale names (auto-generated)
|
|
31
|
+
src/generated/setailwind.css ← CSS custom properties + utilities (auto-generated)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Never edit files in `src/generated/` directly** — they are always overwritten by `codegen`. Only edit `tailwind.config.json` and `globals.css`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step 1 — Check prerequisites
|
|
39
|
+
|
|
40
|
+
1. Check that `design.md` exists in the project root (or at `outputPath` from `.design-snapshot.json`).
|
|
41
|
+
- If missing: stop and tell the user to run `/figma-design-snapshot` first.
|
|
42
|
+
2. Check that `tailwind.config.json` exists.
|
|
43
|
+
- If missing: stop and tell the user this skill requires the SE Studio token pipeline.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 2 — Parse design.md
|
|
48
|
+
|
|
49
|
+
Read `design.md` and extract:
|
|
50
|
+
|
|
51
|
+
### Colours
|
|
52
|
+
Find the `## Colour Palette` section. For every table row matching `| Token | Hex |`, extract the token name and hex value. Ignore swatch columns. Collect all colours regardless of group (Primary / Neutral / etc.).
|
|
53
|
+
|
|
54
|
+
### Typography
|
|
55
|
+
Find `## Typography`. Extract the **Desktop** table (lines under `### Desktop`) and the **Mobile** table (lines under `### Mobile`). For each row capture: style name, size (strip `px`), weight label, line-height, letter-spacing.
|
|
56
|
+
|
|
57
|
+
Weight label → number mapping:
|
|
58
|
+
- Regular → 400
|
|
59
|
+
- Medium → 500
|
|
60
|
+
- SemiBold → 600
|
|
61
|
+
- Bold → 700
|
|
62
|
+
|
|
63
|
+
Letter-spacing conversion (Figma `%` → CSS `em`): divide by 100. e.g. `−1.3%` → `-0.013em`. If the value is `0` keep it as `0`.
|
|
64
|
+
|
|
65
|
+
Line-height conversion: strip `%` and divide by 100. e.g. `120%` → `1.2`. If already a decimal, keep as-is. If in `px`, keep as the px value.
|
|
66
|
+
|
|
67
|
+
### Font family
|
|
68
|
+
Find `**Font family:**` in the Typography section. Extract the font name (e.g. `Booton-TRIAL`).
|
|
69
|
+
|
|
70
|
+
### Breakpoints
|
|
71
|
+
Find `## Breakpoints & Canvas`. Extract rows — identify the desktop canvas width (largest) and mobile canvas width (smallest).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Step 3 — Read current tailwind.config.json
|
|
76
|
+
|
|
77
|
+
Read `tailwind.config.json` in full. Record:
|
|
78
|
+
- `colorOptions` — the current colour map (must preserve all existing keys)
|
|
79
|
+
- `fontTable.font` — the current font family string
|
|
80
|
+
- `fontTable.weight` — the base font weight
|
|
81
|
+
- `fontTable.styles` — the current style map (keys are class names used in the codebase — **never rename or remove them**)
|
|
82
|
+
- `colorOpposites` — existing colour contrast pairs
|
|
83
|
+
- `foregroundColors` — existing foreground colour list
|
|
84
|
+
- `colorFile` — keep unchanged
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Step 4 — Ask about fonts
|
|
89
|
+
|
|
90
|
+
Before modifying any file, ask the user this question (use `AskUserQuestion`):
|
|
91
|
+
|
|
92
|
+
> The design uses **[font name from design.md]** (Regular 400, Medium 500, SemiBold 600).
|
|
93
|
+
>
|
|
94
|
+
> How should I handle the font?
|
|
95
|
+
>
|
|
96
|
+
> **A)** I have the font files — tell me the path (relative to project root or absolute) and I'll copy them to `public/fonts/` and wire up `@font-face`.
|
|
97
|
+
> **B)** I don't have the files yet — set up placeholder `@font-face` rules so I can drop files in later.
|
|
98
|
+
> **C)** Skip fonts — only update colours and type sizes.
|
|
99
|
+
|
|
100
|
+
Wait for the answer. Record as `fontStrategy` (A / B / C). If A, also record `fontSourcePath` from their reply.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Step 5 — Build the colour patch
|
|
105
|
+
|
|
106
|
+
Construct the updated `colorOptions` object:
|
|
107
|
+
|
|
108
|
+
1. **Start with all existing entries** exactly as they are. The `Light` and `Dark` keys are especially critical — they feed `lookupColourString()`, `lookupOpposite()`, and `lookupColourClassNames()` throughout the component system.
|
|
109
|
+
|
|
110
|
+
2. **Add new entries** from design.md. Skip any name that already exists (case-insensitive match). Use the exact name string from design.md as the key (the codegen tool kebab-cases it for CSS variables automatically).
|
|
111
|
+
|
|
112
|
+
3. **Compute colour opposites** for each new colour using WCAG relative luminance:
|
|
113
|
+
1. Parse the hex value into R, G, B as integers 0–255, then normalise to 0–1 by dividing by 255.
|
|
114
|
+
2. Linearise each channel: if c ≤ 0.03928 → `c / 12.92`, else `((c + 0.055) / 1.055) ^ 2.4`.
|
|
115
|
+
3. Compute luminance: `L = 0.2126·R + 0.7152·G + 0.0722·B`.
|
|
116
|
+
4. If `L ≤ 0.179` → the colour is dark; opposite is `"Light"` (white/light text will be legible).
|
|
117
|
+
If `L > 0.179` → the colour is light; opposite is `"Dark"` (dark text will be legible).
|
|
118
|
+
|
|
119
|
+
4. **Show the user** the full list of colours being added (name + hex + computed opposite) before writing anything. Ask them to confirm or correct any pairs.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Step 6 — Build the typography patch
|
|
124
|
+
|
|
125
|
+
Map Figma's style names to the existing project class names using this table. The left column is how the style appears in design.md; the right is the `fontTable.styles` key to update in `tailwind.config.json`.
|
|
126
|
+
|
|
127
|
+
| design.md style | tailwind.config.json key |
|
|
128
|
+
|---|---|
|
|
129
|
+
| H1 (Desktop) | `h1` |
|
|
130
|
+
| H2 (Desktop) | `h2` |
|
|
131
|
+
| H3 (Desktop) | `h3` |
|
|
132
|
+
| H4 (Desktop) | `h4` |
|
|
133
|
+
| P1 (Desktop) | `p1` |
|
|
134
|
+
| P2 (Desktop) | `p2` |
|
|
135
|
+
| P3 (Desktop) | `p3` |
|
|
136
|
+
| P4 (Desktop) | `p3Med` |
|
|
137
|
+
| Button 1 (Desktop) | `large-button` |
|
|
138
|
+
|
|
139
|
+
> **Note:** If your project uses different `fontTable.styles` keys, adjust the right-hand column to match what is in your `tailwind.config.json`. Keys not present in the table are left exactly as-is.
|
|
140
|
+
|
|
141
|
+
For each mapped style, construct the updated `fontTable.styles` entry following these rules:
|
|
142
|
+
|
|
143
|
+
- `"default"` = the **Mobile** size for that style (from the Mobile table in design.md)
|
|
144
|
+
- `"desktop"` = the **Desktop** size (only include if it differs from `"default"`)
|
|
145
|
+
- `"weight"` = only include when the weight is not the base `fontTable.weight` (typically 400)
|
|
146
|
+
- `"lineHeight"` = converted value (see Step 2 conversion rules)
|
|
147
|
+
- `"letterSpacing"` = converted value (see Step 2 conversion rules); omit if `0`
|
|
148
|
+
- `"additional"` = **preserve exactly** from the existing entry (e.g. `text-transform: uppercase` on `h1`) — do not remove it
|
|
149
|
+
- Style keys not in the mapping table above (e.g. `hHome`, `h2Med`, `h2plus`, `nav`, etc.) → **leave completely unchanged**
|
|
150
|
+
|
|
151
|
+
Example output for `h2`:
|
|
152
|
+
```json
|
|
153
|
+
"h2": {
|
|
154
|
+
"weight": 500,
|
|
155
|
+
"letterSpacing": "-0.013em",
|
|
156
|
+
"lineHeight": 1.2,
|
|
157
|
+
"default": 40,
|
|
158
|
+
"desktop": 51
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Step 7 — Build the font patch
|
|
165
|
+
|
|
166
|
+
### Strategy A — font files provided
|
|
167
|
+
|
|
168
|
+
1. List all font files at `fontSourcePath` matching `*.woff2`, `*.woff`, `*.ttf`, `*.otf`.
|
|
169
|
+
2. Create `public/fonts/` if it does not exist.
|
|
170
|
+
3. Copy all matched files into `public/fonts/`.
|
|
171
|
+
4. For each file, infer the face weight from the filename:
|
|
172
|
+
- Filename contains `Regular` or `400` → weight: 400, style: normal
|
|
173
|
+
- Contains `Medium` or `500` → weight: 500, style: normal
|
|
174
|
+
- Contains `SemiBold` or `Semi` or `600` → weight: 600, style: normal
|
|
175
|
+
- Contains `Bold` or `700` → weight: 700, style: normal
|
|
176
|
+
- Contains `Italic` → add `font-style: italic` alongside the matching weight
|
|
177
|
+
5. Build `@font-face` blocks — prefer `woff2` format; fall back to `woff` then `ttf`.
|
|
178
|
+
6. Insert the `@font-face` blocks into `globals.css` **before** the `@import "tailwindcss"` line (font declarations must precede Tailwind).
|
|
179
|
+
7. Update `tailwind.config.json` → `fontTable.font` to the font family name from design.md.
|
|
180
|
+
8. Update `globals.css` `@theme` block: replace the `--font-sans` value with `"[FontName]", system-ui, ui-sans-serif, sans-serif`.
|
|
181
|
+
|
|
182
|
+
### Strategy B — placeholder
|
|
183
|
+
|
|
184
|
+
1. Add the following block to `globals.css` before the `@import "tailwindcss"` line:
|
|
185
|
+
|
|
186
|
+
```css
|
|
187
|
+
/* TODO: Replace src with actual font files placed in public/fonts/ */
|
|
188
|
+
@font-face {
|
|
189
|
+
font-family: "[FontName]";
|
|
190
|
+
src: local("[FontName]");
|
|
191
|
+
font-weight: 400;
|
|
192
|
+
font-style: normal;
|
|
193
|
+
font-display: swap;
|
|
194
|
+
}
|
|
195
|
+
@font-face {
|
|
196
|
+
font-family: "[FontName]";
|
|
197
|
+
src: local("[FontName]");
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
font-style: normal;
|
|
200
|
+
font-display: swap;
|
|
201
|
+
}
|
|
202
|
+
@font-face {
|
|
203
|
+
font-family: "[FontName]";
|
|
204
|
+
src: local("[FontName]");
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
font-style: normal;
|
|
207
|
+
font-display: swap;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
2. Update `tailwind.config.json` → `fontTable.font` to the font family name.
|
|
212
|
+
3. Update `globals.css` `--font-sans` to `"[FontName]", system-ui, ui-sans-serif, sans-serif`.
|
|
213
|
+
4. After writing, tell the user: *"Place your font files in `public/fonts/` and replace each `src: local(...)` line with `src: url('/fonts/YourFile.woff2') format('woff2')`."*
|
|
214
|
+
|
|
215
|
+
### Strategy C — skip
|
|
216
|
+
|
|
217
|
+
No font changes. Skip this step entirely.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Step 8 — Apply all changes
|
|
222
|
+
|
|
223
|
+
Write files in this order:
|
|
224
|
+
|
|
225
|
+
1. **`tailwind.config.json`** — merge in the new `colorOptions`, updated `colorOpposites`, and updated `fontTable.styles`. All other fields (`sizes`, `colorFile`, `foregroundColors`, etc.) must be preserved exactly.
|
|
226
|
+
2. **`src/app/globals.css`** — apply font changes (strategies A or B only).
|
|
227
|
+
3. **`public/fonts/`** — copy font files (strategy A only).
|
|
228
|
+
|
|
229
|
+
Before writing `tailwind.config.json`, do a final sanity check:
|
|
230
|
+
- `Light` and `Dark` keys still present with their original hex values
|
|
231
|
+
- No existing `fontTable.styles` keys removed
|
|
232
|
+
- JSON is valid
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Step 9 — Regenerate
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pnpm codegen
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
If this fails:
|
|
243
|
+
- Show the error output
|
|
244
|
+
- Diagnose the likely cause (invalid JSON in tailwind.config.json, unknown field, etc.)
|
|
245
|
+
- Suggest a fix
|
|
246
|
+
- Do NOT run `pnpm validate` until codegen succeeds
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Step 10 — Validate
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
pnpm validate
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This runs Biome lint + TypeScript type-check. Report pass or fail. If it fails, show the errors.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Step 11 — Report to user
|
|
261
|
+
|
|
262
|
+
Summarise what happened:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
✓ Colours added: [N] new colours (list names)
|
|
266
|
+
✓ Typography updated: [list of class names updated]
|
|
267
|
+
✓ Font: [registered as FontName / placeholder set up / skipped]
|
|
268
|
+
✓ pnpm codegen: passed
|
|
269
|
+
✓ pnpm validate: passed (or: ✗ failed — see errors above)
|
|
270
|
+
|
|
271
|
+
Next steps:
|
|
272
|
+
git diff tailwind.config.json ← review the token changes
|
|
273
|
+
git diff src/app/globals.css ← review font changes
|
|
274
|
+
git add -p && git commit -m "design: apply snapshot YYYY-MM-DD"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## What this skill does NOT touch
|
|
280
|
+
|
|
281
|
+
- `design.md` — read-only input, never modified
|
|
282
|
+
- `.design-snapshot.json` — read-only config
|
|
283
|
+
- `src/generated/*` — auto-regenerated by codegen, never edited directly
|
|
284
|
+
- Any files in `src/project/` — components are not updated; only tokens change
|
|
285
|
+
- Existing `fontTable.styles` keys not in the mapping table — left exactly as-is
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: site-workflows-contentful-vercel-setup
|
|
3
|
+
description: Set up Contentful revalidation webhooks and live preview for a new SE Studio project on Vercel. Creates the Contentful webhook via the Management API and sets REVALIDATION_SECRET in Vercel. Run after site-workflows-new-project bootstrap when the Vercel project is deployed.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-studio
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Contentful & Vercel Webhook Setup
|
|
11
|
+
|
|
12
|
+
Wire up the Contentful revalidation webhook and live preview for an SE Studio project. The `/api/revalidate` route handler and `frame-ancestors` CSP are already in the template — this skill only configures them.
|
|
13
|
+
|
|
14
|
+
**Prerequisites:**
|
|
15
|
+
- `site-workflows-new-project` has been run (`.env.local` has `CONTENTFUL_SPACE_ID`, `CONTENTFUL_MANAGEMENT_TOKEN`, `CONTENTFUL_ENVIRONMENT_NAME`)
|
|
16
|
+
- The Vercel project is linked (`vercel whoami` succeeds) and has been deployed at least once
|
|
17
|
+
- `pnpm install` has been run
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Step 1 — Check prerequisites
|
|
22
|
+
|
|
23
|
+
Run these checks:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Confirm Vercel is linked
|
|
27
|
+
vercel whoami
|
|
28
|
+
cat .vercel/project.json 2>/dev/null || cat .vercel/repo.json 2>/dev/null
|
|
29
|
+
|
|
30
|
+
# Confirm required env vars are present in .env.local
|
|
31
|
+
grep -E "CONTENTFUL_SPACE_ID|CONTENTFUL_MANAGEMENT_TOKEN" .env.local
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If `CONTENTFUL_MANAGEMENT_TOKEN` is missing: tell the user to add it. It's found in Contentful → Settings → CMA Tokens → Generate personal token.
|
|
35
|
+
|
|
36
|
+
If the project isn't linked to Vercel: run the `vercel:deploy` skill first.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Step 2 — Ask for the Vercel production URL
|
|
41
|
+
|
|
42
|
+
Ask the user: **"What is the Vercel production URL for this project?"**
|
|
43
|
+
|
|
44
|
+
This is the only value not already in environment files. Examples:
|
|
45
|
+
- `https://my-project.vercel.app`
|
|
46
|
+
- `https://myclient.com` (custom domain, if configured)
|
|
47
|
+
|
|
48
|
+
Store this as `SITE_URL` for use in subsequent steps.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 3 — Create `scripts/setup-contentful-webhooks.ts`
|
|
53
|
+
|
|
54
|
+
Write this file to the project. It handles secret generation, webhook creation, and Vercel env var updates in one run.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
/**
|
|
58
|
+
* Set up Contentful revalidation webhook and Vercel env vars.
|
|
59
|
+
*
|
|
60
|
+
* Usage:
|
|
61
|
+
* pnpm tsx scripts/setup-contentful-webhooks.ts https://your-site.vercel.app
|
|
62
|
+
*
|
|
63
|
+
* Prerequisites:
|
|
64
|
+
* - CONTENTFUL_SPACE_ID and CONTENTFUL_MANAGEMENT_TOKEN set in .env.local
|
|
65
|
+
* - Vercel CLI authenticated (vercel whoami)
|
|
66
|
+
*
|
|
67
|
+
* What it does:
|
|
68
|
+
* 1. Generates REVALIDATION_SECRET if not already set
|
|
69
|
+
* 2. Creates (or replaces) the Contentful revalidation webhook
|
|
70
|
+
* 3. Sets REVALIDATION_SECRET in Vercel (production + preview)
|
|
71
|
+
* 4. Updates .env.local with the new secret
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
import { createClient } from 'contentful-management';
|
|
75
|
+
import { randomBytes } from 'node:crypto';
|
|
76
|
+
import { spawnSync } from 'node:child_process';
|
|
77
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
78
|
+
import { resolve } from 'node:path';
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Validate args
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const siteUrl = process.argv[2];
|
|
85
|
+
if (!siteUrl || !siteUrl.startsWith('https://')) {
|
|
86
|
+
console.error('Usage: pnpm tsx scripts/setup-contentful-webhooks.ts https://your-site.vercel.app');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Load .env.local
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
const envPath = resolve('.env.local');
|
|
95
|
+
let envContent = readFileSync(envPath, 'utf-8');
|
|
96
|
+
|
|
97
|
+
function getEnv(key: string): string {
|
|
98
|
+
const value = envContent.match(new RegExp(`^${key}=(.*)`, 'm'))?.[1]?.trim() ?? process.env[key];
|
|
99
|
+
if (!value) throw new Error(`${key} not found in .env.local or environment`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setEnvLocal(key: string, value: string): void {
|
|
104
|
+
if (new RegExp(`^${key}=`, 'm').test(envContent)) {
|
|
105
|
+
envContent = envContent.replace(new RegExp(`^${key}=.*`, 'm'), `${key}=${value}`);
|
|
106
|
+
} else {
|
|
107
|
+
envContent += `\n${key}=${value}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Set Vercel env var (pipe value via stdin to avoid shell escaping issues)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function vercelSetEnv(name: string, value: string, env: string): void {
|
|
116
|
+
spawnSync('vercel', ['env', 'rm', name, env, '--yes'], { stdio: 'inherit' });
|
|
117
|
+
|
|
118
|
+
const args =
|
|
119
|
+
env === 'preview'
|
|
120
|
+
? ['env', 'add', name, env, '']
|
|
121
|
+
: ['env', 'add', name, env];
|
|
122
|
+
|
|
123
|
+
const result = spawnSync('vercel', args, {
|
|
124
|
+
input: value,
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (result.status !== 0) {
|
|
130
|
+
throw new Error(`Failed to set ${name} in Vercel ${env}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Main
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const WEBHOOK_NAME = 'Vercel Revalidation';
|
|
139
|
+
|
|
140
|
+
async function main(): Promise<void> {
|
|
141
|
+
const managementToken = getEnv('CONTENTFUL_MANAGEMENT_TOKEN');
|
|
142
|
+
const spaceId = getEnv('CONTENTFUL_SPACE_ID');
|
|
143
|
+
|
|
144
|
+
// Generate secret if not already set
|
|
145
|
+
let secret = envContent.match(/^REVALIDATION_SECRET=(.+)/m)?.[1]?.trim();
|
|
146
|
+
if (!secret) {
|
|
147
|
+
secret = randomBytes(32).toString('base64url');
|
|
148
|
+
console.log('Generated new REVALIDATION_SECRET');
|
|
149
|
+
} else {
|
|
150
|
+
console.log('Using existing REVALIDATION_SECRET from .env.local');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const webhookUrl = `${siteUrl}/api/revalidate?secret=${secret}`;
|
|
154
|
+
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
// Create Contentful webhook
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
console.log('Connecting to Contentful…');
|
|
160
|
+
const client = createClient({ accessToken: managementToken });
|
|
161
|
+
|
|
162
|
+
// Delete existing webhook with the same name (idempotent)
|
|
163
|
+
const existing = await client.webhook.getMany({ spaceId, query: { limit: 100 } });
|
|
164
|
+
const duplicate = existing.items.find((w) => w.name === WEBHOOK_NAME);
|
|
165
|
+
if (duplicate) {
|
|
166
|
+
console.log(`Removing existing webhook: ${duplicate.sys.id}`);
|
|
167
|
+
await client.webhook.delete({ spaceId, webhookDefinitionId: duplicate.sys.id });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const topics = [
|
|
171
|
+
'Entry.publish',
|
|
172
|
+
'Entry.unpublish',
|
|
173
|
+
'Entry.delete',
|
|
174
|
+
'Entry.archive',
|
|
175
|
+
'Entry.unarchive',
|
|
176
|
+
'Asset.publish',
|
|
177
|
+
'Asset.unpublish',
|
|
178
|
+
'Asset.delete',
|
|
179
|
+
'Asset.archive',
|
|
180
|
+
'Asset.unarchive',
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
console.log(`Creating webhook → ${webhookUrl}`);
|
|
184
|
+
const webhook = await client.webhook.create(
|
|
185
|
+
{ spaceId },
|
|
186
|
+
{ name: WEBHOOK_NAME, url: webhookUrl, topics, active: true, headers: [] },
|
|
187
|
+
);
|
|
188
|
+
console.log(`Webhook created: ${webhook.sys.id}`);
|
|
189
|
+
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
// Update Vercel env vars
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
for (const env of ['production', 'preview'] as const) {
|
|
195
|
+
console.log(`Setting REVALIDATION_SECRET in Vercel ${env}…`);
|
|
196
|
+
vercelSetEnv('REVALIDATION_SECRET', secret, env);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// Update .env.local
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
setEnvLocal('REVALIDATION_SECRET', secret);
|
|
204
|
+
writeFileSync(envPath, envContent);
|
|
205
|
+
console.log('Updated .env.local');
|
|
206
|
+
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
// Summary
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
console.log('\nDone!');
|
|
212
|
+
console.log(` Webhook: ${WEBHOOK_NAME} (${webhook.sys.id})`);
|
|
213
|
+
console.log(` URL: ${webhookUrl}`);
|
|
214
|
+
console.log(` Secret: ${secret.slice(0, 8)}…`);
|
|
215
|
+
console.log('\nNext: set up Content Preview in Contentful (see skill Step 5)');
|
|
216
|
+
console.log('Then redeploy to pick up the new secret:');
|
|
217
|
+
console.log(' vercel deploy --prod');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch((err) => {
|
|
221
|
+
console.error(err instanceof Error ? err.message : err);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Step 4 — Run the script
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pnpm tsx scripts/setup-contentful-webhooks.ts <SITE_URL>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Replace `<SITE_URL>` with the URL from Step 2.
|
|
235
|
+
|
|
236
|
+
Report the webhook ID and the truncated secret to the user so they can confirm the output looks correct.
|
|
237
|
+
|
|
238
|
+
If the script fails:
|
|
239
|
+
- `CONTENTFUL_MANAGEMENT_TOKEN not found` → token missing from `.env.local`
|
|
240
|
+
- `vercel: command not found` → install with `npm i -g vercel` then `vercel login`
|
|
241
|
+
- Contentful 403 → management token may be expired; regenerate in Contentful → Settings → CMA Tokens
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Step 5 — Set up Content Preview (Live Preview / Inspector mode)
|
|
246
|
+
|
|
247
|
+
This lets editors open the live site directly from a Contentful entry and use inspector mode (click a field to highlight it in the preview iframe).
|
|
248
|
+
|
|
249
|
+
Guide through: **Contentful → Settings → Content preview → Add content preview**
|
|
250
|
+
|
|
251
|
+
Settings:
|
|
252
|
+
- **Name**: `Vercel Preview`
|
|
253
|
+
- For each content type that maps to a page (usually "Page", "Article", "Person" — check the project's `src/lib/registrations.ts` for what's registered):
|
|
254
|
+
- Enable that content type
|
|
255
|
+
- Set the preview URL to: `https://<SITE_URL>/preview?id={entry.sys.id}`
|
|
256
|
+
|
|
257
|
+
The `/preview` route in the template resolves the entry ID to the correct page URL and redirects.
|
|
258
|
+
|
|
259
|
+
**Note:** If this project has a separate staging Vercel deployment with `DRAFT_ONLY=true` (so editors see unpublished content), use that deployment's URL for the preview URL instead of production.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Step 6 — Redeploy and test
|
|
264
|
+
|
|
265
|
+
Redeploy so the new `REVALIDATION_SECRET` env var is live:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
vercel deploy --prod
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Or push to the production branch if the project uses git-based deploys.
|
|
272
|
+
|
|
273
|
+
#### Test the revalidation webhook
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
curl -X POST "https://<SITE_URL>/api/revalidate?secret=<REVALIDATION_SECRET>"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Expected: `200 OK`. A `401` means the secret in the URL doesn't match the deployed env var (check Vercel dashboard → Settings → Environment Variables).
|
|
280
|
+
|
|
281
|
+
#### Confirm webhook delivery in Contentful
|
|
282
|
+
|
|
283
|
+
1. Publish any entry in Contentful
|
|
284
|
+
2. Contentful → Settings → Webhooks → click the webhook → Activity Log
|
|
285
|
+
3. Confirm the most recent call shows `200`
|
|
286
|
+
|
|
287
|
+
#### Test Content Preview
|
|
288
|
+
|
|
289
|
+
1. Open any Page entry in Contentful
|
|
290
|
+
2. Click the preview icon (eye / open in new tab)
|
|
291
|
+
3. The site opens in an iframe showing that page
|
|
292
|
+
4. Clicking a text field in the Contentful sidebar should highlight the corresponding element in the preview (inspector mode — enabled by the `frame-ancestors` CSP already in `next.config.ts`)
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## What to do next
|
|
297
|
+
|
|
298
|
+
- **If inspector mode doesn't highlight fields**: check that `getPreviewFieldProps` is applied to field containers in the components. See `.cursorrules` for the pattern.
|
|
299
|
+
- **For draft content visibility**: set `DRAFT_ONLY=true` in Vercel on a preview/staging environment (not production) so editors can see unpublished content without affecting live users.
|
|
300
|
+
- **To rotate the secret later**: re-run `scripts/setup-contentful-webhooks.ts` — it will generate a new secret and update everything automatically.
|