@silverassist/agents-toolkit 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -35,7 +35,7 @@ URL for them above, as well as copies of any plain-text lines
35
35
  beginning with `Required Notice:` that the licensor provided
36
36
  with the software. For example:
37
37
 
38
- > Required Notice: Copyright SilverAssist (https://silverassist.com)
38
+ > Required Notice: Copyright SilverAssist (<https://silverassist.com>)
39
39
 
40
40
  ## Changes and New Works License
41
41
 
@@ -132,4 +132,4 @@ of your licenses.
132
132
 
133
133
  ---
134
134
 
135
- Required Notice: Copyright 2026 SilverAssist (https://silverassist.com)
135
+ Required Notice: Copyright 2026 SilverAssist (<https://silverassist.com>)
package/README.md CHANGED
@@ -231,6 +231,7 @@ The same set of prompts is available for all supported tools.
231
231
  | `create-github-pr` | Create a pull request (GitHub) | `{issue-number}` | GitHub |
232
232
  | `finalize-pr` | Finalize and merge PR (Jira) | `{ticket-id}` | Jira |
233
233
  | `finalize-github-pr` | Finalize and merge PR (GitHub) | `{issue-number}` | GitHub |
234
+ | `prepare-github-release` | Prepare a GitHub release (auto-detects WordPress vs Node, tag vs Release) | — | GitHub |
234
235
 
235
236
  ### Utility
236
237
 
@@ -416,6 +417,8 @@ Reusable prompt fragments shared between tools:
416
417
  | `github-integration.md` | GitHub issue operations (MCP) |
417
418
  | `documentation.md` | Documentation standards |
418
419
  | `pr-template.md` | Pull request templates (GitHub Issues + Jira) |
420
+ | `release-node.md` | Node/npm release bump & quality checks (used by `prepare-github-release`) |
421
+ | `release-wordpress.md` | WordPress plugin release bump & quality checks (used by `prepare-github-release`) |
419
422
 
420
423
  ## Instructions
421
424
 
@@ -428,6 +431,8 @@ File-type specific guidelines applied automatically by Copilot and available as
428
431
  | `server-actions.instructions.md` | `**/actions/*.ts` | Next.js Server Actions |
429
432
  | `tests.instructions.md` | `*.test.ts, *.test.tsx` | Testing patterns |
430
433
  | `css-styling.instructions.md` | `*.css, *.tsx` | Tailwind CSS & shadcn/ui standards |
434
+ | `caching.instructions.md` | `next.config.*, src/proxy.ts, src/lib/**, **/route.ts, **/page.tsx` | Next.js caching: read-vs-mutation fetch caching, ISR tiers, CDN invalidation |
435
+ | `seo-ai-optimization.instructions.md` | `*.tsx` | Semantic HTML, accessibility tree, metadata, JSON-LD & E-E-A-T for AI Search |
431
436
 
432
437
  ## Skills
433
438
 
@@ -437,6 +442,7 @@ Specialized knowledge guides for domain-specific patterns:
437
442
  |-------|-------------|
438
443
  | `component-architecture` | React component patterns, folder structure, naming conventions |
439
444
  | `domain-driven-design` | DDD principles, domain organization, barrel exports |
445
+ | `nextjs-caching` | Next.js caching strategy: read-vs-mutation fetch, ISR tiers, CDN invalidation, diagnosing dynamic-render leaks |
440
446
  | `testing-patterns` | Jest + RTL patterns for Next.js 15 and Server Actions |
441
447
 
442
448
  Skills follow the [`npx skills`](https://github.com/vercel-labs/skills) standard: the real files live once in the canonical `.agents/skills/` store, and each agent's `skills/` directory symlinks to it (single source of truth, easy updates). Pass `--copy` to materialize real copies instead.
package/bin/cli.js CHANGED
@@ -51,7 +51,7 @@ const DEFAULT_CONFIG = {
51
51
  */
52
52
  const FILE_CATEGORIES = {
53
53
  instructions: {
54
- react: ['css-styling', 'react-components', 'server-actions', 'tests', 'typescript'],
54
+ react: ['caching', 'css-styling', 'react-components', 'seo-ai-optimization', 'server-actions', 'tests', 'typescript'],
55
55
  wordpress: ['php-standards', 'wordpress-plugin-architecture', 'testing-standards'],
56
56
  universal: ['documentation-language', 'github-workflow'],
57
57
  },
@@ -62,18 +62,20 @@ const FILE_CATEGORIES = {
62
62
  'analyze-ticket', 'work-ticket', 'analyze-github-issue', 'work-github-issue',
63
63
  'create-plan', 'create-pr', 'prepare-pr', 'finalize-pr',
64
64
  'create-github-pr', 'finalize-github-pr',
65
- 'review-code', 'fix-issues', 'add-tests', 'prepare-release',
65
+ 'review-code', 'fix-issues', 'add-tests', 'prepare-github-release',
66
66
  ],
67
67
  jira: ['analyze-ticket', 'work-ticket', 'create-pr', 'finalize-pr'],
68
68
  github: ['analyze-github-issue', 'work-github-issue', 'create-github-pr', 'finalize-github-pr'],
69
69
  },
70
70
  partials: {
71
+ react: ['release-node'],
72
+ wordpress: ['release-wordpress'],
71
73
  jira: ['jira-integration'],
72
74
  github: ['github-integration'],
73
75
  universal: ['git-operations', 'pr-template', 'validations', 'documentation'],
74
76
  },
75
77
  skills: {
76
- react: ['component-architecture', 'testing-patterns'],
78
+ react: ['component-architecture', 'nextjs-caching', 'testing-patterns'],
77
79
  wordpress: ['create-component', 'plugin-creation', 'quality-checks', 'testing'],
78
80
  universal: ['domain-driven-design', 'release-management'],
79
81
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverassist/agents-toolkit",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Reusable AI agent prompts for development workflows with Jira integration — supports GitHub Copilot, Claude Code, and Codex",
5
5
  "author": "Santiago Ramirez",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module @silverassist/agents-toolkit
4
4
  */
5
5
 
6
- export const VERSION = "2.4.0";
6
+ export const VERSION = "2.5.1";
7
7
 
8
8
  export const PROMPTS = {
9
9
  workflow: [
@@ -14,8 +14,8 @@ export const PROMPTS = {
14
14
  "create-pr",
15
15
  "finalize-github-pr",
16
16
  "finalize-pr",
17
+ "prepare-github-release",
17
18
  "prepare-pr",
18
- "prepare-release",
19
19
  "work-github-issue",
20
20
  "work-ticket",
21
21
  ],
@@ -36,15 +36,19 @@ export const PARTIALS = [
36
36
  "github-integration",
37
37
  "jira-integration",
38
38
  "pr-template",
39
+ "release-node",
40
+ "release-wordpress",
39
41
  "validations",
40
42
  ];
41
43
 
42
44
  export const INSTRUCTIONS = [
45
+ "caching",
43
46
  "css-styling",
44
47
  "documentation-language",
45
48
  "github-workflow",
46
49
  "php-standards",
47
50
  "react-components",
51
+ "seo-ai-optimization",
48
52
  "server-actions",
49
53
  "testing-standards",
50
54
  "tests",
@@ -57,6 +61,7 @@ export const SKILLS = [
57
61
  "component-architecture",
58
62
  "create-component",
59
63
  "domain-driven-design",
64
+ "nextjs-caching",
60
65
  "plugin-creation",
61
66
  "quality-checks",
62
67
  "release-management",
@@ -13,11 +13,13 @@
13
13
 
14
14
  ```
15
15
  [Instructions]|root:.github/instructions
16
- |css-styling.instructions.md CSS/Tailwind patterns, cn() utility, responsive design
17
- |react-components.instructions.md Component structure, exports, props, early returns
18
- |server-actions.instructions.md → Server action patterns, validation, error handling
19
- |tests.instructions.md Test structure, mocking, assertions
20
- |typescript.instructions.md Type safety, destructuring, JSDoc
16
+ |caching.instructions.md Next.js caching: read-vs-mutation fetch, ISR tiers, CDN invalidation
17
+ |css-styling.instructions.md CSS/Tailwind patterns, cn() utility, responsive design
18
+ |react-components.instructions.md → Component structure, exports, props, early returns
19
+ |seo-ai-optimization.instructions.md Semantic HTML, a11y tree, metadata, JSON-LD for AI Search
20
+ |server-actions.instructions.md Server action patterns, validation, error handling
21
+ |tests.instructions.md → Test structure, mocking, assertions
22
+ |typescript.instructions.md → Type safety, destructuring, JSDoc
21
23
 
22
24
  [Prompts]|root:.github/prompts
23
25
  |add-tests,analyze-ticket,create-plan,create-pr,finalize-pr,fix-issues,prepare-pr,review-code,work-ticket
@@ -133,6 +135,30 @@ import { myFunction } from '@/lib/my-module';
133
135
 
134
136
  ---
135
137
 
138
+ ## 🗄️ Caching Rules (CRITICAL)
139
+
140
+ | Rule | Requirement |
141
+ |------|-------------|
142
+ | **Cache by intent** | Cache reads, never mutations — decide on intent, NOT the HTTP method |
143
+ | **POST reads** | A `POST` read (e.g. filter/`geo-search`) MUST set `next: { revalidate, tags }` to cache |
144
+ | **Mutations** | `submit`/lead/`PUT`/`DELETE` → `cache: "no-store"` (flag with `mutation: true`) |
145
+ | **ISR** | Every cacheable route exports `revalidate`; never on form/personalized routes |
146
+ | **Invalidation** | `/api/revalidate` calls `revalidateTag`/`revalidatePath` AND the CDN invalidation |
147
+
148
+ ```ts
149
+ // ❌ WRONG: leaves POST reads uncached → route turns dynamic (private, no-store)
150
+ next: method === "GET" ? { revalidate, tags } : { revalidate: 0 }
151
+
152
+ // ✅ CORRECT: reads cache regardless of method; mutations opt out
153
+ const isMutation = mutation || method === "PUT" || method === "DELETE";
154
+ ...(isMutation ? { cache: "no-store" } : { next: { revalidate, tags } })
155
+ ```
156
+
157
+ 📄 **Full details:** `.github/instructions/caching.instructions.md`
158
+ 📄 **Project-specific context (if present):** `docs/CACHING.md`
159
+
160
+ ---
161
+
136
162
  ## 📝 Git Conventions
137
163
 
138
164
  | Type | Format |
@@ -190,6 +216,8 @@ Before ANY push to dev/staging/main:
190
216
  | Creating/editing components | `react-components.instructions.md` |
191
217
  | Writing CSS/Tailwind | `css-styling.instructions.md` |
192
218
  | Creating server actions | `server-actions.instructions.md` |
219
+ | Data fetching / `next.config` / routes / ISR / revalidate | `caching.instructions.md` |
220
+ | Pages, metadata, JSON-LD, accessibility/SEO | `seo-ai-optimization.instructions.md` |
193
221
  | Writing tests | `tests.instructions.md` |
194
222
  | TypeScript questions | `typescript.instructions.md` |
195
223
  | **Before pushing/PR** | `tests.instructions.md` + run quality checks |
@@ -0,0 +1,145 @@
1
+ ---
2
+ applyTo: "**/next.config.*,**/src/proxy.ts,**/src/lib/**/*.ts,**/src/app/**/route.ts,**/src/app/**/page.tsx"
3
+ ---
4
+ # Caching Standards (CRITICAL)
5
+
6
+ These rules are **mandatory** when touching data fetching, route segment configs, the asset/page
7
+ proxy, image config, or the on-demand revalidate route. Caching has several **independent layers**
8
+ (ISR page cache, data-fetch cache, request dedup, asset proxy, edge/CDN) — fixing one does not fix
9
+ the others.
10
+
11
+ > Apply to any Silver Side Next.js (App Router) frontend consuming the CCDS API and/or WordPress
12
+ > (headless GraphQL). If the project keeps a `docs/CACHING.md`, treat it as the extended reference.
13
+
14
+ ## Hard rules
15
+
16
+ 1. **Cache reads, not mutations — decide on intent, NEVER on the HTTP method.**
17
+ Some reads must use `POST` because they take a body (e.g. CCDS `geo-search`). They still have to
18
+ cache like a `GET`. Mutations (`submit-review`, lead submit, and any `PUT`/`DELETE`/`PATCH`) must
19
+ stay uncached with `cache: "no-store"`. Expose a `mutation: true` flag on the client and mark every
20
+ write with it.
21
+ - ❌ **Never** gate caching on `method === "GET"` (e.g. `next: method === "GET" ? {...} : { revalidate: 0 }`).
22
+ That leaves POST reads uncached and forces the whole route into dynamic rendering
23
+ (`cache-control: private, no-store`).
24
+
25
+ 2. **A `POST` read caches its DATA with `next` options — but the ROUTE still renders dynamically.**
26
+ Next.js does not cache `POST` by default; any read `fetch` (GET or POST) to CCDS or the WP GraphQL
27
+ endpoint must set `next: { revalidate, tags }` (the body is part of the cache key, so different
28
+ filter bodies cache independently). **However** (WEB-1069, Next 16) a page whose data comes from a
29
+ POST read still prerenders as `ƒ` (Dynamic) and serves `cache-control: private, no-store` even with
30
+ those options — the data-fetch cache and the route's static/dynamic classification are independent.
31
+ So `next: { revalidate, tags }` is **necessary but not sufficient** to make a POST-read page
32
+ CDN-cacheable; you must also apply a rendering/edge strategy (rule 2b). GET reads ISR natively and
33
+ need nothing extra.
34
+
35
+ 2b. **Make a POST-read page cacheable at the rendering/edge layer.** Pick one (lowest risk first):
36
+ - **CDN edge override (current/default):** `src/proxy.ts` matches city/community paths and sets
37
+ `Cache-Control: public, s-maxage=2592000, stale-while-revalidate=2592000` — the same header ISR
38
+ pages emit (see rule 7 `expireTime`), so the CDN policy is uniform. Origin stays dynamic; the CDN
39
+ caches the deterministic-per-URL response. No build-time risk. This is what WEB-1069 shipped.
40
+ - **`export const dynamic = "force-static"` (interim):** prerenders the route as real ISR, but a
41
+ bad CCDS record/response at build time caches a blank page (the WEB-1058 regression) — use only
42
+ when the page is fully null-safe and reads throw on 5xx at runtime. Validate per repo.
43
+ - **`cacheComponents: true` + `use cache` (strategic):** PPR migration; do not mix ad hoc with
44
+ route-segment `export const revalidate`.
45
+
46
+ 3. **Always pair `tags` with a `revalidate` duration.** `next: { tags }` alone either holds stale data
47
+ indefinitely or (Next 16 default) does not cache at runtime at all. Use a `CACHE_DURATIONS`-style
48
+ default (e.g. 24h) so freshness survives a missed webhook.
49
+
50
+ 4. **`React.cache()` is request dedup only.** It collapses duplicate calls within a single render. It
51
+ does **not** cache across requests and is never a substitute for `next: { revalidate }`.
52
+
53
+ 5. **Every cacheable route exports `revalidate`.** CCDS/WP data changes rarely and is refreshed
54
+ on-demand (webhooks + tags), so the default is intentionally **long: `2592000` (30d)** for CCDS
55
+ reads, `WP_CACHE_DURATIONS`, and page segments alike (WEB-1069). Shorter tiers (24h/7d) are fine
56
+ per-route for volatile sources. Do **not** add `revalidate` to mutation/personalized routes (forms,
57
+ thank-you, wizards). Default to `export const revalidate` over `dynamic = "force-static"` so a
58
+ failed ISR revalidation preserves the last good cache instead of overwriting it with a broken page
59
+ (force-static is still a valid POST-read option per rule 2b once null-safety + throw-on-5xx exist).
60
+
61
+ 6. **On-demand revalidation dual-invalidates.** `/api/revalidate` must call `revalidateTag`/
62
+ `revalidatePath` **and** the CDN invalidation (e.g. `invalidateCloudFrontPaths`) in the same
63
+ request, otherwise the CDN keeps serving stale until its own TTL.
64
+
65
+ 7. **`next.config` cache config.** `expireTime` sets the CDN stale window: Next emits
66
+ `s-maxage=<revalidate>, stale-while-revalidate=<expireTime − revalidate>` for ISR pages, so
67
+ `expireTime` **must be ≥ the largest page `revalidate`**. Set `expireTime: 5184000` (60d) so a 30d
68
+ page yields `s-maxage=2592000, stale-while-revalidate=2592000` — the same header the proxy sets on
69
+ dynamic pages (rule 2b). (WEB-1069 bug: `expireTime: 86400` under a 30d revalidate = invalid stale
70
+ window.) Also set image `minimumCacheTTL: 31536000` (1y — optimized images are content-hashed and
71
+ never change), `qualities`, and `formats: ["image/avif", "image/webp"]`. No malformed
72
+ `remotePatterns` hostnames.
73
+
74
+ 8. **Asset proxy TTLs** are type-keyed (image/css/font) at `365d + SWR 30d`.
75
+
76
+ ## Canonical snippet — API client (read vs. mutation)
77
+
78
+ ```ts
79
+ interface FetchOptions {
80
+ endpoint: string;
81
+ method?: "GET" | "POST" | "PUT" | "DELETE";
82
+ body?: object | null;
83
+ revalidateTag?: string;
84
+ revalidate?: number;
85
+ /** Writes are never cached. Reads (default) cache regardless of method. */
86
+ mutation?: boolean;
87
+ }
88
+
89
+ export async function fetchData<T>({
90
+ endpoint,
91
+ method = "GET",
92
+ body = null,
93
+ revalidateTag,
94
+ revalidate = 2592000, // 30d time-based fallback; pair with tags for surgical invalidation
95
+ mutation = false,
96
+ }: FetchOptions): Promise<T | null> {
97
+ const isMutation = mutation || method === "PUT" || method === "DELETE";
98
+
99
+ const requestOptions: RequestInit = {
100
+ method,
101
+ // Reads (GET or POST) cache cross-request via `next`; mutations opt out.
102
+ ...(isMutation
103
+ ? { cache: "no-store" as RequestCache }
104
+ : { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
105
+ };
106
+
107
+ if ((method === "POST" || method === "PUT") && body) {
108
+ requestOptions.body = JSON.stringify(body);
109
+ }
110
+ // ...fetch + error handling...
111
+ }
112
+ ```
113
+
114
+ ## Canonical snippet — WordPress GraphQL client (cached POST)
115
+
116
+ ```ts
117
+ export const WP_CACHE_DURATIONS = {
118
+ pages: 2592000, posts: 2592000, menus: 2592000, staticPages: 2592000, default: 2592000, // 30d
119
+ } as const;
120
+
121
+ export async function fetchWPAPI<T>(
122
+ query: string,
123
+ { variables, revalidate = WP_CACHE_DURATIONS.default, tags }: {
124
+ variables?: Record<string, unknown>; revalidate?: number; tags?: string[];
125
+ } = {},
126
+ ) {
127
+ const res = await fetch(WP_API_URL, {
128
+ method: "POST",
129
+ headers,
130
+ body: JSON.stringify({ query, variables }),
131
+ next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
132
+ });
133
+ }
134
+ ```
135
+
136
+ ## Do NOT
137
+
138
+ - ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached.
139
+ - ❌ `fetch(API_URL, { method: "POST", body })` with no `next` options (bare, uncached POST read).
140
+ - ❌ `next: { tags }` with no `revalidate`.
141
+ - ❌ Adding `export const revalidate` to a form/mutation/personalized route.
142
+ - ❌ Treating `React.cache()` as cross-request caching.
143
+ - ❌ Caching a mutation (`submit-review`, lead submit, `PUT`/`DELETE`/`PATCH`).
144
+ - ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` —
145
+ that is a separate, deliberate migration; do not introduce it ad hoc.
@@ -0,0 +1,272 @@
1
+ ---
2
+ applyTo: "**/*.tsx"
3
+ ---
4
+ # SEO & AI Optimization Patterns
5
+
6
+ These rules ensure components are optimized for Google's generative AI features (AI Overviews, AI
7
+ Mode) and browser-agent interactions: agent-friendly HTML, a clean accessibility tree, semantic
8
+ structure, and E-E-A-T signals. Pairs with the `ai-seo-optimization` skill and the `audit-ai-seo`
9
+ prompt.
10
+
11
+ ---
12
+
13
+ ## Semantic HTML (CRITICAL)
14
+
15
+ ### Interactive Elements
16
+
17
+ **NEVER use `<div>` or `<span>` with click handlers. Use proper semantic elements.**
18
+
19
+ ```tsx
20
+ // ✅ CORRECT: Semantic button
21
+ <button onClick={handleClick} type="button">
22
+ Click me
23
+ </button>
24
+
25
+ // ✅ CORRECT: Navigation link
26
+ <Link href="/page">Go to page</Link>
27
+
28
+ // ❌ WRONG: Non-semantic clickable div
29
+ <div onClick={handleClick} className="cursor-pointer">
30
+ Click me
31
+ </div>
32
+
33
+ // ❌ WRONG: Span with role hack
34
+ <span role="button" onClick={handleClick}>
35
+ Click me
36
+ </span>
37
+ ```
38
+
39
+ ### Form Inputs
40
+
41
+ **Every input MUST have an associated label. Placeholder is NOT a substitute.**
42
+
43
+ ```tsx
44
+ // ✅ CORRECT: Label with htmlFor
45
+ <label htmlFor="email">Email address</label>
46
+ <input id="email" name="email" type="email" required aria-required="true" />
47
+
48
+ // ✅ CORRECT: Wrapped label (implicit association)
49
+ <label>
50
+ Email address
51
+ <input name="email" type="email" required />
52
+ </label>
53
+
54
+ // ❌ WRONG: Placeholder only (invisible to accessibility tree)
55
+ <input placeholder="Enter your email" type="email" />
56
+
57
+ // ❌ WRONG: aria-label without visible label (poor for agents)
58
+ <input aria-label="Email" type="email" />
59
+ ```
60
+
61
+ ### Heading Hierarchy
62
+
63
+ **Headings must follow logical order. Never skip levels.**
64
+
65
+ ```tsx
66
+ // ✅ CORRECT: Proper hierarchy
67
+ <h1>Page Title</h1>
68
+ <section>
69
+ <h2>Section Title</h2>
70
+ <h3>Subsection</h3>
71
+ </section>
72
+
73
+ // ❌ WRONG: Skipping h2
74
+ <h1>Page Title</h1>
75
+ <h3>Subsection</h3>
76
+
77
+ // ❌ WRONG: Using heading for styling only
78
+ <h4 className="text-sm font-bold">Small bold text</h4> // Use <p> with classes instead
79
+ ```
80
+
81
+ ### Lists
82
+
83
+ ```tsx
84
+ // ✅ CORRECT: Semantic list
85
+ <ul>
86
+ {items.map(item => <li key={item.id}>{item.name}</li>)}
87
+ </ul>
88
+
89
+ // ❌ WRONG: Div-based list
90
+ <div className="flex flex-col gap-2">
91
+ {items.map(item => <div key={item.id}>{item.name}</div>)}
92
+ </div>
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Accessibility for AI Agents
98
+
99
+ ### Skip-to-Content Link
100
+
101
+ The root layout MUST include a skip-to-content link as the first focusable element:
102
+
103
+ ```tsx
104
+ <body>
105
+ <a
106
+ href="#main-content"
107
+ className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground"
108
+ >
109
+ Skip to main content
110
+ </a>
111
+ {/* Header/Navigation */}
112
+ <main id="main-content">
113
+ {children}
114
+ </main>
115
+ </body>
116
+ ```
117
+
118
+ ### Accessible Names
119
+
120
+ All interactive elements MUST have an accessible name:
121
+
122
+ ```tsx
123
+ // ✅ CORRECT: Button with text content (name from content)
124
+ <button>Submit form</button>
125
+
126
+ // ✅ CORRECT: Icon button with aria-label
127
+ <button aria-label="Close dialog">
128
+ <XIcon className="h-4 w-4" />
129
+ </button>
130
+
131
+ // ❌ WRONG: Icon button without accessible name
132
+ <button>
133
+ <XIcon className="h-4 w-4" />
134
+ </button>
135
+ ```
136
+
137
+ ### Image Alt Text
138
+
139
+ ```tsx
140
+ // ✅ CORRECT: Descriptive alt text
141
+ <Image alt="Two-story brick building with a landscaped courtyard entrance" src={src} />
142
+
143
+ // ✅ CORRECT: Decorative image (empty alt)
144
+ <Image alt="" src={decorativeBg} aria-hidden="true" />
145
+
146
+ // ❌ WRONG: Generic alt text
147
+ <Image alt="photo" src={src} />
148
+ <Image alt="image 1" src={src} />
149
+
150
+ // ❌ WRONG: Missing alt
151
+ <Image src={src} />
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Metadata & SEO
157
+
158
+ ### generateMetadata Pattern
159
+
160
+ Every page with dynamic content MUST implement `generateMetadata`:
161
+
162
+ ```tsx
163
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
164
+ const { slug } = await params;
165
+ const data = await getData(slug);
166
+ if (!data) return {};
167
+
168
+ return {
169
+ title: data.title,
170
+ description: data.description,
171
+ openGraph: {
172
+ title: data.title,
173
+ description: data.description,
174
+ images: data.image ? [{ url: data.image }] : undefined,
175
+ },
176
+ alternates: {
177
+ canonical: getCanonicalUrl(slug),
178
+ },
179
+ };
180
+ }
181
+ ```
182
+
183
+ ### No Snippet Blocking
184
+
185
+ **NEVER add `nosnippet` to content pages.** This prevents AI feature inclusion:
186
+
187
+ ```tsx
188
+ // ❌ WRONG: Blocks AI features
189
+ export const metadata = {
190
+ robots: { index: true, follow: true, nosnippet: true },
191
+ };
192
+
193
+ // ✅ CORRECT: Allow snippets (default behavior)
194
+ export const metadata = {
195
+ robots: { index: true, follow: true },
196
+ };
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Structured Data (JSON-LD)
202
+
203
+ ### Implementation Pattern
204
+
205
+ Use a dedicated JSON-LD component in layout components:
206
+
207
+ ```tsx
208
+ export function JsonLd({ data }: { data: Record<string, unknown> }) {
209
+ return (
210
+ <script
211
+ type="application/ld+json"
212
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
213
+ />
214
+ );
215
+ }
216
+ ```
217
+
218
+ ### Required Schema by Page Type
219
+
220
+ | Page Type | Required Schema |
221
+ |-----------|----------------|
222
+ | Homepage | `Organization`, `WebSite` with `SearchAction` |
223
+ | Local business / Location | `LocalBusiness` with address, geo, rating |
224
+ | Blog Post | `Article` with author (`Person`), dates |
225
+ | Author Page | `Person` with credentials, `knowsAbout` |
226
+ | FAQ Section | `FAQPage` with Q&A pairs |
227
+ | All Pages | `BreadcrumbList` |
228
+
229
+ ---
230
+
231
+ ## Layout Stability (CLS Prevention)
232
+
233
+ ```tsx
234
+ // ✅ CORRECT: Explicit dimensions prevent layout shift
235
+ <Image src={src} alt="..." width={800} height={600} />
236
+
237
+ // ✅ CORRECT: Aspect ratio container
238
+ <div className="aspect-video relative">
239
+ <Image src={src} alt="..." fill className="object-cover" />
240
+ </div>
241
+
242
+ // ❌ WRONG: No dimensions (causes layout shift)
243
+ <img src={src} alt="..." />
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Server-Side Rendering
249
+
250
+ **Critical content MUST render on the server.** Client Components should only wrap interactive UI, not content:
251
+
252
+ ```tsx
253
+ // ✅ CORRECT: Content in Server Component
254
+ export default async function DetailPage({ params }: Props) {
255
+ const item = await getItem(params.slug);
256
+ return (
257
+ <main id="main-content">
258
+ <h1>{item.name}</h1>
259
+ <p>{item.description}</p>
260
+ <ContactForm itemId={item.id} /> {/* Client Component for form only */}
261
+ </main>
262
+ );
263
+ }
264
+
265
+ // ❌ WRONG: Entire page as Client Component
266
+ "use client";
267
+ export default function DetailPage() {
268
+ const [item, setItem] = useState(null);
269
+ useEffect(() => { fetchItem().then(setItem); }, []);
270
+ // Content invisible to crawlers until JS executes
271
+ }
272
+ ```
@@ -0,0 +1,58 @@
1
+ # Release — Node / npm Partial
2
+
3
+ Version-bump and quality-check mechanics for **Node.js / npm packages** (anything with a
4
+ `package.json`). Included by `prepare-github-release` when the project is detected as a Node package.
5
+
6
+ ## Detection signal
7
+
8
+ - A `package.json` exists at the repo root **and** there is no WordPress plugin header
9
+ (`*.php` file with a `Version:` plugin header). If both are present, prefer the WordPress partial.
10
+
11
+ ## Version source of truth
12
+
13
+ | File | What to update |
14
+ |------|----------------|
15
+ | `package.json` | `"version": "X.Y.Z"` |
16
+ | `package-lock.json` | top-level `version` **and** the root package entry under `packages[""].version` |
17
+
18
+ Prefer the npm tooling so both files stay in sync automatically:
19
+
20
+ ```bash
21
+ # Bumps package.json + package-lock.json and (by default) creates a commit + tag.
22
+ # Use --no-git-tag-version to keep the version change uncommitted so it can go in the release branch/PR.
23
+ npm version X.Y.Z --no-git-tag-version
24
+ ```
25
+
26
+ If you edit `package.json` by hand instead, re-sync the lockfile without touching dependencies:
27
+
28
+ ```bash
29
+ npm install --package-lock-only
30
+ ```
31
+
32
+ ## Quality checks (run BEFORE bumping)
33
+
34
+ Inspect `package.json` `scripts` and run whatever exists — do not assume script names:
35
+
36
+ ```bash
37
+ npm ci # clean install when a lockfile is present (CI parity)
38
+ npm test --if-present
39
+ npm run lint --if-present
40
+ npm run type-check --if-present
41
+ npm run build --if-present
42
+ ```
43
+
44
+ Do **not** proceed if any check fails.
45
+
46
+ ## What the release usually produces
47
+
48
+ - **npm publish** — most Node packages publish to the npm registry. The publish step almost always
49
+ lives in a workflow triggered by a GitHub Release (`on: release: [created|published]`) or a tag
50
+ push — confirm via the workflow-analysis step in the orchestrator before assuming a bare tag is enough.
51
+ - Optional build artifacts (bundled `dist/`, types) attached to the Release.
52
+
53
+ ## Notes
54
+
55
+ - `private: true` in `package.json` means the package is **not** published to npm — the release is
56
+ tag/GitHub-Release only. Surface this to the user.
57
+ - The npm version in `package.json` is the source of truth that `publish.yml`-style workflows read,
58
+ so it must be committed before the tag/Release is created.
@@ -0,0 +1,56 @@
1
+ # Release — WordPress Plugin Partial
2
+
3
+ Version-bump and quality-check mechanics for **Silver Assist WordPress plugins**. Included by
4
+ `prepare-github-release` when the project is detected as a WordPress plugin. See the
5
+ `release-management` skill for full documentation and troubleshooting.
6
+
7
+ ## Detection signal
8
+
9
+ - A `*.php` file with a plugin header (`* Version: X.Y.Z`) at the repo root, usually alongside a
10
+ `composer.json` and often a `readme.txt`.
11
+
12
+ ## Version source of truth
13
+
14
+ | File | What to update |
15
+ |------|----------------|
16
+ | Main plugin file | `* Version: X.Y.Z` header **and** the `VERSION` constant if defined |
17
+ | `readme.txt` | `Stable tag: X.Y.Z` (if present) |
18
+ | `composer.json` | `"version": "X.Y.Z"` (if present) |
19
+
20
+ Prefer the bundled script so every file stays in sync:
21
+
22
+ ```bash
23
+ ./scripts/update-version.sh X.Y.Z
24
+ # Some plugins ship the simple variant instead:
25
+ ./scripts/update-version-simple.sh X.Y.Z
26
+ ```
27
+
28
+ ## Quality checks (run BEFORE bumping)
29
+
30
+ Do **not** proceed if any check fails:
31
+
32
+ ```bash
33
+ ./scripts/run-quality-checks.sh --skip-wp-setup phpcs phpstan
34
+ ```
35
+
36
+ If the script is absent, fall back to the tools directly:
37
+
38
+ ```bash
39
+ composer run phpcs --if-present
40
+ composer run phpstan --if-present
41
+ composer run test --if-present
42
+ ```
43
+
44
+ ## What the release usually produces
45
+
46
+ - **Distributable ZIP** — the GitHub Actions release workflow builds the plugin ZIP and attaches it
47
+ to the GitHub Release. This almost always triggers on `on: release: [created|published]`, so a bare
48
+ tag is typically **not** enough — confirm via the workflow-analysis step in the orchestrator.
49
+ - No npm publish; WordPress plugins are distributed as ZIP artifacts (and/or wordpress.org SVN).
50
+
51
+ ## Notes
52
+
53
+ - Some plugins use `master` instead of `main` as the default branch (e.g.
54
+ `silver-assist-post-revalidate`) — always verify with `gh repo view --json defaultBranchRef`.
55
+ - The plugin-header version is what WordPress and the release workflow read, so it must be committed
56
+ before the tag/Release is created.
@@ -85,3 +85,4 @@ npm run build --if-present
85
85
  - [ ] No sensitive data exposed (API keys, secrets)
86
86
  - [ ] No `any` types introduced
87
87
  - [ ] JSDoc comments on new functions
88
+ - [ ] If data fetching / routes / `next.config` changed: caching follows `caching.instructions.md` (reads cache regardless of method, mutations `no-store`, cacheable routes export `revalidate`)
@@ -55,6 +55,14 @@ High-level description of the solution.
55
55
  ### API Changes
56
56
  If applicable, document any API changes.
57
57
 
58
+ ### Caching Impact (Next.js)
59
+ If the feature touches data fetching, routes, or `next.config`, state the caching plan:
60
+ - Reads vs. mutations (mutations never cached; reads cache regardless of GET/POST)
61
+ - `revalidate` tier + cache `tags` for any new fetches
62
+ - Whether new routes export `revalidate`, and any on-demand invalidation (Next.js + CDN) needed
63
+
64
+ See `.github/instructions/caching.instructions.md`.
65
+
58
66
  ## Risk Assessment
59
67
 
60
68
  | Risk | Probability | Impact | Mitigation |
@@ -0,0 +1,134 @@
1
+ ---
2
+ description: Prepare a GitHub release (version bump, changelog, tag/Release) — auto-detects WordPress vs Node projects and the correct tag-vs-Release flow
3
+ agent: agent
4
+ tools:
5
+ - run_in_terminal
6
+ - read_file
7
+ - replace_string_in_file
8
+ - create_file
9
+ ---
10
+
11
+ # Prepare GitHub Release
12
+
13
+ Prepare a new version release and drive it through the **correct GitHub flow** for the current
14
+ project. This prompt is **project-agnostic**: it detects the ecosystem (WordPress plugin vs Node/npm
15
+ package) and analyzes the repo's GitHub Actions workflows to decide whether a **bare tag** or a full
16
+ **GitHub Release** is required.
17
+
18
+ ## Prerequisites
19
+ - Reference: `.github/prompts/_partials/git-operations.md`
20
+ - Reference: `.github/prompts/_partials/release-wordpress.md` (WordPress projects)
21
+ - Reference: `.github/prompts/_partials/release-node.md` (Node/npm projects)
22
+ - `gh` CLI authenticated. Releases are a GitHub concept — if the `origin` remote is **not** GitHub
23
+ (e.g. Bitbucket), stop and tell the user this flow does not apply.
24
+
25
+ ## Inputs
26
+
27
+ Ask the user:
28
+ 1. **Version type** — `patch`, `minor`, or `major`? (default: patch). Suggest one from the
29
+ `[Unreleased]` changelog content: new features → `minor`, fixes only → `patch`, breaking → `major`.
30
+ 2. **Changelog entry** — reuse the existing `[Unreleased]` section if present, otherwise offer to
31
+ generate one from `git log` since the last tag.
32
+
33
+ ## Steps
34
+
35
+ ### 1. Detect host and project type
36
+
37
+ ```bash
38
+ git remote get-url origin # must be github.com — else stop
39
+ git fetch --tags --quiet
40
+ git tag --sort=-v:refname | head -1 # latest tag (current released version)
41
+ ```
42
+
43
+ Detect the **project type** and follow the matching partial:
44
+
45
+ | Signal | Project type | Use partial |
46
+ |--------|--------------|-------------|
47
+ | `*.php` with a `* Version:` plugin header | WordPress plugin | `release-wordpress.md` |
48
+ | `package.json` (no WP header) | Node / npm | `release-node.md` |
49
+ | neither | Generic | inline fallback: bump a `VERSION` file / changelog only |
50
+
51
+ ### 2. Determine the new version
52
+
53
+ - Read the **current** version from the source of truth named in the matching partial (not just the
54
+ git tag — they can drift).
55
+ - Apply the bump type to compute `X.Y.Z`.
56
+ - **NEVER reuse an existing tag** — tags are immutable. If `vX.Y.Z` already exists, bump again.
57
+
58
+ ### 3. Run quality checks (per partial)
59
+
60
+ Run the ecosystem's checks from the matching partial (WordPress: phpcs/phpstan; Node: `npm test` +
61
+ lint/type-check/build). **Do not proceed if any check fails.**
62
+
63
+ ### 4. Bump the version (per partial)
64
+
65
+ Update every version file listed in the matching partial (WordPress: plugin header / `readme.txt` /
66
+ `composer.json`; Node: `package.json` + `package-lock.json`).
67
+
68
+ ### 5. Update CHANGELOG.md
69
+
70
+ Promote the `[Unreleased]` section to the new version, keeping a Keep-a-Changelog structure:
71
+
72
+ ```markdown
73
+ ## [X.Y.Z] - YYYY-MM-DD
74
+
75
+ ### Added/Changed/Fixed
76
+ - ...
77
+ ```
78
+
79
+ If there is no `[Unreleased]` section, add a new `## [X.Y.Z]` block at the top with the changes.
80
+
81
+ ### 6. Create release branch, commit, and PR
82
+
83
+ ```bash
84
+ BASE_BRANCH=$(node -e "try{const c=require('./.agents-toolkit.json');console.log(c.pr?.targetBranch||c.git?.defaultBranch||'main')}catch{console.log('main')}")
85
+ git checkout -b release/vX.Y.Z
86
+ git add -A
87
+ git commit -m "chore: bump version to X.Y.Z"
88
+ git push -u origin release/vX.Y.Z
89
+ gh pr create --base "$BASE_BRANCH" --title "Release vX.Y.Z" --body "## Changes
90
+
91
+ - changelog entry" | cat
92
+ ```
93
+
94
+ ### 7. Analyze workflows → decide tag-only vs GitHub Release
95
+
96
+ **This is the critical step.** Read the repo's workflows and tell the user exactly what to do after
97
+ the release PR merges:
98
+
99
+ ```bash
100
+ ls .github/workflows/ 2>/dev/null
101
+ # inspect the `on:` triggers and publish/build steps of each workflow
102
+ ```
103
+
104
+ Decide from the triggers:
105
+
106
+ | Workflow trigger | Post-merge action | Why |
107
+ |------------------|-------------------|-----|
108
+ | `on: release: [created\|published]` | **Create a GitHub Release** (`gh release create vX.Y.Z`) | A bare tag does **not** fire `release` workflows — publishing/build only runs on the Release event |
109
+ | `on: push: tags: ['v*']` | **Push the tag** (`git push origin vX.Y.Z`) — Release optional | The tag push alone triggers the workflow |
110
+ | neither / no workflow | **Push the tag** for history; optionally `gh release create` for visibility | No automation depends on it |
111
+
112
+ Also scan the workflow **steps** and report what the release produces (e.g. `npm publish`, plugin
113
+ ZIP artifact, Docker image) so the user knows what will happen.
114
+
115
+ ### 8. Post-merge instructions (tailored to step 7)
116
+
117
+ Give the exact commands for the detected flow, e.g.:
118
+
119
+ ```bash
120
+ git checkout "$BASE_BRANCH" && git pull
121
+ git tag vX.Y.Z
122
+ git push origin vX.Y.Z
123
+ # If a `release:`-triggered workflow exists, ALSO create the Release so it fires:
124
+ gh release create vX.Y.Z --generate-notes
125
+ ```
126
+
127
+ ## Important
128
+
129
+ - **NEVER reuse an existing tag** — tags are immutable. If a tag exists, bump to the next version.
130
+ - The `release-management` skill has full documentation for WordPress plugins — use it for troubleshooting.
131
+ - Some projects use `master` instead of `main` — always verify the default branch with
132
+ `gh repo view --json defaultBranchRef | cat`.
133
+ - A bare tag and a GitHub Release are **not** interchangeable: many publish/build workflows only run
134
+ on the `release` event. Always complete step 7 before telling the user the release is done.
@@ -56,6 +56,18 @@ For each changed file:
56
56
  - [ ] Efficient data structures
57
57
  - [ ] No memory leaks potential
58
58
 
59
+ ### 5. Caching & Data Fetching (Next.js)
60
+
61
+ > See `.github/instructions/caching.instructions.md`. Caching is decided by **read-vs-mutation**, not the HTTP method.
62
+
63
+ - [ ] Reads cache regardless of method — a `POST` read (e.g. `geo-search`) sets `next: { revalidate, tags }` (NOT `revalidate: 0`)
64
+ - [ ] No `next: method === "GET" ? {...} : { revalidate: 0 }` gating (leaves POST reads uncached → route turns dynamic, `private, no-store`)
65
+ - [ ] Mutations (`submit`/lead/`PUT`/`DELETE`) use `cache: "no-store"` and are never cached
66
+ - [ ] `tags` always paired with a `revalidate` duration (no bare `next: { tags }`)
67
+ - [ ] Cacheable routes export `revalidate`; not added to form/personalized routes
68
+ - [ ] `React.cache()` not used as a substitute for cross-request caching
69
+ - [ ] On-demand revalidation invalidates both Next.js (`revalidateTag`/`revalidatePath`) and the CDN
70
+
59
71
  ## Output
60
72
 
61
73
  ### Review Summary
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: nextjs-caching
3
+ description: Caching strategy for Next.js (App Router) frontends consuming the CCDS API and headless WordPress. Use when asked about caching, ISR, "revalidate", "stale data", "page not cached", "private / no-store header", POST requests not caching, CloudFront/CDN invalidation, `next: { revalidate, tags }`, or when a page unexpectedly renders dynamically.
4
+ ---
5
+
6
+ # Next.js Caching Skill
7
+
8
+ Canonical caching strategy for Silver Side Next.js (App Router) frontends. Use it when touching data
9
+ fetching, route segment configs, the asset/page proxy, image config, or the on-demand revalidate
10
+ route — and when diagnosing "why isn't this page cached / why is it serving stale".
11
+
12
+ > Companion: `caching.instructions.md` (the short, auto-applied rules). This skill is the deep
13
+ > reference and decision guide. If a project keeps a `docs/CACHING.md`, that is its project-specific
14
+ > extension (endpoints, CloudFront IDs, per-route tiers) and is never overwritten by the toolkit.
15
+
16
+ ---
17
+
18
+ ## Core principle
19
+
20
+ **Cache by intent (read vs. mutation), never by the HTTP method.** Some reads must use `POST` because
21
+ they take a body (e.g. CCDS `geo-search`). Their *data* still has to cache like a `GET` — send
22
+ `next: { revalidate, tags }`, never `no-store`. Mutations must never cache.
23
+
24
+ **But caching the data is not the same as making the page static.** A POST read caches its response
25
+ cross-request, yet Next.js still renders the *route* dynamically (`private, no-store`). The data-fetch
26
+ cache (layer 2) and the route's static/dynamic classification (layers 1/5) are **independent** — see
27
+ the two sections below. This is the WEB-1069 gotcha and the reason city/community pages needed an edge
28
+ override even though their fetches were already cached.
29
+
30
+ ---
31
+
32
+ ## The caching layers (independent)
33
+
34
+ Fixing one does **not** fix the others.
35
+
36
+ | # | Layer | Where | Purpose |
37
+ |---|-------|-------|---------|
38
+ | 1 | ISR / page cache | `export const revalidate` per route | Time-based regeneration of rendered pages |
39
+ | 2 | Data-fetch cache | `next: { revalidate, tags }` on each `fetch` | Cross-request caching of CCDS/WP responses |
40
+ | 3 | Request dedup | `React.cache()` around client fns | Collapse duplicate calls within one render |
41
+ | 4 | Asset proxy | `src/app/assets/[...path]/route.ts` | Long-lived `Cache-Control` on WP images/CSS/fonts |
42
+ | 5 | Edge / CDN | `src/proxy.ts` + CloudFront | `s-maxage` at the edge + on-demand invalidation |
43
+
44
+ **Layer 2 is the most commonly broken.** A page can be ISR-enabled (layer 1) yet still hit origin on
45
+ every request if its data fetches (layer 2) are not cached — and an uncached fetch also forces the
46
+ whole route into **dynamic rendering**, which emits `cache-control: private, no-cache, no-store`.
47
+
48
+ ---
49
+
50
+ ## POST reads: the data caches, but the route stays dynamic (the key gotcha)
51
+
52
+ Two independent facts, both true:
53
+
54
+ 1. **The data fetch caches.** From `next/dist/server/lib/patch-fetch.js`: a `fetch` is uncacheable
55
+ only when it has no `next`/`cache` options *and* the segment `revalidate` is `0`. So a POST **with**
56
+ `next: { revalidate }` IS cached cross-request — the **request body is part of the cache key**
57
+ (`generateCacheKey` hashes `init.body`), so different filter bodies cache independently. This is
58
+ real and bounds origin/CCDS load; it is why the WordPress GraphQL POST caches fine.
59
+
60
+ 2. **The route still renders dynamically.** Empirically (WEB-1069, Next 16), a page whose data comes
61
+ from a POST read prerenders as `ƒ` (Dynamic) and serves `cache-control: private, no-store` — **even
62
+ with** `next: { revalidate, tags }` on the fetch. Next treats the POST as request-time data for
63
+ prerendering, so ISR (layer 1) never engages. GET reads do not have this problem — they ISR
64
+ natively (`s-maxage`, `x-nextjs-cache: HIT`).
65
+
66
+ **Implication:** `next: { revalidate, tags }` on a POST read is **necessary** (data cache) but **not
67
+ sufficient** to make the page CDN-cacheable. You must additionally pick a rendering/edge strategy
68
+ (next section). `revalidate` + `tags` alone will NOT flip a POST-read page from `private, no-store` to
69
+ `public`.
70
+
71
+ ## Making a POST-read page cacheable (rendering / edge layer)
72
+
73
+ Pick one (ordered by risk, lowest first):
74
+
75
+ 1. **CDN edge override (current, lowest risk).** `src/proxy.ts` matches the city/community paths and
76
+ sets `Cache-Control: public, s-maxage=2592000, stale-while-revalidate=2592000` — the **same**
77
+ header the ISR pages emit (see `expireTime` under ISR tiers), so the CDN policy is uniform across
78
+ static and dynamic routes. The origin stays dynamic; the CDN caches the deterministic-per-URL
79
+ response. Per-repo regex, no build-time risk — this is what WEB-1069 shipped.
80
+ 2. **`export const dynamic = "force-static"` (interim).** Forces the POST read to `force-cache` and
81
+ prerenders the route as real ISR (confirmed via `prerender-manifest.json`). Removed in WEB-1058
82
+ because a CCDS failure at build time cached a **blank page**; safer now that reads throw on 5xx at
83
+ runtime (a failed revalidation keeps the last good cache), but full prerender is sensitive to
84
+ null/bad records — keep the page fully null-safe and validate per repo before adopting.
85
+ 3. **`cacheComponents: true` + `use cache` (strategic).** Wrap the POST read in `use cache` so its data
86
+ lands in the static shell (PPR). Next-recommended long term; larger migration — do **not** mix ad
87
+ hoc with route-segment `export const revalidate`.
88
+
89
+ ---
90
+
91
+ ## Canonical API client (read vs. mutation)
92
+
93
+ ```ts
94
+ interface FetchOptions {
95
+ endpoint: string;
96
+ method?: "GET" | "POST" | "PUT" | "DELETE";
97
+ body?: object | null;
98
+ revalidateTag?: string;
99
+ revalidate?: number;
100
+ /** Writes are never cached. Reads (default) cache regardless of method. */
101
+ mutation?: boolean;
102
+ }
103
+
104
+ export async function fetchData<T>({
105
+ endpoint,
106
+ method = "GET",
107
+ body = null,
108
+ revalidateTag,
109
+ revalidate = 2592000, // 30d time-based fallback; pair with tags for surgical invalidation
110
+ mutation = false,
111
+ }: FetchOptions): Promise<T | null> {
112
+ const isMutation = mutation || method === "PUT" || method === "DELETE";
113
+
114
+ const requestOptions: RequestInit = {
115
+ method,
116
+ // Reads (GET or POST) cache cross-request via `next`; mutations opt out.
117
+ ...(isMutation
118
+ ? { cache: "no-store" as RequestCache }
119
+ : { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
120
+ };
121
+
122
+ if ((method === "POST" || method === "PUT") && body) {
123
+ requestOptions.body = JSON.stringify(body);
124
+ }
125
+ // ...fetch + error handling...
126
+ }
127
+ ```
128
+
129
+ Mark every write explicitly:
130
+
131
+ ```ts
132
+ await fetchData({ endpoint: "community/submit-review", method: "POST", body, mutation: true });
133
+ ```
134
+
135
+ WordPress GraphQL client (POST read — must carry `next`):
136
+
137
+ ```ts
138
+ await fetch(WP_API_URL, {
139
+ method: "POST",
140
+ body: JSON.stringify({ query, variables }),
141
+ next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
142
+ });
143
+ ```
144
+
145
+ ---
146
+
147
+ ## ISR revalidate tiers
148
+
149
+ CCDS/WP data changes rarely and is refreshed on-demand via webhooks + tags, so the time-based default
150
+ is intentionally **long** (WEB-1069):
151
+
152
+ | Route / config | Value |
153
+ |----------------|-------|
154
+ | CCDS reads (client default) | `2592000` (30d) |
155
+ | WordPress GraphQL reads (`WP_CACHE_DURATIONS`) | `2592000` (30d) |
156
+ | Listing / detail / city / community / state / landing / WP catch-all | `2592000` (30d) |
157
+ | `next.config` `expireTime` (CDN stale window) | `5184000` (60d) → emits `s-maxage=2592000, stale-while-revalidate=2592000` |
158
+ | `next.config` image `minimumCacheTTL` | `31536000` (1y) — optimized images are content-hashed, never change |
159
+
160
+ - Every cacheable route exports `revalidate`. Do **not** add it to form/personalized routes.
161
+ - **`expireTime` sets the CDN stale window.** Next emits `s-maxage=<revalidate>,
162
+ stale-while-revalidate=<expireTime − revalidate>` for ISR pages, so `expireTime` **must be ≥ the
163
+ largest `revalidate`** or the stale window is invalid (the WEB-1069 bug: `expireTime: 86400` under a
164
+ 30d revalidate). Set it to `5184000` (60d) so a 30d page yields the same header the proxy sets on
165
+ dynamic pages: `s-maxage=2592000, stale-while-revalidate=2592000`.
166
+ - The long default is safe because a missed webhook still self-heals within 30d; use `tags` for
167
+ immediate surgical invalidation. Shorter tiers (24h/7d) are fine per-route if a source is volatile.
168
+ - Default to `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
169
+ preserves the last good cache instead of overwriting it with a broken page. Pair with a client that
170
+ **throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the
171
+ build phase (so `generateStaticParams` can skip a bad entry without failing the build). `force-static`
172
+ is still a valid POST-read option (see "Making a POST-read page cacheable") **once** the page is
173
+ fully null-safe and the throw-on-5xx guard is in place.
174
+
175
+ ---
176
+
177
+ ## On-demand revalidation (dual invalidation)
178
+
179
+ `/api/revalidate` must invalidate **both** layers in one request, or the CDN serves stale until its
180
+ own TTL expires:
181
+
182
+ ```ts
183
+ revalidatePath(path, "page"); // or "layout"
184
+ revalidateTag(tag); // granular per-state / per-city tags
185
+ if (invalidateCDN) await invalidateCloudFrontPaths([path]);
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Anti-patterns
191
+
192
+ - ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached (origin hit
193
+ every render). Note: even a *correctly* cached POST read still renders the route dynamically — that
194
+ needs an edge override, not just `next` options (see "Making a POST-read page cacheable").
195
+ - ❌ Bare `fetch(url, { method: "POST", body })` for a read — refetched from origin every render.
196
+ - ❌ `next: { tags }` with no `revalidate` — holds stale data or doesn't cache at runtime.
197
+ - ❌ Caching a mutation (`submit`, lead, `PUT`/`DELETE`/`PATCH`).
198
+ - ❌ Treating `React.cache()` as cross-request caching (it's request-scoped dedup only).
199
+ - ❌ `dynamic = "force-static"` on a page that is **not** fully null-safe or lacks throw-on-5xx — a bad
200
+ CCDS record/response at build time caches a broken/blank page (the WEB-1058 regression). It is a
201
+ valid option only once those guards exist.
202
+ - ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` —
203
+ that is a separate, deliberate migration; do not introduce it ad hoc.
204
+
205
+ ---
206
+
207
+ ## Diagnosing "page is not cached"
208
+
209
+ 1. **Check the response header.** `cache-control: private, no-cache, no-store` ⇒ the route is rendering
210
+ dynamically. Something opted it into dynamic rendering.
211
+ 2. **Find the dynamic trigger.** Usually an uncached fetch (a POST read without `next`, or
212
+ `revalidate: 0`), or a request-time API (`cookies()`, `headers()`, `searchParams`).
213
+ 3. **Fix layer 2 first.** Give read fetches `next: { revalidate, tags }`; mark mutations `no-store`.
214
+ 4. **Confirm the route exports `revalidate`.**
215
+ 5. **Reconcile CDN vs. ISR TTLs.** If ISR is 24h but CDN `s-maxage` is shorter, an ISR revalidation
216
+ should trigger a CloudFront invalidation for that path.
217
+
218
+ ---
219
+
220
+ ## Verification
221
+
222
+ ```bash
223
+ # A POST-read page (e.g. a city) must be publicly cacheable:
224
+ curl -sI https://<host>/<care-type>/<state>/<city> | grep -i cache-control
225
+ # expect: cache-control: public, ... (NOT private/no-store)
226
+ ```
227
+
228
+ In the build output, `●` (or "Static"/"ISR") means prerendered; `ƒ` ("Dynamic") means it renders per
229
+ request — a POST-read page should be the former. `next.config` `logging.fetches.fullUrl: true` shows
230
+ per-fetch cache decisions in dev.
@@ -1,74 +0,0 @@
1
- ---
2
- description: Prepare a new release for a Silver Assist WordPress plugin (version bump, changelog, tag, PR)
3
- agent: agent
4
- tools:
5
- - run_in_terminal
6
- - read_file
7
- - replace_string_in_file
8
- - create_file
9
- ---
10
-
11
- # Prepare Release
12
-
13
- Prepare a new version release for the current Silver Assist plugin.
14
-
15
- ## Inputs
16
-
17
- Ask the user:
18
- 1. **Version type** — Is this a `patch`, `minor`, or `major` release? (default: patch)
19
- 2. **Changelog entry** — What changed? (or offer to generate from recent commits)
20
-
21
- ## Steps
22
-
23
- 1. **Detect plugin** — Find the plugin root, main plugin file, and current version.
24
-
25
- 2. **Determine new version** — Based on the version type, calculate the next semantic version.
26
-
27
- 3. **Run quality checks** — Execute `./scripts/run-quality-checks.sh --skip-wp-setup phpcs phpstan` to verify the code is clean before release. Do NOT proceed if checks fail.
28
-
29
- 4. **Update version** — Run the version update script:
30
- ```bash
31
- ./scripts/update-version.sh X.Y.Z
32
- ```
33
- Or if the plugin uses the simple variant:
34
- ```bash
35
- ./scripts/update-version-simple.sh X.Y.Z
36
- ```
37
- This updates the version in the main plugin file, `readme.txt` (if present), and `composer.json`.
38
-
39
- 5. **Update CHANGELOG.md** — Add a new entry at the top following this format:
40
- ```markdown
41
- ## [X.Y.Z] - YYYY-MM-DD
42
-
43
- ### Added/Changed/Fixed
44
- - Description of change
45
- ```
46
-
47
- 6. **Create release branch and commit** — Following the branch naming convention:
48
- ```bash
49
- git checkout -b release/vX.Y.Z
50
- git add -A
51
- git commit -m "chore: bump version to X.Y.Z"
52
- ```
53
-
54
- 7. **Push and create PR** — Push the branch and create a PR:
55
- ```bash
56
- git push origin release/vX.Y.Z
57
- gh pr create --title "Release vX.Y.Z" --body "## Changes\n\n- changelog entry" | cat
58
- ```
59
-
60
- 8. **Post-merge instructions** — Remind the user:
61
- - After merging the PR, create the tag from the main/master branch:
62
- ```bash
63
- git checkout main && git pull
64
- git tag vX.Y.Z
65
- git push origin vX.Y.Z
66
- ```
67
- - The GitHub Actions release workflow will automatically build the ZIP and create the GitHub Release.
68
-
69
- ## Important
70
-
71
- - **NEVER reuse an existing tag** — tags are immutable. If a tag exists, bump to the next version.
72
- - The release-management skill has full documentation — use it for troubleshooting.
73
- - Some plugins use `master` instead of `main` (e.g., silver-assist-post-revalidate).
74
- - Always verify the default branch with `git branch --show-current` or `gh repo view --json defaultBranchRef | cat`.