# CLAUDE.md: WordPress block theme + custom blocks plugin ## About this project - **Site:** [Business name], [one-line description of what the business does] - **Audience:** [who the site is for] - **Local URL:** [http://my-site.local] - **Theme:** `[my-theme]`, a child theme of Twenty Twenty-Five (block theme / Full Site Editing) - **Custom blocks plugin:** `[my-custom-blocks]` - **Active third-party plugins:** [e.g. an SEO plugin, a backup plugin]. Keep this list short, see "Plugin policy" below. ## Writing style Applies to any text you write for this project: page copy, blog posts, code comments, commit messages and documentation. - Use [British] English spelling (e.g. "colour", "organise", "optimise", "centre"). - Do not use em dashes or en dashes as sentence punctuation. Rephrase with a comma, colon, semicolon, brackets or a new sentence. - Never leave placeholder or lorem ipsum text on a real page. - Write all titles and headings in **sentence case**: capitalise only the first word and proper nouns (e.g. WordPress, GitHub). This covers page and post titles, SEO titles, every `h1` to `h6`, block section titles and button labels. Good: "How we build WordPress websites". Bad: "How We Build WordPress Websites". ## Local development - The site runs locally in LocalWP. Never make untested changes on the live site. - Local runs its own MySQL on a per-site socket, so a bare `wp` command may fail with "Error establishing a database connection". Use Local's "Open Site Shell", or a `wp` wrapper that passes the socket path to PHP. - Take a backup (Local export or the backup plugin) before any update or bulk content change. ## Git workflow - The theme and the blocks plugin are separate Git repositories, pushed to GitHub. - **Commit after every change.** One small, focused commit per change, with a message that says what changed and why (e.g. "Reduce hero heading size on mobile"). - Stage only the files you changed. Never commit `build/` or `node_modules/`. - WordPress core, third-party plugins and uploads are not in Git; they are covered by backups. - Use a feature branch for anything risky, such as WordPress or plugin updates that need code changes. ## Plugin policy - Every plugin is extra code to update, secure and pay for. Do not install a new plugin without asking first. - Prefer a small custom block, a theme template or a shortcode in our own code over a general-purpose plugin. - No page builders. No slider, fonts or forms plugins: build these into the theme or blocks plugin. --- ## Theme (`[my-theme]`) ### Structure - `style.css`: all custom CSS, including the global CSS variables - `functions.php`: enqueues styles, includes files from `inc/`, registers filters - `theme.json`: layout widths and editor settings - `templates/`: page, single post, 404 and other templates - `parts/`: template parts (header, footer) - `patterns/`: block patterns - `inc/`: site-specific PHP (admin guides, shortcodes, form handlers) - `assets/fonts/`: self-hosted webfonts ### Overriding the parent theme - A file in the child theme's `parts/` or `templates/` takes precedence over the parent's. - If the Site Editor has a saved customisation, the database copy wins over the file. Clear it in Appearance > Editor > Template Parts > "Clear customisations" before expecting a file change to show. - To override a parent pattern, give the child pattern the same `Slug:`. ### Global CSS variables All colours, fonts and spacing are CSS custom properties on `:root` in `style.css`. **Always use the variables; never hardcode hex colours, font names or spacing values.** If a new value is needed, add a variable first, then reference it with `var()`. | Variable | Value | Purpose | |---|---|---| | `--clr-brand` | [#000000] | Buttons, accents, active borders | | `--clr-brand-hover` | [#000000] | Hover state for brand elements | | `--clr-text-primary` | [#000000] | Headings | | `--clr-text-body` | [#000000] | Body text | | `--clr-text-muted` | [#000000] | Captions, subtitles | | `--clr-surface` | [#ffffff] | Card and section backgrounds | | `--clr-surface-dark` | [#000000] | Dark section backgrounds | | `--clr-border` | [#000000] | Borders, dividers | | `--font-family` | ['Body Font', sans-serif] | Body text | | `--font-family-heading` | ['Heading Font', serif] | Headings | | `--content-max-width` | [1200px] | Maximum content width (match `theme.json` `wideSize`) | | `--gutter` | [1.5rem] | Horizontal page padding | | `--section-padding-md` | [8rem] | Standard section padding, desktop | | `--section-padding-md-mobile` | [6rem] | Standard section padding, mobile | Keep this table up to date whenever a variable is added, changed or removed. ### Accessibility - Target WCAG [AAA] contrast: 7:1 for normal text, 4.5:1 for large text. Check every new colour pairing against its background. - Exactly one `h1` per page, with a logical heading order below it. - Every meaningful image needs descriptive alt text. - Everything interactive must work with a keyboard and have a visible focus style. ### Buttons Every button on the site uses one canonical style, defined by `--btn-*` global CSS variables (background, text colour, radius, padding, font size, shadow, transition). Style every new button with these variables, including core block buttons such as the File block's download button (`.wp-block-file__button`), rather than leaving WordPress's default rounded buttons in place. ### Fonts Fonts are self-hosted in `assets/fonts/` and declared with `@font-face` in `style.css`. Do not load fonts from Google Fonts or any other CDN: it adds a third-party request and sends visitor data outside the site (a GDPR concern). --- ## Custom blocks plugin (`[my-custom-blocks]`) ### Structure - `src//`: source for each block (`block.json`, `index.js`, `edit.js`, `save.js`, `style.css`) - `src/components/`: shared React components; check here before writing a new UI pattern - `build//`: compiled output, generated by `npm run build` (never edit, never commit) - `webpack.config.js`: list of blocks to build - `[my-custom-blocks].php`: registers blocks; dynamic blocks need a `render_callback` here ### Build Whenever a `src/` file changes, run `npm run build` before committing. ### Portability: the plugin must work on any site This plugin is reused across projects. Keep it generic: - **No site-specific code.** Page IDs, menus, contact details, language setup and admin guides belong in the site's theme, not here. - **Generic English defaults.** `block.json` defaults are placeholders such as "Your Headline Here" and "hello@example.com", never a real phone number, email address or client copy. - **Branding comes from the theme.** The plugin ships neutral default global CSS variables. Each site's theme overrides them with a filter in `functions.php`: ```php add_filter( '[prefix]_design_tokens', function ( $tokens ) { return array_merge( $tokens, [ '--clr-brand' => '[#000000]', '--clr-brand-hover' => '[#000000]', ] ); } ); ``` ### Naming - Block folders are kebab-case English and describe the layout, not the content: `services-grid`, `full-width-banner`, `contact-section`. - The block `title` says what the block does ("Services Grid"), not what one site put in it. - **Renaming a block is a breaking change.** Content is stored in the database by block name, so existing pages will break. Do not rename a block that is in use. ### Editable content rule Every piece of visible text must be editable in the block editor without touching code. - Headings, labels, button text and body copy: inline `` in `edit.js` and `` in `save.js`. - The sidebar is for non-text settings only: images, toggles, colours, links. - Never hardcode a visible string in `edit.js`, `save.js` or a PHP `render_callback`. Every string needs an attribute in `block.json` with a sensible default. ### Adding a new block 1. Create `src//` with `block.json`, `index.js`, `edit.js`, `save.js`, `style.css`. 2. Add the block to `webpack.config.js`. 3. Register it in `[my-custom-blocks].php` (with a `render_callback` if `save.js` returns `null`). 4. Run `npm run build`. 5. Test it in the editor and on the front end, on desktop and mobile. 6. Commit. ### Changing a static block's markup: add a deprecation For a static block (`save.js` returns markup), WordPress re-runs the current `save()` on every saved instance and compares the result with what is in the database. Any change to the output makes existing instances show "This block contains unexpected or invalid content". Whenever you change `save.js` output, add the previous `save()` (and previous attributes, if they changed) to `src//deprecated.js` and pass `deprecated` to `registerBlockType`. Add a `migrate()` function if attribute values need reshaping. Do not fix old content by regex-editing the database instead. ### Block CSS - Styles live in `src//style.css` and use the global CSS variables. - Test at mobile, tablet and desktop widths. - Use the theme's section padding and margin utility classes for vertical spacing rather than hardcoding padding in each block. --- ## Editing page content with wp-cli Page and post content lives in the database, not in files. When editing `post_content` from a script: 1. **`wp_insert_post()` and `wp_update_post()` strip lone backslashes.** A `\u2014` escape in a block's JSON attributes becomes the literal text `u2014` on the page. Encode block attributes with `JSON_UNESCAPED_UNICODE` and pass content through `wp_slash()`. 2. **Never guess a static block's HTML.** Read the block's `save.js` and reproduce its markup exactly, or the block will fail validation. 3. **Verify after every save.** Render the post through `the_content` and check for PHP errors, block validation problems and stray `uXXXX` text before calling the edit done. 4. **Save new content as a draft** and let a human review it before publishing. ## Updating WordPress 1. Back up first. 2. Update in layers, checking the site after each one: third-party plugins, then the custom blocks plugin, then the parent theme, then WordPress core. 3. Read changelogs for deprecated hooks or Block API changes before updating. ## Before calling a task done - [ ] The change works on desktop and mobile - [ ] No hardcoded colours, fonts or spacing (global CSS variables only) - [ ] All visible text is editable in the block editor - [ ] `npm run build` has been run if `src/` changed - [ ] Deprecation added if a static block's markup changed - [ ] Spelling and punctuation follow the writing style rules - [ ] Titles and headings are in sentence case - [ ] Changes committed with a clear message