@linchpinagency/skills 0.1.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.
Files changed (38) hide show
  1. package/README.md +337 -0
  2. package/bin/install.mjs +231 -0
  3. package/package.json +44 -0
  4. package/skills/browser-automation/SKILL.md +93 -0
  5. package/skills/commit-and-release/SKILL.md +135 -0
  6. package/skills/dependency-updates/SKILL.md +102 -0
  7. package/skills/design-previews/SKILL.md +118 -0
  8. package/skills/engagement-types/SKILL.md +108 -0
  9. package/skills/investigate/SKILL.md +95 -0
  10. package/skills/project-context/SKILL.md +89 -0
  11. package/skills/quality-gates/SKILL.md +94 -0
  12. package/skills/quality-gates/references/toolchain.md +104 -0
  13. package/skills/safety-hooks/SKILL.md +121 -0
  14. package/skills/safety-hooks/scripts/check-destructive.sh +80 -0
  15. package/skills/safety-hooks/scripts/check-edit-boundary.sh +65 -0
  16. package/skills/support-triage/SKILL.md +103 -0
  17. package/skills/task-tracking/SKILL.md +243 -0
  18. package/skills/web-qa/SKILL.md +108 -0
  19. package/skills/web-qa/references/qa-checklist.md +98 -0
  20. package/skills/wordpress-blocks/SKILL.md +110 -0
  21. package/skills/wordpress-blocks/references/block-grammar.md +94 -0
  22. package/skills/wordpress-blocks/references/core-blocks.md +123 -0
  23. package/skills/wordpress-blocks/references/patterns-and-parts.md +70 -0
  24. package/skills/wordpress-blocks/references/recipes/faq.md +56 -0
  25. package/skills/wordpress-blocks/references/recipes/hero.md +74 -0
  26. package/skills/wordpress-blocks/references/recipes/pricing-table.md +77 -0
  27. package/skills/wordpress-blocks/references/tool-contract.md +167 -0
  28. package/skills/wordpress-blocks/references/validation.md +38 -0
  29. package/skills/wp-audit/SKILL.md +115 -0
  30. package/skills/wp-block-conventions/SKILL.md +134 -0
  31. package/skills/wp-block-conventions/references/block-anatomy.md +175 -0
  32. package/skills/wp-implementation-choice/SKILL.md +88 -0
  33. package/skills/wp-local-setup/SKILL.md +262 -0
  34. package/skills/wp-pressable/SKILL.md +172 -0
  35. package/skills/wp-studio-cli/SKILL.md +165 -0
  36. package/skills/write-a-linchpin-skill/SKILL.md +195 -0
  37. package/skills/write-a-linchpin-skill/references/template.md +83 -0
  38. package/upstream.json +20 -0
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: wp-audit
3
+ description: Audit a WordPress site for performance, accessibility, and visible frontend quality, then report prioritized fixes and re-measure after changes. Use when asked to review, optimize, QA, or "check why the site is slow", when Core Web Vitals or Lighthouse scores come up, when a client asks about accessibility or WCAG, or before handing a build back to a client. Measures locally via the Studio MCP; production numbers require the live site. Not for fixing lint or tests — use `quality-gates`.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # WordPress audit
8
+
9
+ An audit is only worth what its evidence is worth. The job is to **measure what can be
10
+ measured, inspect what can't, and never blur the line between them** — an agency report that
11
+ implies a tool ran when a model eyeballed a screenshot is worse than no report.
12
+
13
+ Start with the smallest audit that answers the question actually asked.
14
+
15
+ ## When to use
16
+
17
+ - "Why is the site slow?", Core Web Vitals, Lighthouse, or page-weight questions.
18
+ - Accessibility questions — contrast, focus states, readability, WCAG.
19
+ - Frontend QA or a polish pass before handing work back to a client.
20
+ - Verifying that a fix actually improved things (before/after).
21
+
22
+ **Not this skill:** lint, PHPCS, PHPStan, or tests — [`quality-gates`](../quality-gates/SKILL.md).
23
+ Performance *techniques* (caching, asset strategy) — the upstream `wp-performance` skill.
24
+ Operating the local site — [`wp-studio-cli`](../wp-studio-cli/SKILL.md). Operating a live
25
+ server — [`wp-pressable`](../wp-pressable/SKILL.md).
26
+
27
+ ## Owns
28
+
29
+ Canonical for: audit scope selection, the measurement loop, the thresholds below, evidence
30
+ honesty, and the before/after comparison. Defers the fixes themselves to the skill that owns
31
+ the code being changed.
32
+
33
+ ## Preflight — what are you actually measuring?
34
+
35
+ | Target | How | Caveat |
36
+ | --- | --- | --- |
37
+ | Local Studio site | [`wp-studio-cli`](../wp-studio-cli/SKILL.md) → `need_for_speed`, `take_screenshot`, `inspect_design` | **Synthetic.** PHP-WASM + SQLite, no CDN, no production cache — good for diagnosis and before/after, not a production claim |
38
+ | Production / staging | Real-browser tooling against the live URL; server state via [`wp-pressable`](../wp-pressable/SKILL.md) read-only commands | The only numbers you can quote to a client as real |
39
+ | Legacy local (wp-env / LocalWP) | Browser tooling against the local URL | Studio MCP tools won't apply |
40
+
41
+ Resolve two things before measuring: **which environment**, and **which URL path** (default
42
+ `/` if unspecified). State both in the report — an audit without them isn't reproducible.
43
+
44
+ ## Procedure
45
+
46
+ 1. **Pick the scope** — performance, accessibility, visual QA, or a named combination. Say
47
+ which you chose when the request was ambiguous. → The user knows what's being measured.
48
+ 2. **Measure** with the tools the environment offers. Take a screenshot for anything visual
49
+ so findings are anchored to evidence. → Raw numbers and images captured, not recalled.
50
+ 3. **Interpret against the thresholds** below rather than by feel. → Every finding is
51
+ Good / Needs improvement / Poor, with the measured value shown.
52
+ 4. **Diagnose in WordPress terms.** A number is not a finding; "LCP 4.1s because the hero
53
+ image is an unresized 2.4 MB PNG" is. Inspect the theme, plugins, and block markup to get
54
+ from symptom to cause. → Each finding names a probable cause and where it lives.
55
+ 5. **Prioritize ruthlessly.** A short list ordered by impact-per-effort beats an exhaustive
56
+ one. → Top findings first; the long tail summarized, not enumerated.
57
+ 6. **Re-measure after fixes**, same environment and path, and report what moved, what didn't,
58
+ and any tradeoff taken. → Before/after pairs for every metric you claimed to improve.
59
+
60
+ ## Thresholds
61
+
62
+ Google's Core Web Vitals bands — these are the public standard, not house opinion:
63
+
64
+ | Metric | Good | Needs improvement | Poor |
65
+ | --- | --- | --- | --- |
66
+ | TTFB | < 800 ms | 800–1800 ms | > 1800 ms |
67
+ | FCP | < 1800 ms | 1800–3000 ms | > 3000 ms |
68
+ | LCP | < 2500 ms | 2500–4000 ms | > 4000 ms |
69
+ | CLS | < 0.1 | 0.1–0.25 | > 0.25 |
70
+
71
+ Page-composition warning signs: DOM > 1500 elements · page weight > 3 MB · > 80 requests ·
72
+ scripts > 20 files or 500 KB · stylesheets > 10 files or 200 KB.
73
+
74
+ Common WordPress causes worth checking before anything exotic: oversized or unresized
75
+ images, heavy or duplicated plugins, render-blocking and duplicated font loads, unused theme
76
+ CSS shipped in full, and wrapper-heavy block layouts inflating the DOM.
77
+
78
+ ## Accessibility — say what you actually did
79
+
80
+ Without a dedicated scanner, this pass is **visual review plus code inspection**, and the
81
+ report must say so. Never describe it as a WCAG audit, never assign a conformance level, and
82
+ never imply a client is compliant on this basis. When a real conformance claim is needed,
83
+ say that a dedicated accessibility audit is required — that's a different engagement.
84
+
85
+ What this pass reliably catches: low text/background contrast, weak or ambiguous CTA states,
86
+ missing or invisible focus states, animation with no `prefers-reduced-motion` fallback,
87
+ readability problems from font size / line height / density, and meaning carried by color
88
+ alone. Use `inspect_design` for computed styles when it beats reading the CSS.
89
+
90
+ ## Guardrails
91
+
92
+ - **Never present synthetic local numbers as production performance.** Label the environment
93
+ on every number you report.
94
+ - **Never claim a check ran that didn't.** If a conclusion came from looking at a screenshot
95
+ or reading code, say so in the finding.
96
+ - **Never assert WCAG conformance** from this pass — see above.
97
+ - **Never change code during an audit** unless the user asked for fixes too; an audit that
98
+ quietly edits things can't be trusted as a baseline.
99
+ - **Never run mutating commands against production** to gather evidence — read-only
100
+ diagnostics via [`wp-pressable`](../wp-pressable/SKILL.md), and confirm before anything else.
101
+ - **Don't over-optimize one dimension silently.** When performance, accessibility, and design
102
+ conflict, surface the tradeoff and let the user choose.
103
+ - Don't pad the report. Ten low-impact nitpicks buried around two real problems is a worse
104
+ deliverable than the two real problems.
105
+
106
+ ## Done
107
+
108
+ - [ ] Environment and URL path stated, and the environment's limits noted.
109
+ - [ ] Scope stated, and it matches what was asked.
110
+ - [ ] Every metric reported with its measured value and threshold band.
111
+ - [ ] Every finding has a probable cause and a location, not just a symptom.
112
+ - [ ] Evidence source labeled per finding — measured, inspected, or observed visually.
113
+ - [ ] No WCAG conformance claim; accessibility limits stated plainly.
114
+ - [ ] Findings ordered by impact; the top fixes are actionable as written.
115
+ - [ ] If fixes were made, before/after numbers from the same environment and path.
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: wp-block-conventions
3
+ description: Build and edit custom Gutenberg blocks the way Linchpin structures them — apiVersion 3 under the `linchpin/` namespace, dynamic `render.php` plus Interactivity API `view.js`, parent/child blocks wired through block context, and the shared wp-scripts build with `--blocks-manifest --webpack-copy-php --experimental-modules`. Use when creating a custom block, editing an existing one, deciding whether a block belongs in the shared linchpin-blocks plugin or a project's functionality plugin, or reviewing block code. Not for authoring page content from blocks — use `wordpress-blocks`.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Linchpin block conventions
8
+
9
+ Linchpin blocks are **dynamic, Interactivity-API blocks** built with `wp-scripts` inside a
10
+ nested `blocks/` workspace. The structure is consistent across the shared block library and
11
+ project functionality plugins, so a block written to these conventions can move between them.
12
+
13
+ This skill is the house layer. Generic "how blocks work" is the upstream
14
+ `wp-block-development` skill — don't restate it here; read it for the underlying APIs.
15
+
16
+ ## When to use
17
+
18
+ - Creating a new custom block, or extending an existing one.
19
+ - Deciding where a block belongs — shared library vs one project.
20
+ - Reviewing block code for house consistency.
21
+ - A block builds but doesn't register, or the editor shows it as invalid.
22
+
23
+ **Not this skill:** composing page/post content out of existing blocks —
24
+ [`wordpress-blocks`](../wordpress-blocks/SKILL.md). Choosing whether the request even needs
25
+ a block — [`wp-implementation-choice`](../wp-implementation-choice/SKILL.md). Block theme
26
+ templates and `theme.json` — upstream `wp-block-themes`.
27
+
28
+ ## Owns
29
+
30
+ Canonical for: where blocks live, the file layout, house `block.json` fields, the
31
+ dynamic-render + Interactivity pattern, parent/child context, and the build/registration
32
+ chain. Defers block *semantics* to upstream and content authoring to `wordpress-blocks`.
33
+
34
+ ## Preflight — which home does this block belong in?
35
+
36
+ | Question | Home |
37
+ | --- | --- |
38
+ | Reusable across clients (accordion, tabs, cards, slider, counter)? | **`linchpin/linchpin-blocks`** — the shared plugin, `blocks/src/<slug>/` |
39
+ | Specific to one site's content model or design (a client grid, a bespoke hero)? | That project's functionality plugin — `plugins/<project>-functionality/blocks/src/<slug>/` |
40
+ | Could core or an installed plugin already do it? | Neither — see [`wp-implementation-choice`](../wp-implementation-choice/SKILL.md) |
41
+
42
+ Putting a client-specific block in the shared library is the mistake that costs later: every
43
+ other project inherits it, and it can't be changed without regression-testing all of them.
44
+
45
+ Then confirm the workspace: blocks build from a **nested `blocks/package.json`**, not the
46
+ repo root. Lint and build there.
47
+
48
+ ## The shape of a block
49
+
50
+ ```
51
+ blocks/src/<slug>/
52
+ ├── block.json # metadata — the source of truth for registration
53
+ ├── index.js # registerBlockType, wires edit/save/icon/transforms
54
+ ├── edit.js # editor component
55
+ ├── save.js # usually InnerBlocks.Content only (dynamic blocks render in PHP)
56
+ ├── render.php # server render — copied to build/ by --webpack-copy-php
57
+ ├── view.js # frontend behavior via @wordpress/interactivity (script module)
58
+ ├── style.scss # front + editor styles
59
+ ├── edit.scss # editor-only styles
60
+ ├── icon.js # block icon
61
+ ├── controls.js # inspector/toolbar controls (or controls/ when it grows)
62
+ ├── store.js # editor-side data store, when the block needs one
63
+ └── transforms.js # block transforms, when offered
64
+ ```
65
+
66
+ Only `block.json`, `index.js`, and `edit.js` are always present. Add the rest when the block
67
+ actually needs them — see [`references/block-anatomy.md`](references/block-anatomy.md) for
68
+ the field-by-field detail, the parent/child context pattern, and `render.php` conventions.
69
+
70
+ ## House rules
71
+
72
+ - **`apiVersion: 3`** and the `linchpin/` namespace on every block.
73
+ - **Dynamic by default.** Real markup lives in `render.php`; `save.js` typically returns just
74
+ `<InnerBlocks.Content />`. This avoids the block-validation invalidation that static markup
75
+ causes whenever output changes.
76
+ - **Frontend behavior is the Interactivity API** — `viewScriptModule: file:./view.js`,
77
+ `store('linchpin/<block>', { state, actions, callbacks })`, and `data-wp-*` directives
78
+ emitted from `get_block_wrapper_attributes()`. Not jQuery, not a bespoke bundle.
79
+ - **Parent → child data flows through block context**, namespaced
80
+ `linchpin/<parent>/<key>` via `providesContext` / `usesContext`. Families
81
+ (accordion + accordion-pane, tabs + tabs-pane, cards + card) are built this way.
82
+ - **Ship an `example`** with representative `innerBlocks` so the inserter preview isn't blank.
83
+ - **Lean on `supports`** (color, spacing, typography, layout, `anchor`) before inventing
84
+ custom attributes — you get the core UI and theme.json integration for free.
85
+ - **PHP logic belongs in a Model/Helper class**, not inline in `render.php`. Keep render
86
+ files to composing wrapper attributes and echoing markup.
87
+
88
+ ## Build and registration
89
+
90
+ From the `blocks/` workspace:
91
+
92
+ ```bash
93
+ npm run build # wp-scripts build --blocks-manifest --webpack-copy-php --experimental-modules …
94
+ npm run start # same, watching
95
+ ```
96
+
97
+ Each flag matters: `--blocks-manifest` emits `build/blocks-manifest.php`,
98
+ `--webpack-copy-php` copies `render.php` into `build/`, and `--experimental-modules` is what
99
+ makes `viewScriptModule` work. Dropping one silently breaks registration or frontend behavior.
100
+
101
+ Registration reads the built output, not `src/`: WordPress 6.8+ on a block theme registers
102
+ the whole set at once with `wp_register_block_types_from_metadata_collection( build,
103
+ build/blocks-manifest.php )`, with a per-block `register_block_type()` loop as the fallback.
104
+ **A block that doesn't appear in the editor is usually unbuilt, not misregistered** — build
105
+ first, then debug.
106
+
107
+ ## Guardrails
108
+
109
+ - **Never edit anything in `build/`** — it's generated, and the next build discards it.
110
+ - **Never put a client-specific block in the shared library** (see Preflight).
111
+ - **Never use `core/html`** for layout or normal text; emit proper block markup.
112
+ - **Never change a dynamic block's `save.js` output casually** — changing saved markup
113
+ invalidates every existing instance in the database. Prefer `render.php` changes, and when
114
+ saved output must change, provide a deprecation.
115
+ - **Never hand-bump a `version` field** without checking `release-please-config.json`
116
+ `extra-files` — release-please may own it ([`commit-and-release`](../commit-and-release/SKILL.md)).
117
+ - **Never add a dependency to the root `package.json`** for block code; blocks have their own
118
+ workspace manifest.
119
+ - Don't skip `--webpack-copy-php` or `--experimental-modules` in a one-off build to "go
120
+ faster" — you'll debug a phantom registration bug instead.
121
+
122
+ ## Done
123
+
124
+ - [ ] The block's home (shared library vs project plugin) is a deliberate choice.
125
+ - [ ] `block.json` is apiVersion 3, `linchpin/`-namespaced, with `textdomain`, an `example`,
126
+ and `supports` doing the work custom attributes would otherwise duplicate.
127
+ - [ ] Dynamic render in `render.php`; `save.js` doesn't hand-roll markup that PHP owns.
128
+ - [ ] Frontend behavior uses the Interactivity API with a namespaced store.
129
+ - [ ] Parent/child data passes through namespaced block context, not props or globals.
130
+ - [ ] Built from the `blocks/` workspace with all three flags; block appears in the inserter.
131
+ - [ ] Markup validates in the editor — no "unexpected or invalid content"
132
+ ([`wp-studio-cli`](../wp-studio-cli/SKILL.md) `validate_blocks`).
133
+ - [ ] `npm run lint:js` and `npm run lint:css` pass in that workspace
134
+ ([`quality-gates`](../quality-gates/SKILL.md)).
@@ -0,0 +1,175 @@
1
+ # Block anatomy
2
+
3
+ Field-level detail for a Linchpin block. The house patterns are in `SKILL.md`; this is the
4
+ lookup you open while writing the files.
5
+
6
+ ## `block.json`
7
+
8
+ ```jsonc
9
+ {
10
+ "$schema": "https://schemas.wp.org/trunk/block.json",
11
+ "apiVersion": 3,
12
+ "name": "linchpin/accordion",
13
+ "version": "1.5.7",
14
+ "title": "Accordion",
15
+ "category": "design",
16
+ "keywords": ["linchpin", "accordion", "collapse", "expand"],
17
+ "description": "Controls a group of accordion blocks.",
18
+ "textdomain": "linchpin-blocks",
19
+
20
+ "attributes": { /* only what `supports` can't express */ },
21
+ "supports": { /* prefer these over custom attributes */ },
22
+ "providesContext": { "linchpin/accordion/autoClose": "autoClose" },
23
+ "styles": [ { "name": "grouped", "label": "Grouped", "isDefault": true } ],
24
+ "example": { "attributes": {}, "innerBlocks": [ /* … */ ], "viewportWidth": 640 },
25
+
26
+ "editorScript": "file:./index.js",
27
+ "editorStyle": "file:./index.css",
28
+ "style": "file:./style-index.css",
29
+ "render": "file:./render.php",
30
+ "viewScriptModule": "file:./view.js"
31
+ }
32
+ ```
33
+
34
+ Notes:
35
+
36
+ - **`category`** — use an existing core category (`design`, `media`, `text`, `widgets`)
37
+ unless the project registers its own.
38
+ - **`keywords`** are the inserter's search surface. Include `linchpin` plus the words an
39
+ editor would actually type, including synonyms the title doesn't contain.
40
+ - **`styles`** give editors variations without new blocks. One `isDefault: true`.
41
+ - **`viewScriptModule`** (not `viewScript`) is what pairs with `--experimental-modules` and
42
+ the Interactivity API. Using `viewScript` loads a classic script instead and the
43
+ `data-wp-*` directives will do nothing.
44
+ - **`example`** without `innerBlocks` renders an empty inserter preview for container blocks.
45
+
46
+ ## `supports` before attributes
47
+
48
+ Reach for these first — they wire into theme.json, give editors the standard UI, and keep
49
+ styling in the design system:
50
+
51
+ | Need | `supports` key |
52
+ | --- | --- |
53
+ | Text/background/link color | `color` (`{ "text": …, "background": …, "link": … }`) |
54
+ | Padding, margin, gap | `spacing` (scope with `sides` / `blockGap`) |
55
+ | Font size and family | `typography` |
56
+ | Borders | `__experimentalBorder` |
57
+ | HTML anchor | `anchor` |
58
+ | Inner layout (flex/grid, orientation) | `layout` — set `allowEditing: false` to lock it |
59
+ | Interactivity API directives | `interactivity: true` |
60
+ | Suppress the "Edit as HTML" escape hatch | `html: false` |
61
+
62
+ A custom attribute is the right answer only when it carries data (a term ID, an interval, a
63
+ mode enum), not presentation.
64
+
65
+ ## Parent / child families
66
+
67
+ Container and item blocks are separate blocks joined by **block context**, namespaced by the
68
+ parent:
69
+
70
+ ```jsonc
71
+ // parent — accordion/block.json
72
+ "providesContext": {
73
+ "linchpin/accordion/autoClose": "autoClose",
74
+ "linchpin/accordion/activeUUID": "activeUUID"
75
+ }
76
+
77
+ // child — accordion-pane/block.json
78
+ "usesContext": [ "linchpin/accordion/autoClose", "linchpin/accordion/activeUUID" ]
79
+ ```
80
+
81
+ Rules:
82
+
83
+ - Namespace every context key `linchpin/<parent>/<key>` — bare keys collide across plugins.
84
+ - Constrain the family with `parent` (child only insertable inside its container) or
85
+ `allowedBlocks` on the parent, so editors can't build invalid trees.
86
+ - Context flows **down only**. A child that needs to change parent state does it through the
87
+ Interactivity store, not by mutating context.
88
+
89
+ ## `render.php`
90
+
91
+ ```php
92
+ <?php
93
+ /**
94
+ * Server-side rendering of the `linchpin/accordion` block.
95
+ *
96
+ * @package linchpin-blocks
97
+ *
98
+ * @var array $attributes Array of Block Attributes
99
+ * @var string $content Block Content
100
+ * @var \WP_Block $block WP_Block object.
101
+ */
102
+
103
+ namespace Linchpin_Blocks\Blocks;
104
+
105
+ use Linchpin_Blocks\Model\Blocks\Accordion;
106
+ use function \Linchpin_Blocks\Helper\Block_Utils\classNames;
107
+
108
+ $block_wrapper_attrs = get_block_wrapper_attributes( [
109
+ 'data-wp-interactive' => wp_json_encode( [ 'namespace' => 'linchpin/accordion' ] ),
110
+ 'data-wp-context' => wp_json_encode( $model->get_initial_context( $block, $attributes ) ),
111
+ 'data-wp-init' => 'callbacks.onAccordionInit',
112
+ 'class' => classNames( [ 'has-block-gap' => /* … */ ] ),
113
+ ] );
114
+ ```
115
+
116
+ - Always emit the wrapper through **`get_block_wrapper_attributes()`** — it carries the
117
+ `supports`-generated classes and inline styles. Hand-writing the wrapper drops them.
118
+ - `$attributes`, `$content`, `$block` are provided; document them in the docblock.
119
+ - **Escape on output** (`esc_html`, `esc_attr`, `wp_kses_post`) and pass structured data
120
+ through `wp_json_encode` for directives.
121
+ - Non-trivial logic goes in a `Model\Blocks\<Block>` class; helpers like `classNames()` and
122
+ block-gap resolution live in `Helper\Block_Utils`.
123
+
124
+ ## `view.js` — Interactivity API
125
+
126
+ ```js
127
+ import { store, getContext, getElement } from '@wordpress/interactivity';
128
+
129
+ const { state } = store( 'linchpin/accordion', {
130
+ state: {
131
+ get isOpen() {
132
+ const { isOpen, uuid } = getContext();
133
+ return isOpen.includes( uuid );
134
+ },
135
+ },
136
+ actions: { toggle: () => { /* mutate context */ } },
137
+ callbacks: { onAccordionInit: () => { /* run on init */ } },
138
+ } );
139
+ ```
140
+
141
+ - The store namespace **must match** `data-wp-interactive`'s namespace in `render.php`.
142
+ - Derived values are getters on `state`; user interactions are `actions`; lifecycle hooks are
143
+ `callbacks` referenced by `data-wp-init` / `data-wp-watch`.
144
+ - Mutate `context`, not the DOM. Directives (`data-wp-bind--*`, `data-wp-class--*`,
145
+ `data-wp-on--*`) re-render from state.
146
+ - Keep `view.js` plain module JavaScript — no JSX, no editor packages.
147
+
148
+ ## Full-width section layout
149
+
150
+ When a block renders a full-bleed section, the alignment is Gutenberg's job, not CSS's:
151
+
152
+ - Outer section: `core/group` with `{"align":"full","layout":{"type":"default"}}`.
153
+ - Immediate inner shell: `core/group` with `align:"wide"` — readable text and grids live
154
+ here, not on the full-width outer block.
155
+ - No intermediate constrained groups between the full-width section and the content shell
156
+ unless the design deliberately wants a narrow measure.
157
+ - Keep `theme.json` `contentSize` / `wideSize` aligned with the design.
158
+
159
+ **Debug order when a section still looks boxed:** wrapper alignment → serialized block
160
+ markup → template layout → `theme.json`. Custom breakout CSS is the last resort, and usually
161
+ a sign one of the four is wrong.
162
+
163
+ ## Theme-side gotcha that bites block work
164
+
165
+ Block themes do **not** automatically load `style.css` on the front end. It must be enqueued
166
+ explicitly:
167
+
168
+ ```php
169
+ add_action( 'wp_enqueue_scripts', function () {
170
+ wp_enqueue_style( '<slug>-style', get_stylesheet_uri() );
171
+ } );
172
+ ```
173
+
174
+ Missing this makes block styles look broken in a way that resembles a build problem. Enqueue
175
+ editor styles too, so the editor resembles the front end.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: wp-implementation-choice
3
+ description: Decide which WordPress abstraction a request should become — theme/template work, a custom block, a functionality plugin, existing core blocks and patterns, or an off-the-shelf plugin — before any code is written. Use when a WordPress request could be built more than one way, when someone proposes a custom plugin or block, when scoping a new feature, or when reviewing an approach that feels heavier than the problem. Not for how to build the thing once chosen.
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Choosing the WordPress implementation
8
+
9
+ Most expensive WordPress mistakes are made before the first line of code: a plugin written
10
+ for something a theme template handles, a custom block that duplicates a core block, a
11
+ bespoke build of what WooCommerce already ships. **Pick the smallest abstraction that
12
+ cleanly solves the request** — smaller means less to maintain, less to upgrade, and less to
13
+ hand to the next developer.
14
+
15
+ ## When to use
16
+
17
+ - A request could reasonably be a theme change, a block, or a plugin.
18
+ - Someone proposes building something custom and you're not sure it's warranted.
19
+ - Scoping new work, or reviewing an approach that feels heavy for the problem.
20
+
21
+ **Not this skill:** how to build each thing once chosen — see the routing table below.
22
+
23
+ ## Owns
24
+
25
+ Canonical for: the choice itself and the reasons behind it. Every "how" belongs to the skill
26
+ this routes to.
27
+
28
+ ## Preflight — what already exists?
29
+
30
+ Answer these before designing anything custom; each "yes" removes work:
31
+
32
+ 1. **Does a core block or pattern do it?** Check the site's registered patterns and template
33
+ parts first — [`wordpress-blocks`](../wordpress-blocks/SKILL.md) covers reuse-before-build.
34
+ 2. **Does the shared block library already have it?** `linchpin/linchpin-blocks` ships
35
+ accordion, tabs, cards, slider, counter, and more.
36
+ 3. **Does a well-known plugin own this problem?** Ecommerce is WooCommerce; forms, SEO, and
37
+ membership all have mature answers. Building a lesser version is a liability you maintain
38
+ forever.
39
+ 4. **Does the project's functionality plugin already have a hook or module for it?**
40
+
41
+ ## The routing table
42
+
43
+ | The deliverable is really… | Build it as | Then use |
44
+ | --- | --- | --- |
45
+ | Layout, templates, styling, a visual redesign | **Theme work** (`theme.json`, templates, parts) | Upstream `wp-block-themes` |
46
+ | Page/post content composed from existing blocks | **Content**, not code | [`wordpress-blocks`](../wordpress-blocks/SKILL.md) |
47
+ | An editor-insertable content component that core can't express | **Custom block** | [`wp-block-conventions`](../wp-block-conventions/SKILL.md) |
48
+ | Reusable behavior that must survive a theme change — admin UI, settings, REST endpoints, cron, integrations, post types | **Functionality plugin** | Upstream `wp-plugin-development` |
49
+ | Data exposed to a frontend or third party | **REST endpoint** in the plugin | Upstream `wp-rest-api` |
50
+ | Performance, accessibility, or QA review of what's there | **An audit**, not a build | [`wp-audit`](../wp-audit/SKILL.md) |
51
+
52
+ ## Decision rules
53
+
54
+ - **Presentation is never a plugin.** If the deliverable is how something looks, it belongs
55
+ in the theme or a block, not in PHP that outputs markup.
56
+ - **Reusable backend behavior is never a theme.** Anything that must survive a redesign —
57
+ post types, endpoints, scheduled jobs, integrations — goes in the functionality plugin.
58
+ Business logic in `functions.php` dies with the next theme.
59
+ - **A block is for content editors.** If nobody will ever insert it in the editor, it isn't a
60
+ block; it's a template part or plugin code.
61
+ - **Shared vs project** matters as much as which abstraction: a block or module useful to
62
+ every client belongs in the shared library, a client-specific one in that project. See
63
+ [`wp-block-conventions`](../wp-block-conventions/SKILL.md).
64
+ - **Configuration beats code.** A core block with the right `supports` and theme.json tokens
65
+ beats a custom block that hardcodes the same result.
66
+ - **When two options both fit cleanly, take the smaller one** — and say why in the PR, so the
67
+ choice is reviewable rather than assumed.
68
+
69
+ ## Guardrails
70
+
71
+ - **Never build what a well-established plugin already does** without an explicit reason the
72
+ user has accepted (licensing, a hard requirement it can't meet, unacceptable bloat).
73
+ - **Never introduce a custom block that duplicates a core block** with different styling —
74
+ that's a block style variation or theme.json work.
75
+ - **Never put business logic in a theme** because it's the file you happen to be editing.
76
+ - **Never scope a plugin's boundaries around "everything this client asked for"** — a
77
+ functionality plugin is for behavior, not a junk drawer.
78
+ - If the right answer is "this shouldn't be built," say so before designing it.
79
+
80
+ ## Done
81
+
82
+ - [ ] Existing core blocks, patterns, shared library blocks, and off-the-shelf plugins were
83
+ checked before designing anything custom.
84
+ - [ ] The chosen abstraction is the smallest that cleanly solves the request.
85
+ - [ ] Presentation and behavior are on the correct sides of the theme/plugin line.
86
+ - [ ] Shared vs project-specific placement decided deliberately.
87
+ - [ ] The reasoning is written down where a reviewer will see it.
88
+ - [ ] Execution handed to the skill that owns the "how".