@se-studio/skills 1.0.5 → 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 +38 -0
- package/package.json +1 -1
- package/skills/contentful-cms-setup/SKILL.md +91 -0
- 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
|
@@ -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.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: site-workflows-copy-doc-snapshot
|
|
3
|
+
description: Syncs content/copy/*.md files from the Google Docs copy document. Run whenever the copy doc is updated to pull changes into git. Produces a diff-friendly merge — CMS annotations and sync-to-CMS state are never touched.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-studio
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Copy Doc Snapshot
|
|
11
|
+
|
|
12
|
+
Pulls the current state of the Google Docs copy document and merges it into the local `content/copy/*.md` files. Running the skill twice with no doc changes produces no diff — only real copy edits show up in git.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/copy-doc-snapshot
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No arguments — reads config from `.copy-doc-sync.json` in the project root.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 1 — Read config
|
|
25
|
+
|
|
26
|
+
Read `.copy-doc-sync.json` from the project root. Shape:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"googleDocId": "...",
|
|
31
|
+
"outputDir": "content/copy",
|
|
32
|
+
"pages": {
|
|
33
|
+
"Homepage": { "file": "homepage.md", "slug": "/", "title": "Site Homepage" }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If the file does not exist, stop and tell the user: "`.copy-doc-sync.json` not found. Create it in the project root with a `googleDocId` and `pages` map."
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Step 2 — Fetch the Google Doc
|
|
43
|
+
|
|
44
|
+
Call `mcp__claude_ai_Google_Drive__read_file_content` with `fileId` set to `googleDocId` from config.
|
|
45
|
+
|
|
46
|
+
If the call fails, stop immediately — do not proceed with stale or partial data. Report the error.
|
|
47
|
+
|
|
48
|
+
Store the full document text as `docText`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 3 — Parse the doc into pages and sections
|
|
53
|
+
|
|
54
|
+
The Google Docs MCP returns the document as markdown-like plain text. The structure uses:
|
|
55
|
+
|
|
56
|
+
- `# Page Name` — H1 line = page boundary. The text after `#` is the page name.
|
|
57
|
+
- `**Section Name**` — A standalone bold line (nothing before or after the `**...**` on the line) = component/section heading.
|
|
58
|
+
- `**Field:** value` or `**Field:**` (labeled line) — field label. Value follows on the same line or on the next non-empty line.
|
|
59
|
+
- Plain text lines after a `**Copy:**` or `**Header:**` label = field value content.
|
|
60
|
+
- `\[placeholder\]` and `TBC` in field values = content not yet confirmed.
|
|
61
|
+
|
|
62
|
+
**Stop processing at `# Current website copy`** — everything from that H1 onwards is reference material and must be ignored.
|
|
63
|
+
|
|
64
|
+
### Parsing pass
|
|
65
|
+
|
|
66
|
+
Split `docText` on H1 headings (`^# .+`) to get page segments. For each page segment:
|
|
67
|
+
|
|
68
|
+
1. The page name is the H1 text (trimmed, without the `#`).
|
|
69
|
+
2. Split the page body on standalone bold lines (`^\*\*[A-Z][^*:]+\*\*\s*$`) to get section segments.
|
|
70
|
+
- A "standalone bold line" is bold text with no other content on the same line and no colon inside it.
|
|
71
|
+
3. For each section segment, extract:
|
|
72
|
+
- `name` — the bold line text (strip `**` markers)
|
|
73
|
+
- `heading` — value after `**Header:**` label (same line or next non-empty line); `null` if not present
|
|
74
|
+
- `subheading` — the first plain-text line after the heading line that is NOT a labeled field; `null` if not present. (Used to detect section-level subtitles like "Studio technology, evidence generation...")
|
|
75
|
+
- `body` — all lines between `**Copy:**` and the next labeled field or section header; joined as paragraphs
|
|
76
|
+
- `links` — all lines between `**CTAs:**` and the next labeled field or section header; one link per line
|
|
77
|
+
- `items` — if the body contains sub-section structure (nested bold names like `**Studio**`, `**Evidence**`), extract each as a sub-item with its own `name`, `subheading`, and `body`
|
|
78
|
+
- `isTBC` — `true` if the `heading` value is literally `TBC` AND the `body` is empty or also `TBC`
|
|
79
|
+
|
|
80
|
+
### Recognising collection items within a section
|
|
81
|
+
|
|
82
|
+
Some sections (e.g. "What We Do", "Featured Research", "From The Blog") contain multiple items structured as:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
**Item Name**
|
|
86
|
+
Subtitle line (optional)
|
|
87
|
+
Body copy here.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Detect this pattern: after the section-level `**Header:**` and `**Copy:**`, if the body contains lines that are standalone bold (`**...**`) followed by non-bold lines, treat each bold+following-text block as a separate item with:
|
|
91
|
+
- `name` = bold text
|
|
92
|
+
- `subheading` = first non-bold line (if short and no verb — likely a descriptor like "Technology & software")
|
|
93
|
+
- `body` = remaining non-bold lines before the next bold item
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Step 4 — For each page in config, merge doc content into the markdown file
|
|
98
|
+
|
|
99
|
+
Work through each entry in `config.pages`. For each page:
|
|
100
|
+
|
|
101
|
+
### 4a. Locate doc content
|
|
102
|
+
|
|
103
|
+
Find the parsed page segment whose name matches the config key (case-insensitive). If not found, note it in the report as "not found in doc" and skip to the next page — do not modify the file.
|
|
104
|
+
|
|
105
|
+
### 4b. Read the existing markdown file
|
|
106
|
+
|
|
107
|
+
Read `{outputDir}/{file}`. If it does not exist, create a minimal stub:
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
---
|
|
111
|
+
slug: {slug}
|
|
112
|
+
title: {title}
|
|
113
|
+
description: TBC
|
|
114
|
+
indexed: true
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
# {title}
|
|
118
|
+
|
|
119
|
+
> Last synced from Google Docs: never
|
|
120
|
+
> Last synced to CMS: never
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Then proceed to 4c using just the doc content (no existing sections to merge with).
|
|
124
|
+
|
|
125
|
+
If it does exist, parse it to identify:
|
|
126
|
+
- The YAML frontmatter block (lines between first `---` pair)
|
|
127
|
+
- The `> Last synced from Google Docs:` line (may be absent — treat as `never`)
|
|
128
|
+
- The `> Last synced to CMS:` line
|
|
129
|
+
- Each `## Section` block with its `<!-- ... -->` annotation line and all fields
|
|
130
|
+
- Each `### Item` block within collections, with their `<!-- item ... -->` annotation lines
|
|
131
|
+
|
|
132
|
+
### 4c. Merge doc sections into existing file structure
|
|
133
|
+
|
|
134
|
+
For each `## Section` in the existing file:
|
|
135
|
+
|
|
136
|
+
**Case 1 — Section found in doc (name match, case-insensitive):**
|
|
137
|
+
- Update `**heading:**` with doc `heading` value (or remove the field if `null`)
|
|
138
|
+
- Update `**body:**` with doc `body` text, reflowed as markdown paragraphs
|
|
139
|
+
- Update `**links:**` with doc `links`, one bullet per line, preserving any `(internal: ...)` or `(external: ...)` annotations that already exist in the file for links with matching labels
|
|
140
|
+
- For collection sections with items: recursively apply the same logic to each `### Item` — match by item name, update `heading`, `preHeading` (from doc `subheading`), `body`
|
|
141
|
+
- **Preserve unchanged:** the `<!-- component: ... | cmsLabel: ... -->` annotation, the `<!-- item | cmsLabel: ... -->` annotations, any `**preHeading:**` fields not provided by the doc, and any `<!-- note: ... -->` lines already present
|
|
142
|
+
|
|
143
|
+
**Case 2 — Section is TBC in doc (`isTBC` is true):**
|
|
144
|
+
- Do NOT update any content fields — leave them exactly as they are
|
|
145
|
+
- Add `<!-- note: TBC in Google Doc as of {today} -->` on the line after the `<!-- component: ... -->` annotation, but only if such a note is not already present
|
|
146
|
+
|
|
147
|
+
**Case 3 — Section exists in file but is NOT in the doc:**
|
|
148
|
+
- Preserve the section and all its content unchanged
|
|
149
|
+
- Add `<!-- note: Not found in Google Doc as of {today} — verify still needed -->` after the section's annotation comment, but only if such a note is not already present
|
|
150
|
+
- Flag this section in the report
|
|
151
|
+
|
|
152
|
+
**Case 4 — Section exists in doc but NOT in the existing file:**
|
|
153
|
+
- Append a new `## Section` block at the end of the file (before any Footer section) using the doc content
|
|
154
|
+
- Use this annotation as placeholder: `<!-- component: Generic Component | cmsLabel: {SECTION_NAME} — {PAGE_TITLE} -->`
|
|
155
|
+
- Flag this in the report as "new section — annotation needs review"
|
|
156
|
+
|
|
157
|
+
### 4d. Preserve these elements unchanged in all cases
|
|
158
|
+
|
|
159
|
+
- The entire YAML frontmatter block
|
|
160
|
+
- `> Last synced to CMS:` line (never touch this)
|
|
161
|
+
- `<!-- component: ... | cmsLabel: ... -->` annotation content
|
|
162
|
+
- `<!-- item | cmsLabel: ... -->` annotation content
|
|
163
|
+
- Any `<!-- note: ... -->` lines already in the file (do not duplicate them)
|
|
164
|
+
- The `## Footer` section (it is reference-only and managed separately via `cms-edit nav`)
|
|
165
|
+
|
|
166
|
+
### 4e. Update the sync date
|
|
167
|
+
|
|
168
|
+
Get today's date: run `date -u +"%Y-%m-%d"` via Bash. Update the `> Last synced from Google Docs:` line to this date. If the line does not exist in the file, insert it immediately after the H1 heading line and before the first `---` separator.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Step 5 — Write files
|
|
173
|
+
|
|
174
|
+
For each page, compare the updated content to the file on disk. If the content is identical (character-for-character), skip writing — do not produce a noisy no-op diff.
|
|
175
|
+
|
|
176
|
+
Otherwise, write the updated content to the file path. Do not reformat YAML frontmatter or alter indentation in sections that were not modified.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Step 6 — Report
|
|
181
|
+
|
|
182
|
+
After all pages are processed, print a summary in this format:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
copy-doc-snapshot complete (YYYY-MM-DD)
|
|
186
|
+
|
|
187
|
+
homepage.md — updated: Hero body, Trusted By body
|
|
188
|
+
about-us.md — no changes (TBC in doc)
|
|
189
|
+
contact-us.md — no changes (TBC in doc)
|
|
190
|
+
|
|
191
|
+
Attention needed:
|
|
192
|
+
homepage.md › Footer: not found in Google Doc — verify still needed
|
|
193
|
+
homepage.md › Services: new section in doc — annotation placeholder added, review cmsLabel
|
|
194
|
+
|
|
195
|
+
New pages in doc not in config (add to .copy-doc-sync.json to enable syncing):
|
|
196
|
+
(none)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If there are no attention items, omit the "Attention needed" block.
|
|
200
|
+
|
|
201
|
+
Finally, suggest the commit message: `git add content/copy && git commit -m "copy: sync from Google Doc (YYYY-MM-DD)"`
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Idempotency rules
|
|
206
|
+
|
|
207
|
+
Running the skill twice on the same day with no doc changes must produce identical files and an identical report:
|
|
208
|
+
|
|
209
|
+
- Never include anything in file content that changes between runs except the `> Last synced from Google Docs:` date line — and that only changes once per day
|
|
210
|
+
- Do not reorder sections, items, or links relative to their existing order in the file
|
|
211
|
+
- Preserve exact whitespace in sections that were not modified
|
|
212
|
+
- Do not add duplicate `<!-- note: ... -->` comments — check if the note already exists before inserting
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Notes
|
|
217
|
+
|
|
218
|
+
- The skill **never touches** `> Last synced to CMS:` — that line is owned by the `contentful-cms-core` workflow
|
|
219
|
+
- The skill **never creates** new page files unless they were already in config — a new page in the doc requires a human to add it to `.copy-doc-sync.json` with its slug
|
|
220
|
+
- The `# Current website copy` section in the doc is legacy reference material — always ignore it and everything after it
|
|
221
|
+
- Bold lines that are subtitles (e.g., `**Homepage copy**`) rather than section headings can be identified because they appear immediately after a page H1 and contain no matching section in the file — ignore them
|
|
222
|
+
- The `.copy-doc-sync.json` config should be committed to git so any team member can regenerate the snapshot
|