# WordPress Block Theme Review Checklist

This checklist covers all requirements for reviewing a WordPress block theme before merging.

---

## Required Templates

Check if the theme includes the following required templates:

| Template | File | Purpose |
|----------|------|---------|
| Archive | `archive.html` | Archive pages (categories, tags, dates, etc.) |
| Index | `index.html` | Fallback template |
| 404 | `404.html` | Page not found |
| Page | `page.html` | Static pages |
| Search | `search.html` | Search results |
| Single Post | `single.html` | Individual posts |

---

## Template Content Checks

### archive.html

In `archive.html` (or the pattern it references):

- [ ] Has archive title block (`wp:query-title` with `type: archive`)
- [ ] Has query loop block (`wp:query`)
- [ ] Query loop has `"inherit": true`
- [ ] Has no-results block (`wp:query-no-results`) with descriptive text
- [ ] Has pagination block (`wp:query-pagination`)

**Why `inherit: true`?** This ensures the query respects the archive context (category, tag, date, etc.) rather than using hardcoded query parameters.

### index.html

In `index.html` (or the pattern it references):

- [ ] Has query loop block (`wp:query`)
- [ ] Query loop has `"inherit": true`
- [ ] Has pagination block (`wp:query-pagination`)

**Note:** Archive title and `query-no-results` are NOT required for index.html (it's the fallback template).

### 404.html

In `404.html` (or the pattern it references):

- [ ] Has error message (heading and/or explanatory text)
- [ ] Has search field (`wp:search`)

### page*.html (All Page Templates)

In all page templates (`page.html`, `page-no-title.html`, `page-with-sidebar.html`, etc.) or the patterns they reference:

- [ ] Each has a content block (`wp:post-content`)
- [ ] (Bonus) Content blocks are locked (`"lock":{"move":false,"remove":true}`)

### search.html

In `search.html` (or the pattern it references):

- [ ] Has search title (`wp:query-title` with `type: search`)
- [ ] Has query loop block (`wp:query`)
- [ ] Query loop has `"inherit": true`
- [ ] Has no-results block (`wp:query-no-results`) with descriptive text
- [ ] Has pagination block (`wp:query-pagination`)

### single*.html (All Single Post Templates)

In all single post templates (`single.html`, `single-no-title.html`, etc.) or the patterns they reference:

- [ ] Each has a content block (`wp:post-content`)
- [ ] (Bonus) Content blocks are locked

### Semantic `<main>` Tag Check (All Templates)

For each template (and any patterns it references):

- [ ] Exactly one block uses `"tagName":"main"` (rendered as `<main>`)
- [ ] No duplicate `<main>` tags across the template and its patterns

**Why?** A document should only have one `<main>` landmark for accessibility. Multiple `<main>` elements confuse screen readers and assistive technologies.

**Search for:**
```
"tagName":"main"
```
or in rendered HTML:
```
<main
```

### Hardcoded Text in Templates

- [ ] No `.html` template files contain hardcoded text strings requiring internationalization
- [ ] All user-facing text should be in patterns (PHP files) where translation functions can be used

**Note:** Template `.html` files cannot use PHP i18n functions. Any text that needs translation must be in a pattern.

---

## theme.json Checks

### Typography

- [ ] `styles.blocks.core/heading.typography.fontFamily` is **not** set in `theme.json`

  Setting a font family on the heading block overrides the Global Styles panel — any font family change the user makes via **Appearance → Editor → Styles → Typography → Headings** will have no effect. If a heading font is needed, set it at the element level (`styles.elements.heading.typography.fontFamily`) instead, which the Global Styles panel can override.

  **Bad:**
  ```json
  {
    "styles": {
      "blocks": {
        "core/heading": {
          "typography": {
            "fontFamily": "var(--wp--preset--font-family--rubik)"
          }
        }
      }
    }
  }
  ```

  **Good:**
  ```json
  {
    "styles": {
      "elements": {
        "heading": {
          "typography": {
            "fontFamily": "var(--wp--preset--font-family--rubik)"
          }
        }
      }
    }
  }
  ```

- [ ] "Big" font sizes (generally over 20px) use the `fluid` option
  ```json
  {
    "fluid": {
      "max": "2.027rem",
      "min": "1.602rem"
    },
    "name": "2X-Large",
    "size": "32px",
    "slug": "custom-6"
  }
  ```

### Spacing

- [ ] If custom `spacingSizes` exist: `defaultSpacingSizes` is `false` (for version 3)
- [ ] `spacingSizes` over 20px use `clamp()` or `min()` for responsive sizing
  ```json
  {
    "name": "Large 64px~32px",
    "size": "clamp(32px, 4.651vw, 64px)",
    "slug": "60"
  }
  ```
- [ ] No unitless values in root-level `styles.spacing.padding`
  ```json
  // Good
  "padding": {
    "left": "var(--wp--preset--spacing--40)",
    "right": "var(--wp--preset--spacing--40)"
  }

  // Bad - unitless
  "padding": {
    "left": "0",
    "right": "0"
  }
  ```

### Custom Templates

- [ ] All templates listed in `customTemplates` exist as actual template files
- [ ] All custom template files are registered in `customTemplates`
- [ ] Custom template titles use proper capitalization

### Template Parts

- [ ] All template parts registered in `templateParts` section
- [ ] Titles use proper capitalization

---

## functions.php Checks

### Block Stylesheet Enqueuing

- [ ] All block stylesheets in `/assets/css/blocks/` are enqueued correctly
- [ ] Uses `wp_enqueue_block_style()` for each block CSS file

**Example:**
```php
wp_enqueue_block_style(
    'core/button',
    array(
        'handle' => 'theme-button',
        'src'    => get_template_directory_uri() . '/assets/css/blocks/button.css',
        'path'   => get_template_directory() . '/assets/css/blocks/button.css',
    )
);
```

---

## Asset Checks

### /assets/fonts/

- [ ] All fonts are registered in `theme.json` or `/styles/typography/*.json`
- [ ] File paths in JSON exactly match actual files (case-sensitive)

### /assets/images/

- [ ] All image files are actually used in the theme
- [ ] All image references use `get_template_directory_uri()` for dynamic paths
  ```php
  // Good
  <?php echo esc_url( get_template_directory_uri() ); ?>/assets/images/photo.jpg

  // Bad - hardcoded
  /wp-content/themes/theme-name/assets/images/photo.jpg
  ```

---

## Template Parts Checks (/parts/)

- [ ] All template parts are used in templates (search for the template part name)
- [ ] No hardcoded text strings or image paths requiring localization
- [ ] If the template part calls a pattern, check that pattern for hardcoded text
- [ ] All template parts registered in `theme.json`'s `templateParts` section
- [ ] Titles properly capitalized

---

## Pattern Checks (/patterns/)

### Internationalization

- [ ] All text strings, labels, and placeholders use translation functions:
  - `esc_html_e()` for escaped HTML output
  - `esc_attr_e()` for escaped attribute output
  - `esc_html__()` / `esc_attr__()` with `echo`
  - `sprintf()` with placeholders for dynamic content
- [ ] No trailing/leading whitespace inside translation strings (handle spacing in HTML/CSS instead)

**Example:**
```php
<?php esc_html_e('Search', 'theme-slug'); ?>

<?php
echo sprintf(
    esc_html__( '%1$sFollow our podcast%2$s and subscribe.', 'theme-slug' ),
    '<strong>',
    '</strong>'
);
?>
```

**i18n false-positive exclusions:** Block comment attributes cannot use PHP translation functions because they are inside HTML comments. These are rendered server-side by their respective blocks. Do NOT flag the following as i18n issues:
- `"label":"Email"` inside `<!-- wp:jetpack/label {"label":"Email"} /-->`
- `"ariaLabel":"Post navigation"` inside `<!-- wp:group {"ariaLabel":"Post navigation"} -->`
- Any other string attributes inside `<!-- wp:blockname {...} -->` comments

The corresponding HTML attributes (e.g., `aria-label="Post navigation"`) are generated server-side from the block comment and cannot be independently translated in the pattern file.

### Pattern Headers

- [ ] Template patterns (used by templates) have `Inserter: no` in the PHP header
  ```php
  /**
   * Title: archive
   * Slug: theme-slug/archive
   * Inserter: no
   */
  ```
- [ ] User-insertable patterns have appropriate `Categories` defined
  ```php
  /**
   * Title: Hero 1
   * Slug: theme-slug/hero-1
   * Categories: about, banner, featured
   */
  ```
- [ ] Category slugs are **all lowercase** — WordPress matching is case-sensitive. `Categories: Posts` will NOT match the core-registered `posts` category and the pattern will be invisible in the inserter.
- [ ] Category slugs are **registered** — either by WordPress core (e.g. `posts`, `portfolio`, `banner`, `featured`, `text`, `header`, `footer`, `buttons`, `about`, `testimonials`, `media`) or by the theme via `register_block_pattern_category()`. An unregistered slug causes the pattern and its category group to vanish from the inserter entirely.
- [ ] Patterns containing `wp:query` have `Block Types: core/query` — without this header, query patterns do not appear in the Query Loop variation picker (the layout chooser shown when inserting a Query Loop block) and are also hidden from the general inserter.
- [ ] User-insertable patterns consistently include `Viewport Width` in their headers
  ```php
  /**
   * Title: Hero 1
   * Slug: theme-slug/hero-1
   * Categories: about, banner, featured
   * Viewport Width: 1440
   */
  ```
  For query patterns, the full recommended header is:
  ```php
  /**
   * Title: Query Cards
   * Slug: theme-slug/query-cards
   * Categories: posts
   * Block Types: core/query
   * Viewport Width: 1440
   */
  ```

**Note on theme reactivation:** WordPress caches pattern registration. After fixing pattern headers, changes will not appear in the inserter until the theme is deactivated and reactivated (switch to another theme and back). Clearing the object cache alone is not sufficient.

---

## Navigation Blocks Check

- [ ] No navigation blocks have `"ref"` attribute (site-specific menu IDs)

**Search for:**
```
wp:navigation.*"ref"
```

**Bad:**
```html
<!-- wp:navigation {"ref":77} /-->
```

**Good:**
```html
<!-- wp:navigation {"style":{"spacing":{"blockGap":"var:preset|spacing|30"}}} /-->
```

---

## Style Variations Check (/styles/)

### Color/Style Variations

- [ ] Identify if theme has color/style variations (`/styles/*.json` at root level)
- [ ] If variations exist, flag for manual contrast testing on test site

### Block Variations (/styles/blocks/)

- [ ] Proper JSON structure with required fields: `version`, `title`, `slug`, `blockTypes`, `styles`

### Typography Variations (/styles/typography/)

- [ ] Reference existing font files with exact case-sensitive filenames

---

## File Metadata Checks

### screenshot.png or screenshot.jpg

- [ ] Dimensions: exactly 1200 x 900 pixels
- [ ] File size: warn if over 1MB

### readme.txt

- [ ] Contributors is `Automattic`
- [ ] Tested up to is the latest WordPress version
- [ ] Requires PHP is `7.2` or higher
- [ ] Theme description is present
- [ ] Changelog is present
- [ ] Copyright section includes:
  - Proper GPL license text
  - Current year
- [ ] All fonts have proper credits:
  - Copyright
  - License (usually OFL 1.1)
  - Source URL
- [ ] All images have proper credits:
  - Source URL
  - License
  - Author (if applicable)

### style.css

- [ ] Version uses semantic versioning (`X.X.X` format)
- [ ] Author is `Automattic`
- [ ] Description is present and matches readme.txt
- [ ] Tested up to matches readme.txt version
- [ ] Requires PHP is `7.2` or higher
- [ ] All Tags are from the allowed list — fetch the allowed tags from `https://api.wordpress.org/themes/info/1.1/?action=feature_list` using `WebFetch`, flatten all arrays (Layout, Features, Subject) into a single set, and compare each tag in style.css against it. Report any invalid tags as issues. This MUST be automated, not deferred to manual review.

---

## Block Markup Validation

Validate that Gutenberg block comments match the actual HTML markup. Mismatches cause "Block contains unexpected or invalid content" errors in the editor — a common result of hand-editing pattern or template part files.

### Block Nesting

- [ ] Every opening block comment (`<!-- wp:blockname -->`) has a matching closing comment (`<!-- /wp:blockname -->`)
- [ ] Self-closing blocks are properly formed (`<!-- wp:blockname /-->`)
- [ ] No orphaned or extra opening/closing comments

### JSON Validity

- [ ] JSON attributes in block comments are valid and parseable
  ```html
  <!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
  ```

### Attribute-to-Markup Consistency

For each non-self-closing block, verify the JSON attributes match the HTML element that follows:

- [ ] `"align"` → element has `align{value}` class (e.g., `alignfull`, `alignwide`)
- [ ] `"textAlign"` → element has `has-text-align-{value}` class
- [ ] `"fontSize"` → element has `has-{value}-font-size` class
- [ ] `"backgroundColor"` → element has `has-{value}-background-color` class
- [ ] `"textColor"` → element has `has-{value}-color` class
- [ ] `"className"` → each class appears in the element's `class` attribute
- [ ] `"dropCap": true` → element has `has-drop-cap` class
- [ ] `"tagName"` → element uses the specified tag (e.g., `"tagName":"main"` → `<main`)

**Example mismatch:**
```html
<!-- wp:paragraph {"align":"center"} -->
<p>Text</p>  ← Missing has-text-align-center class
<!-- /wp:paragraph -->
```

### Tag Name Validation

Verify that the HTML element matches the block type:

| Block | Expected Element |
|-------|-----------------|
| `wp:paragraph` | `<p>` |
| `wp:heading` | `<h1>` through `<h6>` |
| `wp:image` | `<figure>` with `wp-block-image` class |
| `wp:list` | `<ul>` or `<ol>` |
| `wp:quote` | `<blockquote>` |
| `wp:group` | `<div>` (or tag from `tagName` attribute) |

**Note:** This catches common hand-editing mistakes but does not replicate Gutenberg's full block schema validation.

---

## Housekeeping Checks

- [ ] No OS metadata files (`.DS_Store`, `Thumbs.db`, `desktop.ini`) present in the theme
- [ ] No editor/AI tool directories present in the theme: `.idea/`, `.vscode/`, `.claude/`, `.cursor/`, `.windsurf/`

**Why?** These files should be in `.gitignore` and never committed to the repository.

---

## Referenced CSS Files Check

- [ ] `editor-style.css` referenced in `functions.php` exists at the expected path
- [ ] `editor-ui.css` referenced in `functions.php` exists at the expected path (if used)
- [ ] Any other CSS files referenced in `functions.php` exist at their expected paths

**Why?** Missing CSS files cause silent failures — no error is thrown but styles don't load.

---

## Image Alt Text Check

- [ ] Review images with empty `alt=""` attributes in patterns to confirm they are intentionally decorative
- [ ] Images that convey meaning should have descriptive alt text

**Note:** Empty alt text is valid for decorative images (CSS backgrounds, separator images, etc.) but should be verified manually. Flag for review rather than marking as an error.

**Note:** Any tag not found in the allowed list must be flagged as an issue (❌), not a warning. Known invalid tags include `block-themes` (should not be used) and `site-editor` (not a valid tag).

---

## Global Checks (All Files)

### fontSize Values

- [ ] All `fontSize` values use theme presets (not hardcoded px, rem, or em values)

**Good:**
```json
"fontSize": "var(--wp--preset--font-size--custom-2)"
```

**Bad:**
```json
"fontSize": "18px"
```

### URLs

- [ ] No links point to local, staging, or development URLs
- [ ] Search for: `localhost`, `staging.`, `.local`, `127.0.0.1`, `dev.`, `test.`

### Unicode Line Terminators

- [ ] No U+2028 (Line Separator) or U+2029 (Paragraph Separator) characters in `.php` or `.html` files

These invisible characters sneak in from copy-pasting out of browsers or rich text editors. In PHP, a `//` comment ending with U+2028 won't terminate the comment, silently commenting out the next line.

**How to check:**
```bash
python3 -c "
import os, sys
found = False
for root, dirs, files in os.walk('.'):
    dirs[:] = [d for d in dirs if d != '.git']
    for f in files:
        if not f.endswith(('.php', '.html')): continue
        path = os.path.join(root, f)
        data = open(path, 'rb').read()
        for i in range(len(data)-2):
            if data[i]==0xE2 and data[i+1]==0x80 and data[i+2] in (0xA8,0xA9):
                char = 'U+2028' if data[i+2]==0xA8 else 'U+2029'
                line = data[:i].count(b'\n') + 1
                print(f'{path}:{line}: {char}')
                found = True
if not found: print('OK')
"
```

---

## Additional Templates Check

- [ ] Identify any templates beyond the standard set (archive, index, 404, page*, search, single*)
- [ ] Flag templates like `home.html`, `front-page.html`, or custom templates (e.g., `category-*.html`) for manual review

---

## Manual Review Items

These items cannot be fully automated and require human verification:

- [ ] Color variations contrast testing (if variations exist)
- [ ] Visual review of theme on test site
- [ ] Custom template functionality
- [ ] Accessibility considerations
- [ ] Mobile responsiveness
- [ ] RTL language support (if tagged)
