@silverassist/agents-toolkit
Advanced tools
| --- | ||
| applyTo: "**/next.config.*,**/src/proxy.ts,**/src/lib/**/*.ts,**/src/app/**/route.ts,**/src/app/**/page.tsx" | ||
| --- | ||
| # Caching Standards (CRITICAL) | ||
| These rules are **mandatory** when touching data fetching, route segment configs, the asset/page | ||
| proxy, image config, or the on-demand revalidate route. Caching has several **independent layers** | ||
| (ISR page cache, data-fetch cache, request dedup, asset proxy, edge/CDN) β fixing one does not fix | ||
| the others. | ||
| > Apply to any Silver Side Next.js (App Router) frontend consuming the CCDS API and/or WordPress | ||
| > (headless GraphQL). If the project keeps a `docs/CACHING.md`, treat it as the extended reference. | ||
| ## Hard rules | ||
| 1. **Cache reads, not mutations β decide on intent, NEVER on the HTTP method.** | ||
| Some reads must use `POST` because they take a body (e.g. CCDS `geo-search`). They still have to | ||
| cache like a `GET`. Mutations (`submit-review`, lead submit, and any `PUT`/`DELETE`/`PATCH`) must | ||
| stay uncached with `cache: "no-store"`. Expose a `mutation: true` flag on the client and mark every | ||
| write with it. | ||
| - β **Never** gate caching on `method === "GET"` (e.g. `next: method === "GET" ? {...} : { revalidate: 0 }`). | ||
| That leaves POST reads uncached and forces the whole route into dynamic rendering | ||
| (`cache-control: private, no-store`). | ||
| 2. **A `POST` IS cacheable cross-request β but only with explicit `next` options.** | ||
| Next.js does not cache `POST` by default. Any read `fetch` to CCDS or the WP GraphQL endpoint β | ||
| GET or POST β must set `next: { revalidate, tags }`. The request body is part of the cache key, so | ||
| different filter bodies cache independently. | ||
| 3. **Always pair `tags` with a `revalidate` duration.** `next: { tags }` alone either holds stale data | ||
| indefinitely or (Next 16 default) does not cache at runtime at all. Use a `CACHE_DURATIONS`-style | ||
| default (e.g. 24h) so freshness survives a missed webhook. | ||
| 4. **`React.cache()` is request dedup only.** It collapses duplicate calls within a single render. It | ||
| does **not** cache across requests and is never a substitute for `next: { revalidate }`. | ||
| 5. **Every cacheable route exports `revalidate`.** Suggested tiers: listing 24hβ30d, detail/city 24h | ||
| (`86400`), state/landing 7d (`604800`), WP catch-all 7d. Do **not** add `revalidate` to | ||
| mutation/personalized routes (forms, thank-you, wizards). Prefer `export const revalidate` over | ||
| `dynamic = "force-static"` so a failed ISR revalidation preserves the last good cache instead of | ||
| overwriting it with a broken page. | ||
| 6. **On-demand revalidation dual-invalidates.** `/api/revalidate` must call `revalidateTag`/ | ||
| `revalidatePath` **and** the CDN invalidation (e.g. `invalidateCloudFrontPaths`) in the same | ||
| request, otherwise the CDN keeps serving stale until its own TTL. | ||
| 7. **Image config** in `next.config`: set `minimumCacheTTL: 2592000` (30d), `qualities`, and | ||
| `formats: ["image/avif", "image/webp"]`. No malformed `remotePatterns` hostnames. | ||
| 8. **Asset proxy TTLs** are type-keyed (image/css/font) at `365d + SWR 30d`. | ||
| ## Canonical snippet β API client (read vs. mutation) | ||
| ```ts | ||
| interface FetchOptions { | ||
| endpoint: string; | ||
| method?: "GET" | "POST" | "PUT" | "DELETE"; | ||
| body?: object | null; | ||
| revalidateTag?: string; | ||
| revalidate?: number; | ||
| /** Writes are never cached. Reads (default) cache regardless of method. */ | ||
| mutation?: boolean; | ||
| } | ||
| export async function fetchData<T>({ | ||
| endpoint, | ||
| method = "GET", | ||
| body = null, | ||
| revalidateTag, | ||
| revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation | ||
| mutation = false, | ||
| }: FetchOptions): Promise<T | null> { | ||
| const isMutation = mutation || method === "PUT" || method === "DELETE"; | ||
| const requestOptions: RequestInit = { | ||
| method, | ||
| // Reads (GET or POST) cache cross-request via `next`; mutations opt out. | ||
| ...(isMutation | ||
| ? { cache: "no-store" as RequestCache } | ||
| : { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }), | ||
| }; | ||
| if ((method === "POST" || method === "PUT") && body) { | ||
| requestOptions.body = JSON.stringify(body); | ||
| } | ||
| // ...fetch + error handling... | ||
| } | ||
| ``` | ||
| ## Canonical snippet β WordPress GraphQL client (cached POST) | ||
| ```ts | ||
| export const WP_CACHE_DURATIONS = { | ||
| pages: 86400, posts: 86400, menus: 86400, staticPages: 604800, default: 86400, | ||
| } as const; | ||
| export async function fetchWPAPI<T>( | ||
| query: string, | ||
| { variables, revalidate = WP_CACHE_DURATIONS.default, tags }: { | ||
| variables?: Record<string, unknown>; revalidate?: number; tags?: string[]; | ||
| } = {}, | ||
| ) { | ||
| const res = await fetch(WP_API_URL, { | ||
| method: "POST", | ||
| headers, | ||
| body: JSON.stringify({ query, variables }), | ||
| next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options | ||
| }); | ||
| } | ||
| ``` | ||
| ## Do NOT | ||
| - β `next: method === "GET" ? {...} : { revalidate: 0 }` β leaves POST reads uncached. | ||
| - β `fetch(API_URL, { method: "POST", body })` with no `next` options (bare, uncached POST read). | ||
| - β `next: { tags }` with no `revalidate`. | ||
| - β Adding `export const revalidate` to a form/mutation/personalized route. | ||
| - β Treating `React.cache()` as cross-request caching. | ||
| - β Caching a mutation (`submit-review`, lead submit, `PUT`/`DELETE`/`PATCH`). | ||
| - β Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` β | ||
| that is a separate, deliberate migration; do not introduce it ad hoc. |
| --- | ||
| applyTo: "**/*.tsx" | ||
| --- | ||
| # SEO & AI Optimization Patterns | ||
| These rules ensure components are optimized for Google's generative AI features (AI Overviews, AI | ||
| Mode) and browser-agent interactions: agent-friendly HTML, a clean accessibility tree, semantic | ||
| structure, and E-E-A-T signals. Pairs with the `ai-seo-optimization` skill and the `audit-ai-seo` | ||
| prompt. | ||
| --- | ||
| ## Semantic HTML (CRITICAL) | ||
| ### Interactive Elements | ||
| **NEVER use `<div>` or `<span>` with click handlers. Use proper semantic elements.** | ||
| ```tsx | ||
| // β CORRECT: Semantic button | ||
| <button onClick={handleClick} type="button"> | ||
| Click me | ||
| </button> | ||
| // β CORRECT: Navigation link | ||
| <Link href="/page">Go to page</Link> | ||
| // β WRONG: Non-semantic clickable div | ||
| <div onClick={handleClick} className="cursor-pointer"> | ||
| Click me | ||
| </div> | ||
| // β WRONG: Span with role hack | ||
| <span role="button" onClick={handleClick}> | ||
| Click me | ||
| </span> | ||
| ``` | ||
| ### Form Inputs | ||
| **Every input MUST have an associated label. Placeholder is NOT a substitute.** | ||
| ```tsx | ||
| // β CORRECT: Label with htmlFor | ||
| <label htmlFor="email">Email address</label> | ||
| <input id="email" name="email" type="email" required aria-required="true" /> | ||
| // β CORRECT: Wrapped label (implicit association) | ||
| <label> | ||
| Email address | ||
| <input name="email" type="email" required /> | ||
| </label> | ||
| // β WRONG: Placeholder only (invisible to accessibility tree) | ||
| <input placeholder="Enter your email" type="email" /> | ||
| // β WRONG: aria-label without visible label (poor for agents) | ||
| <input aria-label="Email" type="email" /> | ||
| ``` | ||
| ### Heading Hierarchy | ||
| **Headings must follow logical order. Never skip levels.** | ||
| ```tsx | ||
| // β CORRECT: Proper hierarchy | ||
| <h1>Page Title</h1> | ||
| <section> | ||
| <h2>Section Title</h2> | ||
| <h3>Subsection</h3> | ||
| </section> | ||
| // β WRONG: Skipping h2 | ||
| <h1>Page Title</h1> | ||
| <h3>Subsection</h3> | ||
| // β WRONG: Using heading for styling only | ||
| <h4 className="text-sm font-bold">Small bold text</h4> // Use <p> with classes instead | ||
| ``` | ||
| ### Lists | ||
| ```tsx | ||
| // β CORRECT: Semantic list | ||
| <ul> | ||
| {items.map(item => <li key={item.id}>{item.name}</li>)} | ||
| </ul> | ||
| // β WRONG: Div-based list | ||
| <div className="flex flex-col gap-2"> | ||
| {items.map(item => <div key={item.id}>{item.name}</div>)} | ||
| </div> | ||
| ``` | ||
| --- | ||
| ## Accessibility for AI Agents | ||
| ### Skip-to-Content Link | ||
| The root layout MUST include a skip-to-content link as the first focusable element: | ||
| ```tsx | ||
| <body> | ||
| <a | ||
| href="#main-content" | ||
| className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground" | ||
| > | ||
| Skip to main content | ||
| </a> | ||
| {/* Header/Navigation */} | ||
| <main id="main-content"> | ||
| {children} | ||
| </main> | ||
| </body> | ||
| ``` | ||
| ### Accessible Names | ||
| All interactive elements MUST have an accessible name: | ||
| ```tsx | ||
| // β CORRECT: Button with text content (name from content) | ||
| <button>Submit form</button> | ||
| // β CORRECT: Icon button with aria-label | ||
| <button aria-label="Close dialog"> | ||
| <XIcon className="h-4 w-4" /> | ||
| </button> | ||
| // β WRONG: Icon button without accessible name | ||
| <button> | ||
| <XIcon className="h-4 w-4" /> | ||
| </button> | ||
| ``` | ||
| ### Image Alt Text | ||
| ```tsx | ||
| // β CORRECT: Descriptive alt text | ||
| <Image alt="Two-story brick building with a landscaped courtyard entrance" src={src} /> | ||
| // β CORRECT: Decorative image (empty alt) | ||
| <Image alt="" src={decorativeBg} aria-hidden="true" /> | ||
| // β WRONG: Generic alt text | ||
| <Image alt="photo" src={src} /> | ||
| <Image alt="image 1" src={src} /> | ||
| // β WRONG: Missing alt | ||
| <Image src={src} /> | ||
| ``` | ||
| --- | ||
| ## Metadata & SEO | ||
| ### generateMetadata Pattern | ||
| Every page with dynamic content MUST implement `generateMetadata`: | ||
| ```tsx | ||
| export async function generateMetadata({ params }: PageProps): Promise<Metadata> { | ||
| const { slug } = await params; | ||
| const data = await getData(slug); | ||
| if (!data) return {}; | ||
| return { | ||
| title: data.title, | ||
| description: data.description, | ||
| openGraph: { | ||
| title: data.title, | ||
| description: data.description, | ||
| images: data.image ? [{ url: data.image }] : undefined, | ||
| }, | ||
| alternates: { | ||
| canonical: getCanonicalUrl(slug), | ||
| }, | ||
| }; | ||
| } | ||
| ``` | ||
| ### No Snippet Blocking | ||
| **NEVER add `nosnippet` to content pages.** This prevents AI feature inclusion: | ||
| ```tsx | ||
| // β WRONG: Blocks AI features | ||
| export const metadata = { | ||
| robots: { index: true, follow: true, nosnippet: true }, | ||
| }; | ||
| // β CORRECT: Allow snippets (default behavior) | ||
| export const metadata = { | ||
| robots: { index: true, follow: true }, | ||
| }; | ||
| ``` | ||
| --- | ||
| ## Structured Data (JSON-LD) | ||
| ### Implementation Pattern | ||
| Use a dedicated JSON-LD component in layout components: | ||
| ```tsx | ||
| export function JsonLd({ data }: { data: Record<string, unknown> }) { | ||
| return ( | ||
| <script | ||
| type="application/ld+json" | ||
| dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} | ||
| /> | ||
| ); | ||
| } | ||
| ``` | ||
| ### Required Schema by Page Type | ||
| | Page Type | Required Schema | | ||
| |-----------|----------------| | ||
| | Homepage | `Organization`, `WebSite` with `SearchAction` | | ||
| | Local business / Location | `LocalBusiness` with address, geo, rating | | ||
| | Blog Post | `Article` with author (`Person`), dates | | ||
| | Author Page | `Person` with credentials, `knowsAbout` | | ||
| | FAQ Section | `FAQPage` with Q&A pairs | | ||
| | All Pages | `BreadcrumbList` | | ||
| --- | ||
| ## Layout Stability (CLS Prevention) | ||
| ```tsx | ||
| // β CORRECT: Explicit dimensions prevent layout shift | ||
| <Image src={src} alt="..." width={800} height={600} /> | ||
| // β CORRECT: Aspect ratio container | ||
| <div className="aspect-video relative"> | ||
| <Image src={src} alt="..." fill className="object-cover" /> | ||
| </div> | ||
| // β WRONG: No dimensions (causes layout shift) | ||
| <img src={src} alt="..." /> | ||
| ``` | ||
| --- | ||
| ## Server-Side Rendering | ||
| **Critical content MUST render on the server.** Client Components should only wrap interactive UI, not content: | ||
| ```tsx | ||
| // β CORRECT: Content in Server Component | ||
| export default async function DetailPage({ params }: Props) { | ||
| const item = await getItem(params.slug); | ||
| return ( | ||
| <main id="main-content"> | ||
| <h1>{item.name}</h1> | ||
| <p>{item.description}</p> | ||
| <ContactForm itemId={item.id} /> {/* Client Component for form only */} | ||
| </main> | ||
| ); | ||
| } | ||
| // β WRONG: Entire page as Client Component | ||
| "use client"; | ||
| export default function DetailPage() { | ||
| const [item, setItem] = useState(null); | ||
| useEffect(() => { fetchItem().then(setItem); }, []); | ||
| // Content invisible to crawlers until JS executes | ||
| } | ||
| ``` |
| # Release β Node / npm Partial | ||
| Version-bump and quality-check mechanics for **Node.js / npm packages** (anything with a | ||
| `package.json`). Included by `prepare-github-release` when the project is detected as a Node package. | ||
| ## Detection signal | ||
| - A `package.json` exists at the repo root **and** there is no WordPress plugin header | ||
| (`*.php` file with a `Version:` plugin header). If both are present, prefer the WordPress partial. | ||
| ## Version source of truth | ||
| | File | What to update | | ||
| |------|----------------| | ||
| | `package.json` | `"version": "X.Y.Z"` | | ||
| | `package-lock.json` | top-level `version` **and** the root package entry under `packages[""].version` | | ||
| Prefer the npm tooling so both files stay in sync automatically: | ||
| ```bash | ||
| # Bumps package.json + package-lock.json and (by default) creates a commit + tag. | ||
| # Use --no-git-tag-version to keep the version change uncommitted so it can go in the release branch/PR. | ||
| npm version X.Y.Z --no-git-tag-version | ||
| ``` | ||
| If you edit `package.json` by hand instead, re-sync the lockfile without touching dependencies: | ||
| ```bash | ||
| npm install --package-lock-only | ||
| ``` | ||
| ## Quality checks (run BEFORE bumping) | ||
| Inspect `package.json` `scripts` and run whatever exists β do not assume script names: | ||
| ```bash | ||
| npm ci # clean install when a lockfile is present (CI parity) | ||
| npm test --if-present | ||
| npm run lint --if-present | ||
| npm run type-check --if-present | ||
| npm run build --if-present | ||
| ``` | ||
| Do **not** proceed if any check fails. | ||
| ## What the release usually produces | ||
| - **npm publish** β most Node packages publish to the npm registry. The publish step almost always | ||
| lives in a workflow triggered by a GitHub Release (`on: release: [created|published]`) or a tag | ||
| push β confirm via the workflow-analysis step in the orchestrator before assuming a bare tag is enough. | ||
| - Optional build artifacts (bundled `dist/`, types) attached to the Release. | ||
| ## Notes | ||
| - `private: true` in `package.json` means the package is **not** published to npm β the release is | ||
| tag/GitHub-Release only. Surface this to the user. | ||
| - The npm version in `package.json` is the source of truth that `publish.yml`-style workflows read, | ||
| so it must be committed before the tag/Release is created. |
| # Release β WordPress Plugin Partial | ||
| Version-bump and quality-check mechanics for **Silver Assist WordPress plugins**. Included by | ||
| `prepare-github-release` when the project is detected as a WordPress plugin. See the | ||
| `release-management` skill for full documentation and troubleshooting. | ||
| ## Detection signal | ||
| - A `*.php` file with a plugin header (`* Version: X.Y.Z`) at the repo root, usually alongside a | ||
| `composer.json` and often a `readme.txt`. | ||
| ## Version source of truth | ||
| | File | What to update | | ||
| |------|----------------| | ||
| | Main plugin file | `* Version: X.Y.Z` header **and** the `VERSION` constant if defined | | ||
| | `readme.txt` | `Stable tag: X.Y.Z` (if present) | | ||
| | `composer.json` | `"version": "X.Y.Z"` (if present) | | ||
| Prefer the bundled script so every file stays in sync: | ||
| ```bash | ||
| ./scripts/update-version.sh X.Y.Z | ||
| # Some plugins ship the simple variant instead: | ||
| ./scripts/update-version-simple.sh X.Y.Z | ||
| ``` | ||
| ## Quality checks (run BEFORE bumping) | ||
| Do **not** proceed if any check fails: | ||
| ```bash | ||
| ./scripts/run-quality-checks.sh --skip-wp-setup phpcs phpstan | ||
| ``` | ||
| If the script is absent, fall back to the tools directly: | ||
| ```bash | ||
| composer run phpcs --if-present | ||
| composer run phpstan --if-present | ||
| composer run test --if-present | ||
| ``` | ||
| ## What the release usually produces | ||
| - **Distributable ZIP** β the GitHub Actions release workflow builds the plugin ZIP and attaches it | ||
| to the GitHub Release. This almost always triggers on `on: release: [created|published]`, so a bare | ||
| tag is typically **not** enough β confirm via the workflow-analysis step in the orchestrator. | ||
| - No npm publish; WordPress plugins are distributed as ZIP artifacts (and/or wordpress.org SVN). | ||
| ## Notes | ||
| - Some plugins use `master` instead of `main` as the default branch (e.g. | ||
| `silver-assist-post-revalidate`) β always verify with `gh repo view --json defaultBranchRef`. | ||
| - The plugin-header version is what WordPress and the release workflow read, so it must be committed | ||
| before the tag/Release is created. |
| --- | ||
| description: Prepare a GitHub release (version bump, changelog, tag/Release) β auto-detects WordPress vs Node projects and the correct tag-vs-Release flow | ||
| agent: agent | ||
| tools: | ||
| - run_in_terminal | ||
| - read_file | ||
| - replace_string_in_file | ||
| - create_file | ||
| --- | ||
| # Prepare GitHub Release | ||
| Prepare a new version release and drive it through the **correct GitHub flow** for the current | ||
| project. This prompt is **project-agnostic**: it detects the ecosystem (WordPress plugin vs Node/npm | ||
| package) and analyzes the repo's GitHub Actions workflows to decide whether a **bare tag** or a full | ||
| **GitHub Release** is required. | ||
| ## Prerequisites | ||
| - Reference: `.github/prompts/_partials/git-operations.md` | ||
| - Reference: `.github/prompts/_partials/release-wordpress.md` (WordPress projects) | ||
| - Reference: `.github/prompts/_partials/release-node.md` (Node/npm projects) | ||
| - `gh` CLI authenticated. Releases are a GitHub concept β if the `origin` remote is **not** GitHub | ||
| (e.g. Bitbucket), stop and tell the user this flow does not apply. | ||
| ## Inputs | ||
| Ask the user: | ||
| 1. **Version type** β `patch`, `minor`, or `major`? (default: patch). Suggest one from the | ||
| `[Unreleased]` changelog content: new features β `minor`, fixes only β `patch`, breaking β `major`. | ||
| 2. **Changelog entry** β reuse the existing `[Unreleased]` section if present, otherwise offer to | ||
| generate one from `git log` since the last tag. | ||
| ## Steps | ||
| ### 1. Detect host and project type | ||
| ```bash | ||
| git remote get-url origin # must be github.com β else stop | ||
| git fetch --tags --quiet | ||
| git tag --sort=-v:refname | head -1 # latest tag (current released version) | ||
| ``` | ||
| Detect the **project type** and follow the matching partial: | ||
| | Signal | Project type | Use partial | | ||
| |--------|--------------|-------------| | ||
| | `*.php` with a `* Version:` plugin header | WordPress plugin | `release-wordpress.md` | | ||
| | `package.json` (no WP header) | Node / npm | `release-node.md` | | ||
| | neither | Generic | inline fallback: bump a `VERSION` file / changelog only | | ||
| ### 2. Determine the new version | ||
| - Read the **current** version from the source of truth named in the matching partial (not just the | ||
| git tag β they can drift). | ||
| - Apply the bump type to compute `X.Y.Z`. | ||
| - **NEVER reuse an existing tag** β tags are immutable. If `vX.Y.Z` already exists, bump again. | ||
| ### 3. Run quality checks (per partial) | ||
| Run the ecosystem's checks from the matching partial (WordPress: phpcs/phpstan; Node: `npm test` + | ||
| lint/type-check/build). **Do not proceed if any check fails.** | ||
| ### 4. Bump the version (per partial) | ||
| Update every version file listed in the matching partial (WordPress: plugin header / `readme.txt` / | ||
| `composer.json`; Node: `package.json` + `package-lock.json`). | ||
| ### 5. Update CHANGELOG.md | ||
| Promote the `[Unreleased]` section to the new version, keeping a Keep-a-Changelog structure: | ||
| ```markdown | ||
| ## [X.Y.Z] - YYYY-MM-DD | ||
| ### Added/Changed/Fixed | ||
| - ... | ||
| ``` | ||
| If there is no `[Unreleased]` section, add a new `## [X.Y.Z]` block at the top with the changes. | ||
| ### 6. Create release branch, commit, and PR | ||
| ```bash | ||
| BASE_BRANCH=$(node -e "try{const c=require('./.agents-toolkit.json');console.log(c.pr?.targetBranch||c.git?.defaultBranch||'main')}catch{console.log('main')}") | ||
| git checkout -b release/vX.Y.Z | ||
| git add -A | ||
| git commit -m "chore: bump version to X.Y.Z" | ||
| git push -u origin release/vX.Y.Z | ||
| gh pr create --base "$BASE_BRANCH" --title "Release vX.Y.Z" --body "## Changes | ||
| - changelog entry" | cat | ||
| ``` | ||
| ### 7. Analyze workflows β decide tag-only vs GitHub Release | ||
| **This is the critical step.** Read the repo's workflows and tell the user exactly what to do after | ||
| the release PR merges: | ||
| ```bash | ||
| ls .github/workflows/ 2>/dev/null | ||
| # inspect the `on:` triggers and publish/build steps of each workflow | ||
| ``` | ||
| Decide from the triggers: | ||
| | Workflow trigger | Post-merge action | Why | | ||
| |------------------|-------------------|-----| | ||
| | `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 | | ||
| | `on: push: tags: ['v*']` | **Push the tag** (`git push origin vX.Y.Z`) β Release optional | The tag push alone triggers the workflow | | ||
| | neither / no workflow | **Push the tag** for history; optionally `gh release create` for visibility | No automation depends on it | | ||
| Also scan the workflow **steps** and report what the release produces (e.g. `npm publish`, plugin | ||
| ZIP artifact, Docker image) so the user knows what will happen. | ||
| ### 8. Post-merge instructions (tailored to step 7) | ||
| Give the exact commands for the detected flow, e.g.: | ||
| ```bash | ||
| git checkout "$BASE_BRANCH" && git pull | ||
| git tag vX.Y.Z | ||
| git push origin vX.Y.Z | ||
| # If a `release:`-triggered workflow exists, ALSO create the Release so it fires: | ||
| gh release create vX.Y.Z --generate-notes | ||
| ``` | ||
| ## Important | ||
| - **NEVER reuse an existing tag** β tags are immutable. If a tag exists, bump to the next version. | ||
| - The `release-management` skill has full documentation for WordPress plugins β use it for troubleshooting. | ||
| - Some projects use `master` instead of `main` β always verify the default branch with | ||
| `gh repo view --json defaultBranchRef | cat`. | ||
| - A bare tag and a GitHub Release are **not** interchangeable: many publish/build workflows only run | ||
| on the `release` event. Always complete step 7 before telling the user the release is done. |
| --- | ||
| name: nextjs-caching | ||
| 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. | ||
| --- | ||
| # Next.js Caching Skill | ||
| Canonical caching strategy for Silver Side Next.js (App Router) frontends. Use it when touching data | ||
| fetching, route segment configs, the asset/page proxy, image config, or the on-demand revalidate | ||
| route β and when diagnosing "why isn't this page cached / why is it serving stale". | ||
| > Companion: `caching.instructions.md` (the short, auto-applied rules). This skill is the deep | ||
| > reference and decision guide. If a project keeps a `docs/CACHING.md`, that is its project-specific | ||
| > extension (endpoints, CloudFront IDs, per-route tiers) and is never overwritten by the toolkit. | ||
| --- | ||
| ## Core principle | ||
| **Cache by intent (read vs. mutation), never by the HTTP method.** Some reads must use `POST` because | ||
| they take a body (e.g. CCDS `geo-search`). They still have to cache like a `GET`. Mutations must never | ||
| cache. Getting this wrong silently turns a static page into a per-request dynamic render. | ||
| --- | ||
| ## The caching layers (independent) | ||
| Fixing one does **not** fix the others. | ||
| | # | Layer | Where | Purpose | | ||
| |---|-------|-------|---------| | ||
| | 1 | ISR / page cache | `export const revalidate` per route | Time-based regeneration of rendered pages | | ||
| | 2 | Data-fetch cache | `next: { revalidate, tags }` on each `fetch` | Cross-request caching of CCDS/WP responses | | ||
| | 3 | Request dedup | `React.cache()` around client fns | Collapse duplicate calls within one render | | ||
| | 4 | Asset proxy | `src/app/assets/[...path]/route.ts` | Long-lived `Cache-Control` on WP images/CSS/fonts | | ||
| | 5 | Edge / CDN | `src/proxy.ts` + CloudFront | `s-maxage` at the edge + on-demand invalidation | | ||
| **Layer 2 is the most commonly broken.** A page can be ISR-enabled (layer 1) yet still hit origin on | ||
| every request if its data fetches (layer 2) are not cached β and an uncached fetch also forces the | ||
| whole route into **dynamic rendering**, which emits `cache-control: private, no-cache, no-store`. | ||
| --- | ||
| ## How Next.js 16 actually decides (the rule behind the rule) | ||
| From `next/dist/server/lib/patch-fetch.js`: | ||
| - `fetch` is **not cached by default**. A non-`GET` method is "uncacheable" **only when no explicit | ||
| cache config is provided**. Concretely, a POST is auto-no-cached only when it has no `next`/`cache` | ||
| options *and* the segment `revalidate` is `0`. | ||
| - Therefore **a `POST` IS cached cross-request when you pass an explicit `next: { revalidate }`** (and | ||
| the segment isn't `revalidate: 0`). This is why the WordPress GraphQL POST caches fine. | ||
| - The **request body is part of the cache key** (`incremental-cache` β `generateCacheKey` hashes | ||
| `init.body`), so different filter bodies cache independently and never collide. | ||
| **Implication:** you do **not** need `unstable_cache` or a proxy header override to cache POST reads. | ||
| Just send `next: { revalidate, tags }`. | ||
| --- | ||
| ## Canonical API client (read vs. mutation) | ||
| ```ts | ||
| interface FetchOptions { | ||
| endpoint: string; | ||
| method?: "GET" | "POST" | "PUT" | "DELETE"; | ||
| body?: object | null; | ||
| revalidateTag?: string; | ||
| revalidate?: number; | ||
| /** Writes are never cached. Reads (default) cache regardless of method. */ | ||
| mutation?: boolean; | ||
| } | ||
| export async function fetchData<T>({ | ||
| endpoint, | ||
| method = "GET", | ||
| body = null, | ||
| revalidateTag, | ||
| revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation | ||
| mutation = false, | ||
| }: FetchOptions): Promise<T | null> { | ||
| const isMutation = mutation || method === "PUT" || method === "DELETE"; | ||
| const requestOptions: RequestInit = { | ||
| method, | ||
| // Reads (GET or POST) cache cross-request via `next`; mutations opt out. | ||
| ...(isMutation | ||
| ? { cache: "no-store" as RequestCache } | ||
| : { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }), | ||
| }; | ||
| if ((method === "POST" || method === "PUT") && body) { | ||
| requestOptions.body = JSON.stringify(body); | ||
| } | ||
| // ...fetch + error handling... | ||
| } | ||
| ``` | ||
| Mark every write explicitly: | ||
| ```ts | ||
| await fetchData({ endpoint: "community/submit-review", method: "POST", body, mutation: true }); | ||
| ``` | ||
| WordPress GraphQL client (POST read β must carry `next`): | ||
| ```ts | ||
| await fetch(WP_API_URL, { | ||
| method: "POST", | ||
| body: JSON.stringify({ query, variables }), | ||
| next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options | ||
| }); | ||
| ``` | ||
| --- | ||
| ## ISR revalidate tiers | ||
| | Route type | `export const revalidate` | | ||
| |------------|---------------------------| | ||
| | Listing / index | `2592000` (30d) β webhook handles freshness | | ||
| | Detail / city / community (`[slug]`) | `86400` (24h) | | ||
| | State / advisor landing | `604800` (7d) | | ||
| | WP catch-all (`[[...uri]]`) | `604800` (7d) | | ||
| - Every cacheable route exports `revalidate`. Do **not** add it to form/personalized routes. | ||
| - Prefer `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then | ||
| preserves the last good cache instead of overwriting it with a broken page. Pair with a client that | ||
| **throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the | ||
| build phase (so `generateStaticParams` can skip a bad entry without failing the build). | ||
| --- | ||
| ## On-demand revalidation (dual invalidation) | ||
| `/api/revalidate` must invalidate **both** layers in one request, or the CDN serves stale until its | ||
| own TTL expires: | ||
| ```ts | ||
| revalidatePath(path, "page"); // or "layout" | ||
| revalidateTag(tag); // granular per-state / per-city tags | ||
| if (invalidateCDN) await invalidateCloudFrontPaths([path]); | ||
| ``` | ||
| --- | ||
| ## Anti-patterns | ||
| - β `next: method === "GET" ? {...} : { revalidate: 0 }` β leaves POST reads uncached β the route | ||
| renders dynamically (`private, no-store`). This is the exact regression that broke city pages. | ||
| - β Bare `fetch(url, { method: "POST", body })` for a read β refetched from origin every render. | ||
| - β `next: { tags }` with no `revalidate` β holds stale data or doesn't cache at runtime. | ||
| - β Caching a mutation (`submit`, lead, `PUT`/`DELETE`/`PATCH`). | ||
| - β Treating `React.cache()` as cross-request caching (it's request-scoped dedup only). | ||
| - β `dynamic = "force-static"` on a page whose revalidation can fail (caches a broken page). | ||
| - β Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` β | ||
| that is a separate, deliberate migration; do not introduce it ad hoc. | ||
| --- | ||
| ## Diagnosing "page is not cached" | ||
| 1. **Check the response header.** `cache-control: private, no-cache, no-store` β the route is rendering | ||
| dynamically. Something opted it into dynamic rendering. | ||
| 2. **Find the dynamic trigger.** Usually an uncached fetch (a POST read without `next`, or | ||
| `revalidate: 0`), or a request-time API (`cookies()`, `headers()`, `searchParams`). | ||
| 3. **Fix layer 2 first.** Give read fetches `next: { revalidate, tags }`; mark mutations `no-store`. | ||
| 4. **Confirm the route exports `revalidate`.** | ||
| 5. **Reconcile CDN vs. ISR TTLs.** If ISR is 24h but CDN `s-maxage` is shorter, an ISR revalidation | ||
| should trigger a CloudFront invalidation for that path. | ||
| --- | ||
| ## Verification | ||
| ```bash | ||
| # A POST-read page (e.g. a city) must be publicly cacheable: | ||
| curl -sI https://<host>/<care-type>/<state>/<city> | grep -i cache-control | ||
| # expect: cache-control: public, ... (NOT private/no-store) | ||
| ``` | ||
| In the build output, `β` (or "Static"/"ISR") means prerendered; `Ζ` ("Dynamic") means it renders per | ||
| request β a POST-read page should be the former. `next.config` `logging.fetches.fullUrl: true` shows | ||
| per-fetch cache decisions in dev. |
+5
-3
@@ -54,3 +54,3 @@ #!/usr/bin/env node | ||
| instructions: { | ||
| react: ['css-styling', 'react-components', 'server-actions', 'tests', 'typescript'], | ||
| react: ['caching', 'css-styling', 'react-components', 'seo-ai-optimization', 'server-actions', 'tests', 'typescript'], | ||
| wordpress: ['php-standards', 'wordpress-plugin-architecture', 'testing-standards'], | ||
@@ -66,3 +66,3 @@ universal: ['documentation-language', 'github-workflow'], | ||
| 'create-github-pr', 'finalize-github-pr', | ||
| 'review-code', 'fix-issues', 'add-tests', 'prepare-release', | ||
| 'review-code', 'fix-issues', 'add-tests', 'prepare-github-release', | ||
| ], | ||
@@ -73,2 +73,4 @@ jira: ['analyze-ticket', 'work-ticket', 'create-pr', 'finalize-pr'], | ||
| partials: { | ||
| react: ['release-node'], | ||
| wordpress: ['release-wordpress'], | ||
| jira: ['jira-integration'], | ||
@@ -79,3 +81,3 @@ github: ['github-integration'], | ||
| skills: { | ||
| react: ['component-architecture', 'testing-patterns'], | ||
| react: ['component-architecture', 'nextjs-caching', 'testing-patterns'], | ||
| wordpress: ['create-component', 'plugin-creation', 'quality-checks', 'testing'], | ||
@@ -82,0 +84,0 @@ universal: ['domain-driven-design', 'release-management'], |
+2
-2
@@ -38,3 +38,3 @@ # PolyForm Noncommercial License 1.0.0 | ||
| > Required Notice: Copyright SilverAssist (https://silverassist.com) | ||
| > Required Notice: Copyright SilverAssist (<https://silverassist.com>) | ||
@@ -136,2 +136,2 @@ ## Changes and New Works License | ||
| Required Notice: Copyright 2026 SilverAssist (https://silverassist.com) | ||
| Required Notice: Copyright 2026 SilverAssist (<https://silverassist.com>) |
+1
-1
| { | ||
| "name": "@silverassist/agents-toolkit", | ||
| "version": "2.4.0", | ||
| "version": "2.5.0", | ||
| "description": "Reusable AI agent prompts for development workflows with Jira integration β supports GitHub Copilot, Claude Code, and Codex", | ||
@@ -5,0 +5,0 @@ "author": "Santiago Ramirez", |
+6
-0
@@ -234,2 +234,3 @@ # @silverassist/agents-toolkit | ||
| | `finalize-github-pr` | Finalize and merge PR (GitHub) | `{issue-number}` | GitHub | | ||
| | `prepare-github-release` | Prepare a GitHub release (auto-detects WordPress vs Node, tag vs Release) | β | GitHub | | ||
@@ -420,2 +421,4 @@ ### Utility | ||
| | `pr-template.md` | Pull request templates (GitHub Issues + Jira) | | ||
| | `release-node.md` | Node/npm release bump & quality checks (used by `prepare-github-release`) | | ||
| | `release-wordpress.md` | WordPress plugin release bump & quality checks (used by `prepare-github-release`) | | ||
@@ -433,2 +436,4 @@ ## Instructions | ||
| | `css-styling.instructions.md` | `*.css, *.tsx` | Tailwind CSS & shadcn/ui standards | | ||
| | `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 | | ||
| | `seo-ai-optimization.instructions.md` | `*.tsx` | Semantic HTML, accessibility tree, metadata, JSON-LD & E-E-A-T for AI Search | | ||
@@ -443,2 +448,3 @@ ## Skills | ||
| | `domain-driven-design` | DDD principles, domain organization, barrel exports | | ||
| | `nextjs-caching` | Next.js caching strategy: read-vs-mutation fetch, ISR tiers, CDN invalidation, diagnosing dynamic-render leaks | | ||
| | `testing-patterns` | Jest + RTL patterns for Next.js 15 and Server Actions | | ||
@@ -445,0 +451,0 @@ |
+7
-2
@@ -6,3 +6,3 @@ /** | ||
| export const VERSION = "2.4.0"; | ||
| export const VERSION = "2.5.0"; | ||
@@ -18,4 +18,4 @@ export const PROMPTS = { | ||
| "finalize-pr", | ||
| "prepare-github-release", | ||
| "prepare-pr", | ||
| "prepare-release", | ||
| "work-github-issue", | ||
@@ -41,2 +41,4 @@ "work-ticket", | ||
| "pr-template", | ||
| "release-node", | ||
| "release-wordpress", | ||
| "validations", | ||
@@ -46,2 +48,3 @@ ]; | ||
| export const INSTRUCTIONS = [ | ||
| "caching", | ||
| "css-styling", | ||
@@ -52,2 +55,3 @@ "documentation-language", | ||
| "react-components", | ||
| "seo-ai-optimization", | ||
| "server-actions", | ||
@@ -65,2 +69,3 @@ "testing-standards", | ||
| "domain-driven-design", | ||
| "nextjs-caching", | ||
| "plugin-creation", | ||
@@ -67,0 +72,0 @@ "quality-checks", |
@@ -16,7 +16,9 @@ # Copilot Coding Agent Instructions | ||
| [Instructions]|root:.github/instructions | ||
| |css-styling.instructions.md β CSS/Tailwind patterns, cn() utility, responsive design | ||
| |react-components.instructions.md β Component structure, exports, props, early returns | ||
| |server-actions.instructions.md β Server action patterns, validation, error handling | ||
| |tests.instructions.md β Test structure, mocking, assertions | ||
| |typescript.instructions.md β Type safety, destructuring, JSDoc | ||
| |caching.instructions.md β Next.js caching: read-vs-mutation fetch, ISR tiers, CDN invalidation | ||
| |css-styling.instructions.md β CSS/Tailwind patterns, cn() utility, responsive design | ||
| |react-components.instructions.md β Component structure, exports, props, early returns | ||
| |seo-ai-optimization.instructions.md β Semantic HTML, a11y tree, metadata, JSON-LD for AI Search | ||
| |server-actions.instructions.md β Server action patterns, validation, error handling | ||
| |tests.instructions.md β Test structure, mocking, assertions | ||
| |typescript.instructions.md β Type safety, destructuring, JSDoc | ||
@@ -137,2 +139,26 @@ [Prompts]|root:.github/prompts | ||
| ## ποΈ Caching Rules (CRITICAL) | ||
| | Rule | Requirement | | ||
| |------|-------------| | ||
| | **Cache by intent** | Cache reads, never mutations β decide on intent, NOT the HTTP method | | ||
| | **POST reads** | A `POST` read (e.g. filter/`geo-search`) MUST set `next: { revalidate, tags }` to cache | | ||
| | **Mutations** | `submit`/lead/`PUT`/`DELETE` β `cache: "no-store"` (flag with `mutation: true`) | | ||
| | **ISR** | Every cacheable route exports `revalidate`; never on form/personalized routes | | ||
| | **Invalidation** | `/api/revalidate` calls `revalidateTag`/`revalidatePath` AND the CDN invalidation | | ||
| ```ts | ||
| // β WRONG: leaves POST reads uncached β route turns dynamic (private, no-store) | ||
| next: method === "GET" ? { revalidate, tags } : { revalidate: 0 } | ||
| // β CORRECT: reads cache regardless of method; mutations opt out | ||
| const isMutation = mutation || method === "PUT" || method === "DELETE"; | ||
| ...(isMutation ? { cache: "no-store" } : { next: { revalidate, tags } }) | ||
| ``` | ||
| π **Full details:** `.github/instructions/caching.instructions.md` | ||
| π **Project-specific context (if present):** `docs/CACHING.md` | ||
| --- | ||
| ## π Git Conventions | ||
@@ -195,4 +221,6 @@ | ||
| | Creating server actions | `server-actions.instructions.md` | | ||
| | Data fetching / `next.config` / routes / ISR / revalidate | `caching.instructions.md` | | ||
| | Pages, metadata, JSON-LD, accessibility/SEO | `seo-ai-optimization.instructions.md` | | ||
| | Writing tests | `tests.instructions.md` | | ||
| | TypeScript questions | `typescript.instructions.md` | | ||
| | **Before pushing/PR** | `tests.instructions.md` + run quality checks | |
@@ -88,1 +88,2 @@ # Validations Partial | ||
| - [ ] JSDoc comments on new functions | ||
| - [ ] If data fetching / routes / `next.config` changed: caching follows `caching.instructions.md` (reads cache regardless of method, mutations `no-store`, cacheable routes export `revalidate`) |
@@ -58,2 +58,10 @@ --- | ||
| ### Caching Impact (Next.js) | ||
| If the feature touches data fetching, routes, or `next.config`, state the caching plan: | ||
| - Reads vs. mutations (mutations never cached; reads cache regardless of GET/POST) | ||
| - `revalidate` tier + cache `tags` for any new fetches | ||
| - Whether new routes export `revalidate`, and any on-demand invalidation (Next.js + CDN) needed | ||
| See `.github/instructions/caching.instructions.md`. | ||
| ## Risk Assessment | ||
@@ -60,0 +68,0 @@ |
@@ -59,2 +59,14 @@ --- | ||
| ### 5. Caching & Data Fetching (Next.js) | ||
| > See `.github/instructions/caching.instructions.md`. Caching is decided by **read-vs-mutation**, not the HTTP method. | ||
| - [ ] Reads cache regardless of method β a `POST` read (e.g. `geo-search`) sets `next: { revalidate, tags }` (NOT `revalidate: 0`) | ||
| - [ ] No `next: method === "GET" ? {...} : { revalidate: 0 }` gating (leaves POST reads uncached β route turns dynamic, `private, no-store`) | ||
| - [ ] Mutations (`submit`/lead/`PUT`/`DELETE`) use `cache: "no-store"` and are never cached | ||
| - [ ] `tags` always paired with a `revalidate` duration (no bare `next: { tags }`) | ||
| - [ ] Cacheable routes export `revalidate`; not added to form/personalized routes | ||
| - [ ] `React.cache()` not used as a substitute for cross-request caching | ||
| - [ ] On-demand revalidation invalidates both Next.js (`revalidateTag`/`revalidatePath`) and the CDN | ||
| ## Output | ||
@@ -61,0 +73,0 @@ |
| --- | ||
| description: Prepare a new release for a Silver Assist WordPress plugin (version bump, changelog, tag, PR) | ||
| agent: agent | ||
| tools: | ||
| - run_in_terminal | ||
| - read_file | ||
| - replace_string_in_file | ||
| - create_file | ||
| --- | ||
| # Prepare Release | ||
| Prepare a new version release for the current Silver Assist plugin. | ||
| ## Inputs | ||
| Ask the user: | ||
| 1. **Version type** β Is this a `patch`, `minor`, or `major` release? (default: patch) | ||
| 2. **Changelog entry** β What changed? (or offer to generate from recent commits) | ||
| ## Steps | ||
| 1. **Detect plugin** β Find the plugin root, main plugin file, and current version. | ||
| 2. **Determine new version** β Based on the version type, calculate the next semantic version. | ||
| 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. | ||
| 4. **Update version** β Run the version update script: | ||
| ```bash | ||
| ./scripts/update-version.sh X.Y.Z | ||
| ``` | ||
| Or if the plugin uses the simple variant: | ||
| ```bash | ||
| ./scripts/update-version-simple.sh X.Y.Z | ||
| ``` | ||
| This updates the version in the main plugin file, `readme.txt` (if present), and `composer.json`. | ||
| 5. **Update CHANGELOG.md** β Add a new entry at the top following this format: | ||
| ```markdown | ||
| ## [X.Y.Z] - YYYY-MM-DD | ||
| ### Added/Changed/Fixed | ||
| - Description of change | ||
| ``` | ||
| 6. **Create release branch and commit** β Following the branch naming convention: | ||
| ```bash | ||
| git checkout -b release/vX.Y.Z | ||
| git add -A | ||
| git commit -m "chore: bump version to X.Y.Z" | ||
| ``` | ||
| 7. **Push and create PR** β Push the branch and create a PR: | ||
| ```bash | ||
| git push origin release/vX.Y.Z | ||
| gh pr create --title "Release vX.Y.Z" --body "## Changes\n\n- changelog entry" | cat | ||
| ``` | ||
| 8. **Post-merge instructions** β Remind the user: | ||
| - After merging the PR, create the tag from the main/master branch: | ||
| ```bash | ||
| git checkout main && git pull | ||
| git tag vX.Y.Z | ||
| git push origin vX.Y.Z | ||
| ``` | ||
| - The GitHub Actions release workflow will automatically build the ZIP and create the GitHub Release. | ||
| ## Important | ||
| - **NEVER reuse an existing tag** β tags are immutable. If a tag exists, bump to the next version. | ||
| - The release-management skill has full documentation β use it for troubleshooting. | ||
| - Some plugins use `master` instead of `main` (e.g., silver-assist-post-revalidate). | ||
| - Always verify the default branch with `git branch --show-current` or `gh repo view --json defaultBranchRef | cat`. |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
405196
8.2%64
8.47%1462
0.48%592
1.02%4
-20%