@silverassist/agents-toolkit 2.3.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.
@@ -0,0 +1,147 @@
1
+ ---
2
+ agent: agent
3
+ description: Create a pull request for the current branch linked to a GitHub issue
4
+ ---
5
+
6
+ # Create GitHub Pull Request
7
+
8
+ Create a pull request for the current branch linked to GitHub issue **#{issue-number}**.
9
+
10
+ ## Prerequisites
11
+ - Run `prepare-pr` first to ensure code is ready
12
+ - GitHub MCP connection or `gh` CLI required
13
+ - Reference: `.github/prompts/_partials/pr-template.md`
14
+ - Reference: `.github/prompts/_partials/git-operations.md`
15
+ - Reference: `.github/prompts/_partials/github-integration.md`
16
+
17
+ ## Steps
18
+
19
+ ### 1. Verify Current State
20
+
21
+ ```bash
22
+ git branch --show-current
23
+ git status
24
+ ```
25
+
26
+ Verify:
27
+ - Branch follows convention: `feature/{issue-number}-*` or `bugfix/{issue-number}-*`
28
+ - All changes are committed
29
+ - Not on protected branch
30
+
31
+ ### 2. Review Changes
32
+
33
+ ```bash
34
+ BASE_BRANCH=$(node -e "try{const c=require('./.agents-toolkit.json');console.log(c.pr?.targetBranch||c.git?.defaultBranch||'main')}catch{console.log('main')}")
35
+ git diff "$BASE_BRANCH" --name-only
36
+ ```
37
+
38
+ - Reuse `BASE_BRANCH` in all subsequent steps
39
+ - Summarize the changes made
40
+ - Identify breaking changes or migrations
41
+
42
+ ### 3. Read GitHub Issue
43
+
44
+ Fetch issue **#{issue-number}** details:
45
+ - Get title for PR title
46
+ - Extract acceptance criteria
47
+ - Get any context from comments
48
+
49
+ ```bash
50
+ gh issue view {issue-number} | cat
51
+ ```
52
+
53
+ ### 4. Run Final Validations
54
+
55
+ ```bash
56
+ npm run lint --if-present
57
+ npm run type-check --if-present
58
+ if [ -f tsconfig.json ]; then npx tsc --noEmit; fi
59
+ npm run test --if-present
60
+ npm run build --if-present
61
+ ```
62
+
63
+ Fix any issues before proceeding.
64
+
65
+ ### 5. Push Branch
66
+
67
+ ```bash
68
+ git push -u origin $(git branch --show-current)
69
+ ```
70
+
71
+ ### 6. Create Pull Request
72
+
73
+ #### PR Title
74
+ ```
75
+ {Issue title}
76
+ ```
77
+
78
+ #### PR Description
79
+
80
+ Use this template:
81
+
82
+ ```markdown
83
+ ## Summary
84
+ Brief description of what this PR accomplishes.
85
+
86
+ ## Related Issue
87
+ Closes #{issue-number}
88
+
89
+ ## Changes Made
90
+ - Change 1: Description
91
+ - Change 2: Description
92
+ - Change 3: Description
93
+
94
+ ## Type of Change
95
+ - [ ] 🐛 Bug fix
96
+ - [ ] ✨ New feature
97
+ - [ ] 💥 Breaking change
98
+ - [ ] 📝 Documentation
99
+ - [ ] 🔧 Refactoring
100
+
101
+ ## Testing
102
+ - [ ] Unit tests added/updated
103
+ - [ ] Manual testing performed
104
+ - Describe test cases here
105
+
106
+ ## Screenshots
107
+ (If UI changes, add before/after screenshots)
108
+
109
+ ## Checklist
110
+ - [ ] Code follows project style guidelines
111
+ - [ ] Self-review completed
112
+ - [ ] Documentation updated
113
+ - [ ] Tests pass locally
114
+ ```
115
+
116
+ #### Create via CLI
117
+
118
+ ```bash
119
+ gh pr create \
120
+ --title "{Issue title}" \
121
+ --body "$(cat <<'EOF'
122
+ ## Summary
123
+ ...
124
+
125
+ Closes #{issue-number}
126
+ EOF
127
+ )" \
128
+ --base "$BASE_BRANCH" | cat
129
+ ```
130
+
131
+ #### PR Settings
132
+ - **Source**: Current branch
133
+ - **Target**: `<base-branch>` resolved from `.agents-toolkit.json` (fallback: `main`)
134
+ - **Reviewers**: Based on changed files
135
+
136
+ ### 7. Comment on GitHub Issue
137
+
138
+ Add a comment linking to the PR:
139
+
140
+ ```bash
141
+ gh issue comment {issue-number} --body "## Pull Request Created
142
+
143
+ PR: <pr-url>
144
+ Branch: \`$(git branch --show-current)\`
145
+
146
+ Work in progress. Review requested." | cat
147
+ ```
@@ -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,129 @@
1
+ ---
2
+ agent: agent
3
+ description: Finalize a pull request after approval and prepare for merge
4
+ ---
5
+
6
+ # Finalize GitHub Pull Request
7
+
8
+ Finalize PR for GitHub issue **#{issue-number}** after approval and prepare for merge.
9
+
10
+ ## Prerequisites
11
+ - PR has been approved
12
+ - GitHub MCP connection or `gh` CLI required
13
+ - Reference: `.github/prompts/_partials/git-operations.md`
14
+ - Reference: `.github/prompts/_partials/validations.md`
15
+ - Reference: `.github/prompts/_partials/github-integration.md`
16
+
17
+ ## Steps
18
+
19
+ ### 1. Verify PR Status
20
+
21
+ ```bash
22
+ gh pr view --json state,reviewDecision,statusCheckRollup | cat
23
+ ```
24
+
25
+ Check:
26
+ - All required approvals in place
27
+ - CI/CD pipeline passed
28
+ - No unresolved review comments
29
+
30
+ ### 2. Address Review Comments
31
+
32
+ If there are unresolved comments:
33
+
34
+ ```bash
35
+ gh pr view --json reviews,comments | cat
36
+ ```
37
+
38
+ - List each unresolved comment
39
+ - Address feedback
40
+ - Push additional commits if needed
41
+ - Request re-review if changes are significant:
42
+
43
+ ```bash
44
+ gh pr review --request-changes --body "..." | cat
45
+ # or after fixing:
46
+ gh pr review --approve | cat
47
+ ```
48
+
49
+ ### 3. Sync with Base Branch
50
+
51
+ ```bash
52
+ BASE_BRANCH=$(node -e "try{const c=require('./.agents-toolkit.json');console.log(c.pr?.targetBranch||c.git?.defaultBranch||'main')}catch{console.log('main')}")
53
+ git fetch origin
54
+ git rebase "origin/${BASE_BRANCH}"
55
+ ```
56
+
57
+ If conflicts:
58
+ 1. Resolve each conflict
59
+ 2. Stage resolved files: `git add <file>`
60
+ 3. Continue rebase: `git rebase --continue`
61
+
62
+ Push updated branch:
63
+ ```bash
64
+ git push --force-with-lease
65
+ ```
66
+
67
+ ### 4. Final Validations
68
+
69
+ Run complete validation suite:
70
+ ```bash
71
+ npm run lint --if-present
72
+ npm run type-check --if-present
73
+ if [ -f tsconfig.json ]; then npx tsc --noEmit; fi
74
+ npm run test --if-present
75
+ npm run build --if-present
76
+ ```
77
+
78
+ Verify:
79
+ - No regressions after rebase
80
+ - All tests still pass
81
+ - No new warnings
82
+
83
+ ### 5. Merge Pull Request
84
+
85
+ **Recommended merge strategy**: Squash merge
86
+
87
+ ```bash
88
+ gh pr merge --squash --delete-branch | cat
89
+ ```
90
+
91
+ **Final commit message format**:
92
+ ```
93
+ {Issue title} (#{pr-number})
94
+
95
+ - Key change 1
96
+ - Key change 2
97
+ - Key change 3
98
+ ```
99
+
100
+ ### 6. Post-Merge Tasks
101
+
102
+ After merge is complete:
103
+
104
+ ```bash
105
+ # Return to base branch and sync
106
+ git checkout "$BASE_BRANCH"
107
+ git pull origin "$BASE_BRANCH"
108
+
109
+ # Delete local branch (force if squash-merged)
110
+ git branch -D <branch-name>
111
+
112
+ # Clean up stale references
113
+ git remote prune origin
114
+ ```
115
+
116
+ ### 7. Close GitHub Issue
117
+
118
+ The issue closes automatically if the PR description contains `Closes #{issue-number}`.
119
+ If not, close manually:
120
+
121
+ ```bash
122
+ gh issue close {issue-number} --comment "Completed in PR #<pr-number>." | cat
123
+ ```
124
+
125
+ ### 8. Clean Up
126
+
127
+ - [ ] Delete temporary planning docs from `docs/` (if applicable)
128
+ - [ ] Ensure final documentation is complete
129
+ - [ ] Verify commit history is clean
@@ -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
@@ -93,4 +93,4 @@ Report:
93
93
 
