@silverassist/agents-toolkit 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +6 -0
- package/bin/cli.js +5 -3
- package/package.json +1 -1
- package/src/index.js +7 -2
- package/templates/agents/AGENTS.md +33 -5
- package/templates/shared/instructions/caching.instructions.md +121 -0
- package/templates/shared/instructions/seo-ai-optimization.instructions.md +272 -0
- package/templates/shared/prompts/_partials/release-node.md +58 -0
- package/templates/shared/prompts/_partials/release-wordpress.md +56 -0
- package/templates/shared/prompts/_partials/validations.md +1 -0
- package/templates/shared/prompts/create-plan.prompt.md +8 -0
- package/templates/shared/prompts/prepare-github-release.prompt.md +134 -0
- package/templates/shared/prompts/review-code.prompt.md +12 -0
- package/templates/shared/skills/nextjs-caching/SKILL.md +184 -0
- package/templates/shared/prompts/prepare-release.prompt.md +0 -74
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.
|
|
3
|
+
"version": "2.5.0",
|
|
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.
|
|
6
|
+
export const VERSION = "2.5.0";
|
|
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
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
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,121 @@
|
|
|
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` IS cacheable cross-request — but only with explicit `next` options.**
|
|
26
|
+
Next.js does not cache `POST` by default. Any read `fetch` to CCDS or the WP GraphQL endpoint —
|
|
27
|
+
GET or POST — must set `next: { revalidate, tags }`. The request body is part of the cache key, so
|
|
28
|
+
different filter bodies cache independently.
|
|
29
|
+
|
|
30
|
+
3. **Always pair `tags` with a `revalidate` duration.** `next: { tags }` alone either holds stale data
|
|
31
|
+
indefinitely or (Next 16 default) does not cache at runtime at all. Use a `CACHE_DURATIONS`-style
|
|
32
|
+
default (e.g. 24h) so freshness survives a missed webhook.
|
|
33
|
+
|
|
34
|
+
4. **`React.cache()` is request dedup only.** It collapses duplicate calls within a single render. It
|
|
35
|
+
does **not** cache across requests and is never a substitute for `next: { revalidate }`.
|
|
36
|
+
|
|
37
|
+
5. **Every cacheable route exports `revalidate`.** Suggested tiers: listing 24h–30d, detail/city 24h
|
|
38
|
+
(`86400`), state/landing 7d (`604800`), WP catch-all 7d. Do **not** add `revalidate` to
|
|
39
|
+
mutation/personalized routes (forms, thank-you, wizards). Prefer `export const revalidate` over
|
|
40
|
+
`dynamic = "force-static"` so a failed ISR revalidation preserves the last good cache instead of
|
|
41
|
+
overwriting it with a broken page.
|
|
42
|
+
|
|
43
|
+
6. **On-demand revalidation dual-invalidates.** `/api/revalidate` must call `revalidateTag`/
|
|
44
|
+
`revalidatePath` **and** the CDN invalidation (e.g. `invalidateCloudFrontPaths`) in the same
|
|
45
|
+
request, otherwise the CDN keeps serving stale until its own TTL.
|
|
46
|
+
|
|
47
|
+
7. **Image config** in `next.config`: set `minimumCacheTTL: 2592000` (30d), `qualities`, and
|
|
48
|
+
`formats: ["image/avif", "image/webp"]`. No malformed `remotePatterns` hostnames.
|
|
49
|
+
|
|
50
|
+
8. **Asset proxy TTLs** are type-keyed (image/css/font) at `365d + SWR 30d`.
|
|
51
|
+
|
|
52
|
+
## Canonical snippet — API client (read vs. mutation)
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface FetchOptions {
|
|
56
|
+
endpoint: string;
|
|
57
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
58
|
+
body?: object | null;
|
|
59
|
+
revalidateTag?: string;
|
|
60
|
+
revalidate?: number;
|
|
61
|
+
/** Writes are never cached. Reads (default) cache regardless of method. */
|
|
62
|
+
mutation?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function fetchData<T>({
|
|
66
|
+
endpoint,
|
|
67
|
+
method = "GET",
|
|
68
|
+
body = null,
|
|
69
|
+
revalidateTag,
|
|
70
|
+
revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
|
|
71
|
+
mutation = false,
|
|
72
|
+
}: FetchOptions): Promise<T | null> {
|
|
73
|
+
const isMutation = mutation || method === "PUT" || method === "DELETE";
|
|
74
|
+
|
|
75
|
+
const requestOptions: RequestInit = {
|
|
76
|
+
method,
|
|
77
|
+
// Reads (GET or POST) cache cross-request via `next`; mutations opt out.
|
|
78
|
+
...(isMutation
|
|
79
|
+
? { cache: "no-store" as RequestCache }
|
|
80
|
+
: { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if ((method === "POST" || method === "PUT") && body) {
|
|
84
|
+
requestOptions.body = JSON.stringify(body);
|
|
85
|
+
}
|
|
86
|
+
// ...fetch + error handling...
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Canonical snippet — WordPress GraphQL client (cached POST)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export const WP_CACHE_DURATIONS = {
|
|
94
|
+
pages: 86400, posts: 86400, menus: 86400, staticPages: 604800, default: 86400,
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
export async function fetchWPAPI<T>(
|
|
98
|
+
query: string,
|
|
99
|
+
{ variables, revalidate = WP_CACHE_DURATIONS.default, tags }: {
|
|
100
|
+
variables?: Record<string, unknown>; revalidate?: number; tags?: string[];
|
|
101
|
+
} = {},
|
|
102
|
+
) {
|
|
103
|
+
const res = await fetch(WP_API_URL, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify({ query, variables }),
|
|
107
|
+
next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Do NOT
|
|
113
|
+
|
|
114
|
+
- ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached.
|
|
115
|
+
- ❌ `fetch(API_URL, { method: "POST", body })` with no `next` options (bare, uncached POST read).
|
|
116
|
+
- ❌ `next: { tags }` with no `revalidate`.
|
|
117
|
+
- ❌ Adding `export const revalidate` to a form/mutation/personalized route.
|
|
118
|
+
- ❌ Treating `React.cache()` as cross-request caching.
|
|
119
|
+
- ❌ Caching a mutation (`submit-review`, lead submit, `PUT`/`DELETE`/`PATCH`).
|
|
120
|
+
- ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` —
|
|
121
|
+
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,184 @@
|
|
|
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`). They still have to cache like a `GET`. Mutations must never
|
|
22
|
+
cache. Getting this wrong silently turns a static page into a per-request dynamic render.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## The caching layers (independent)
|
|
27
|
+
|
|
28
|
+
Fixing one does **not** fix the others.
|
|
29
|
+
|
|
30
|
+
| # | Layer | Where | Purpose |
|
|
31
|
+
|---|-------|-------|---------|
|
|
32
|
+
| 1 | ISR / page cache | `export const revalidate` per route | Time-based regeneration of rendered pages |
|
|
33
|
+
| 2 | Data-fetch cache | `next: { revalidate, tags }` on each `fetch` | Cross-request caching of CCDS/WP responses |
|
|
34
|
+
| 3 | Request dedup | `React.cache()` around client fns | Collapse duplicate calls within one render |
|
|
35
|
+
| 4 | Asset proxy | `src/app/assets/[...path]/route.ts` | Long-lived `Cache-Control` on WP images/CSS/fonts |
|
|
36
|
+
| 5 | Edge / CDN | `src/proxy.ts` + CloudFront | `s-maxage` at the edge + on-demand invalidation |
|
|
37
|
+
|
|
38
|
+
**Layer 2 is the most commonly broken.** A page can be ISR-enabled (layer 1) yet still hit origin on
|
|
39
|
+
every request if its data fetches (layer 2) are not cached — and an uncached fetch also forces the
|
|
40
|
+
whole route into **dynamic rendering**, which emits `cache-control: private, no-cache, no-store`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## How Next.js 16 actually decides (the rule behind the rule)
|
|
45
|
+
|
|
46
|
+
From `next/dist/server/lib/patch-fetch.js`:
|
|
47
|
+
|
|
48
|
+
- `fetch` is **not cached by default**. A non-`GET` method is "uncacheable" **only when no explicit
|
|
49
|
+
cache config is provided**. Concretely, a POST is auto-no-cached only when it has no `next`/`cache`
|
|
50
|
+
options *and* the segment `revalidate` is `0`.
|
|
51
|
+
- Therefore **a `POST` IS cached cross-request when you pass an explicit `next: { revalidate }`** (and
|
|
52
|
+
the segment isn't `revalidate: 0`). This is why the WordPress GraphQL POST caches fine.
|
|
53
|
+
- The **request body is part of the cache key** (`incremental-cache` → `generateCacheKey` hashes
|
|
54
|
+
`init.body`), so different filter bodies cache independently and never collide.
|
|
55
|
+
|
|
56
|
+
**Implication:** you do **not** need `unstable_cache` or a proxy header override to cache POST reads.
|
|
57
|
+
Just send `next: { revalidate, tags }`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Canonical API client (read vs. mutation)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface FetchOptions {
|
|
65
|
+
endpoint: string;
|
|
66
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
67
|
+
body?: object | null;
|
|
68
|
+
revalidateTag?: string;
|
|
69
|
+
revalidate?: number;
|
|
70
|
+
/** Writes are never cached. Reads (default) cache regardless of method. */
|
|
71
|
+
mutation?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function fetchData<T>({
|
|
75
|
+
endpoint,
|
|
76
|
+
method = "GET",
|
|
77
|
+
body = null,
|
|
78
|
+
revalidateTag,
|
|
79
|
+
revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
|
|
80
|
+
mutation = false,
|
|
81
|
+
}: FetchOptions): Promise<T | null> {
|
|
82
|
+
const isMutation = mutation || method === "PUT" || method === "DELETE";
|
|
83
|
+
|
|
84
|
+
const requestOptions: RequestInit = {
|
|
85
|
+
method,
|
|
86
|
+
// Reads (GET or POST) cache cross-request via `next`; mutations opt out.
|
|
87
|
+
...(isMutation
|
|
88
|
+
? { cache: "no-store" as RequestCache }
|
|
89
|
+
: { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if ((method === "POST" || method === "PUT") && body) {
|
|
93
|
+
requestOptions.body = JSON.stringify(body);
|
|
94
|
+
}
|
|
95
|
+
// ...fetch + error handling...
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Mark every write explicitly:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await fetchData({ endpoint: "community/submit-review", method: "POST", body, mutation: true });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
WordPress GraphQL client (POST read — must carry `next`):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await fetch(WP_API_URL, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: JSON.stringify({ query, variables }),
|
|
111
|
+
next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ISR revalidate tiers
|
|
118
|
+
|
|
119
|
+
| Route type | `export const revalidate` |
|
|
120
|
+
|------------|---------------------------|
|
|
121
|
+
| Listing / index | `2592000` (30d) — webhook handles freshness |
|
|
122
|
+
| Detail / city / community (`[slug]`) | `86400` (24h) |
|
|
123
|
+
| State / advisor landing | `604800` (7d) |
|
|
124
|
+
| WP catch-all (`[[...uri]]`) | `604800` (7d) |
|
|
125
|
+
|
|
126
|
+
- Every cacheable route exports `revalidate`. Do **not** add it to form/personalized routes.
|
|
127
|
+
- Prefer `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
|
|
128
|
+
preserves the last good cache instead of overwriting it with a broken page. Pair with a client that
|
|
129
|
+
**throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the
|
|
130
|
+
build phase (so `generateStaticParams` can skip a bad entry without failing the build).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## On-demand revalidation (dual invalidation)
|
|
135
|
+
|
|
136
|
+
`/api/revalidate` must invalidate **both** layers in one request, or the CDN serves stale until its
|
|
137
|
+
own TTL expires:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
revalidatePath(path, "page"); // or "layout"
|
|
141
|
+
revalidateTag(tag); // granular per-state / per-city tags
|
|
142
|
+
if (invalidateCDN) await invalidateCloudFrontPaths([path]);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Anti-patterns
|
|
148
|
+
|
|
149
|
+
- ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached → the route
|
|
150
|
+
renders dynamically (`private, no-store`). This is the exact regression that broke city pages.
|
|
151
|
+
- ❌ Bare `fetch(url, { method: "POST", body })` for a read — refetched from origin every render.
|
|
152
|
+
- ❌ `next: { tags }` with no `revalidate` — holds stale data or doesn't cache at runtime.
|
|
153
|
+
- ❌ Caching a mutation (`submit`, lead, `PUT`/`DELETE`/`PATCH`).
|
|
154
|
+
- ❌ Treating `React.cache()` as cross-request caching (it's request-scoped dedup only).
|
|
155
|
+
- ❌ `dynamic = "force-static"` on a page whose revalidation can fail (caches a broken page).
|
|
156
|
+
- ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` —
|
|
157
|
+
that is a separate, deliberate migration; do not introduce it ad hoc.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Diagnosing "page is not cached"
|
|
162
|
+
|
|
163
|
+
1. **Check the response header.** `cache-control: private, no-cache, no-store` ⇒ the route is rendering
|
|
164
|
+
dynamically. Something opted it into dynamic rendering.
|
|
165
|
+
2. **Find the dynamic trigger.** Usually an uncached fetch (a POST read without `next`, or
|
|
166
|
+
`revalidate: 0`), or a request-time API (`cookies()`, `headers()`, `searchParams`).
|
|
167
|
+
3. **Fix layer 2 first.** Give read fetches `next: { revalidate, tags }`; mark mutations `no-store`.
|
|
168
|
+
4. **Confirm the route exports `revalidate`.**
|
|
169
|
+
5. **Reconcile CDN vs. ISR TTLs.** If ISR is 24h but CDN `s-maxage` is shorter, an ISR revalidation
|
|
170
|
+
should trigger a CloudFront invalidation for that path.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Verification
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# A POST-read page (e.g. a city) must be publicly cacheable:
|
|
178
|
+
curl -sI https://<host>/<care-type>/<state>/<city> | grep -i cache-control
|
|
179
|
+
# expect: cache-control: public, ... (NOT private/no-store)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
In the build output, `●` (or "Static"/"ISR") means prerendered; `ƒ` ("Dynamic") means it renders per
|
|
183
|
+
request — a POST-read page should be the former. `next.config` `logging.fetches.fullUrl: true` shows
|
|
184
|
+
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`.
|