94
94
  - Begin implementation following the plan
95
95
  - Use `prepare-pr` when ready for review
96
- - Use `create-pr` to submit pull request
96
+ - Use `create-github-pr` to submit pull request
@@ -8,18 +8,39 @@ Skills are markdown files with YAML frontmatter that provide domain-specific gui
8
8
 
9
9
  ## Structure
10
10
 
11
- Each skill lives in its own folder with a `SKILL.md` file:
11
+ Each skill lives in its own folder with a `SKILL.md` file. Following the
12
+ [`npx skills`](https://github.com/vercel-labs/skills) standard, the real files
13
+ are installed **once** into a canonical `.agents/skills/` store, and each agent's
14
+ skills directory contains symlinks to it (single source of truth):
12
15
 
13
16
  ```
14
- .github/skills/
17
+ .agents/skills/ # canonical store (real files)
18
+ ├── ai-seo-optimization/
19
+ │ └── SKILL.md
15
20
  ├── component-architecture/
16
21
  │ └── SKILL.md
22
+ ├── create-component/
23
+ │ └── SKILL.md
17
24
  ├── domain-driven-design/
18
25
  │ └── SKILL.md
26
+ ├── plugin-creation/
27
+ │ └── SKILL.md
28
+ ├── quality-checks/
29
+ │ └── SKILL.md
30
+ ├── release-management/
31
+ │ └── SKILL.md
32
+ ├── testing/
33
+ │ └── SKILL.md
19
34
  └── testing-patterns/
20
35
  └── SKILL.md
36
+
37
+ .github/skills/ → symlinks to ../../.agents/skills/* (Copilot, Codex)
38
+ .claude/skills/ → symlinks to ../../.agents/skills/* (Claude Code, read natively)
21
39
  ```
22
40
 
41
+ Use `--copy` at install time to materialize real copies instead of symlinks
42
+ (symlinks also fall back to copies automatically on systems that don't support them).
43
+
23
44
  ## Frontmatter Format
24
45
 
25
46
  ```yaml
@@ -47,7 +68,8 @@ Skills are automatically picked up by agents when relevant to your question. You
47
68
 
48
69
  ## Creating Custom Skills
49
70
 
50
- 1. Create a folder: `.github/skills/your-skill-name/`
71
+ 1. Create a folder: `.agents/skills/your-skill-name/` (canonical store)
51
72
  2. Create `SKILL.md` with frontmatter
73
+ 3. Run `npx @silverassist/agents-toolkit install --skills-only` to symlink it into `.github/skills/` and `.claude/skills/`
52
74
  3. Document patterns, examples, and conventions
53
75
  4. Include ✅ CORRECT and ❌ INCORRECT examples
@@ -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.