From website-creator
Guides WordPress block theme development: theme.json styles/settings, templates/parts, patterns, style variations, Site Editor troubleshooting, WCAG contrast checks.
npx claudepluginhub burtrw/website-creator --plugin website-creatorThis skill uses the workspace's default tool permissions.
**Run this check on every color palette BEFORE creating mockups or themes:**
Generates WordPress Full Site Editing block themes: theme.json configs, block templates, template parts, patterns, functions.php, and styles.
Generates custom WordPress FSE block themes from HTML/CSS exports or screenshots by extracting design tokens, mapping to Gutenberg blocks, bundling Google Fonts, and producing installable custom-{slug} themes.
Guides WordPress theme development workflow: setup, template hierarchy, custom post types, block editor support, responsive design, and WP 7.0 features like Pattern Editing and Navigation Overlays.
Share bugs, ideas, or general feedback.
Run this check on every color palette BEFORE creating mockups or themes:
def check_contrast(hex1, hex2):
"""Returns contrast ratio between two hex colors."""
def hex_to_rgb(h):
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def luminance(rgb):
r, g, b = [x/255 for x in rgb]
r = r/12.92 if r <= 0.03928 else ((r+0.055)/1.055)**2.4
g = g/12.92 if g <= 0.03928 else ((g+0.055)/1.055)**2.4
b = b/12.92 if b <= 0.03928 else ((b+0.055)/1.055)**2.4
return 0.2126*r + 0.7152*g + 0.0722*b
l1, l2 = luminance(hex_to_rgb(hex1)), luminance(hex_to_rgb(hex2))
lighter, darker = max(l1, l2), min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
# CHECK ALL TEXT/BACKGROUND COMBINATIONS
palette = {
'bg': '#FFFFFF',
'text': '#1a1a1a',
'muted': '#666666',
'primary': '#BF5700',
'primary_bg': '#2D5016', # Example dark section
}
pairs_to_check = [
('text', 'bg', 4.5), # Body text on background
('muted', 'bg', 4.5), # Muted text on background
('primary', 'bg', 3.0), # Accent (if used as large text)
('bg', 'primary_bg', 4.5), # White text on dark sections
]
print("CONTRAST CHECK RESULTS:")
for fg, bg, min_ratio in pairs_to_check:
ratio = check_contrast(palette[fg], palette[bg])
status = "PASS" if ratio >= min_ratio else "FAIL"
print(f" {fg} on {bg}: {ratio:.1f}:1 (need {min_ratio}:1) [{status}]")
Requirements:
Common failures:
Never use emojis anywhere in generated content — not in headings, paragraphs, button text, or any other text.
Always use CSS text-wrap:
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
p { text-wrap: pretty; }
Templates must only use dynamic blocks that pull content from the database. Static content belongs in patterns that users insert manually.
<!-- GOOD: Template displays database content -->
<!-- wp:post-title /-->
<!-- wp:post-content /-->
<!-- wp:post-featured-image /-->
<!-- wp:query {"queryId":1} -->...<!-- /wp:query -->
<!-- BAD: Hardcoded content in template (causes duplicates on import) -->
<!-- wp:heading -->
<h2>Welcome to Our Agency</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>We build amazing websites...</p>
<!-- /wp:paragraph -->
Why this matters:
Where content goes:
| Type | Location | Purpose |
|---|---|---|
| Templates | templates/ | Structure only — header, footer, content areas |
| Patterns | patterns/ | Reusable blocks users INSERT into pages |
| Actual content | Database | Posts, pages, created by user or imported |
Hero and prominent images MUST incorporate the site's color palette. No random stock photos.
Image sourcing (in order of preference):
<!-- wp:cover {"overlayColor":"primary","dimRatio":40} -->
The goal: Images feel designed for this site, not stock.
IMPORTANT: Before starting any theme work, gather this information:
Use this structure when planning themes:
{
"siteBrief": {
"siteName": "Name of site/business",
"siteType": "SaaS | portfolio | blog | restaurant | e-commerce",
"primaryGoal": "Main conversion goal",
"audience": "Target audience description",
"tone": "Voice and feel",
"brandKeywords": "Aesthetic descriptors"
},
"layoutNotes": [
"Hero with value prop and CTA",
"Feature grid or comparison",
"Social proof (testimonials, logos)",
"Final CTA"
],
"typography": {
"primaryFont": "Heading font",
"secondaryFont": "Body font",
"fontImport": "Google Fonts URL"
}
}
Present specs as a table for user confirmation:
| Field | Value |
|---|---|
| Site Name | [name] |
| Site Type | [type] |
| Primary Goal | [goal] |
| Tone | [tone] |
| Key Sections | [list] |
Generic designs fail because they try to be everything. Commit fully to a direction:
| Direction | Characteristics |
|---|---|
| Brutally minimal | Maximum whitespace, single accent color, stark typography |
| Maximalist | Dense information, layered elements, controlled complexity |
| Retro-futuristic | Vintage meets tech, neon on dark, geometric shapes |
| Organic/natural | Soft curves, earthy tones, textured backgrounds |
| Luxury/refined | Rich colors, sophisticated serif typography, premium feel |
| Playful | Bold colors, rounded shapes, unexpected interactions |
| Editorial/magazine | Grid-based, strong typographic hierarchy, clean sections |
| Brutalist/raw | Unconventional choices, exposed structure, monospace fonts |
AVOID (overused/generic):
PREFER (distinctive choices):
| Site Type | Heading Font | Body Font |
|---|---|---|
| SaaS/Tech | Satoshi, Plus Jakarta Sans, Outfit | Inter, Source Sans Pro |
| Law/Finance | Cormorant Garamond, DM Serif Display | Source Sans Pro |
| Restaurant | Playfair Display, Lora | Josefin Sans, Lato |
| Creative/Portfolio | Clash Display, Fraunces, Syne | Inter, Work Sans |
| Blog/Media | Outfit, Merriweather | Source Serif Pro |
| E-commerce (luxury) | Cormorant, Playfair | Lato, Open Sans |
| E-commerce (modern) | Outfit, Satoshi | Inter |
When a user describes their site, infer the typical sections and structure:
Use animations for delight and micro-interactions:
/* Staggered fade-in */
.fade-in {
opacity: 0;
transform: translateY(20px);
animation: fadeIn 0.6s ease forwards;
}
.fade-in:nth-child(1) { animation-delay: 0.1s; }
.fade-in:nth-child(2) { animation-delay: 0.2s; }
.fade-in:nth-child(3) { animation-delay: 0.3s; }
@keyframes fadeIn {
to { opacity: 1; transform: translateY(0); }
}
/* Smooth hover transitions */
.interactive {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.interactive:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
}
theme.json filesstyle.css (with theme header)theme.jsontemplates/ directory with at least index.htmlBlock theme indicators:
theme.json presenttemplates/ and/or parts/ directories presentStyle hierarchy to remember: Core defaults → theme.json → child theme → user customizations
User customizations can make theme.json edits appear "ignored" - check the database for overrides.
Decide whether changing:
Key theme.json sections:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": { "palette": [...] },
"typography": { "fontSizes": [...] },
"layout": { "contentSize": "800px", "wideSize": "1200px" }
},
"styles": {
"color": { "background": "...", "text": "..." },
"typography": { "fontSize": "...", "fontFamily": "..." },
"elements": { "link": {...}, "button": {...} },
"blocks": { "core/paragraph": {...} }
}
}
templates/ as HTML files (e.g., index.html, single.html, page.html)parts/ without nested subdirectories (e.g., header.html, footer.html)<!-- wp:template-part {"slug":"header"} /-->Required templates for all themes:
page.html - Page with titlepage-no-title.html - Page without title (for landing pages)single.html - Blog post template with: Categories, Date, Title, Featured Image, Commentsindex.html - Blog archive/fallbackfront-page.html - Homepage (if applicable)404.html - Error pageSingle post template should include (in order):
<!-- wp:post-terms {"term":"category"} /-->
<!-- wp:post-date /-->
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-featured-image {"aspectRatio":"16/9"} /-->
<!-- wp:post-content /-->
<!-- wp:comments -->
<!-- Full comments section with comment-template, pagination, post-comments-form -->
<!-- /wp:comments -->
IMPORTANT: Site Title block should NOT be an H1
<!-- wp:site-title {"level":0} /-->"level":0 attribute renders as a <p> tag instead of a headingHeader template part example:
<!-- wp:group {"tagName":"header"} -->
<header class="wp-block-group">
<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between"}} -->
<div class="wp-block-group">
<!-- wp:site-title {"level":0} /-->
<!-- wp:navigation /-->
</div>
<!-- /wp:group -->
</header>
<!-- /wp:group -->
Prefer filesystem patterns under patterns/ for theme-owned patterns.
Pattern file header example:
<?php
/**
* Title: Hero Section
* Slug: theme-name/hero
* Categories: featured
*/
?>
<!-- wp:cover {"...} -->
Style variations are JSON files under styles/ directory.
Note: Once users select a variation, it's stored in the database, so file changes may not auto-update their view.
Example: styles/dark.json
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Dark",
"styles": {
"color": {
"background": "#1a1a1a",
"text": "#ffffff"
}
}
}
CRITICAL: Before sharing a theme preview with users, ALWAYS compare the live theme against the original mockups/designs.
When building from mockups, verify these CSS features are implemented:
Background effects
Hover animations
Decorative elements
Transitions and animations
CRITICAL: After ANY change to theme files, you MUST:
Use this single command after every change:
cd /path/to/project && rm -f theme.zip && cd theme && zip -r ../theme.zip . && cd .. && python3 << 'EOF'
import base64
import json
with open('theme.zip', 'rb') as f:
theme_data = base64.b64encode(f.read()).decode('utf-8')
# Load existing blueprint or create new one
# ... update with new theme_data ...
# Write blueprint.json and open-in-playground.html
EOF
Never share a playground link without rebuilding first!
wp_global_styles post type in databaseparts/, not nestedBlock themes do NOT automatically load style.css. You MUST enqueue it:
function theme_enqueue_styles() {
wp_enqueue_style(
'theme-style',
get_stylesheet_uri(),
array(),
wp_get_theme()->get( 'Version' )
);
}
add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );
add_action( 'enqueue_block_assets', 'theme_enqueue_styles' );
Without this, all custom CSS in style.css will be ignored!
When creating WordPress Playground blueprints for theme previews:
CRITICAL: Theme files must be at the ROOT of the zip, not nested in a folder.
# WRONG - creates theme/style.css in zip
zip -r theme.zip theme/
# CORRECT - creates style.css at root
cd theme && zip -r ../theme.zip .
If you get "Stylesheet is missing" error, the zip structure is wrong.
IMPORTANT: The Write tool times out with large strings (~20KB+). Always use Python to write files containing base64-encoded theme data.
Use this Python script to build the entire preview package:
python3 << 'PYTHON_EOF'
import base64
import json
import os
THEME_NAME = "theme-name" # Change this
PROJECT_DIR = "/path/to/project" # Change this
os.chdir(PROJECT_DIR)
# 1. Base64 encode the theme.zip
with open('theme.zip', 'rb') as f:
theme_data = base64.b64encode(f.read()).decode('utf-8')
# 2. Build the blueprint with inline data URL
blueprint = {
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"landingPage": "/",
"preferredVersions": {
"php": "8.0",
"wp": "latest"
},
"steps": [
{
"step": "writeFile",
"path": f"/wordpress/wp-content/themes/{THEME_NAME}.zip",
"data": {
"resource": "url",
"url": f"data:application/zip;base64,{theme_data}"
}
},
{
"step": "unzip",
"zipPath": f"/wordpress/wp-content/themes/{THEME_NAME}.zip",
"extractToPath": f"/wordpress/wp-content/themes/{THEME_NAME}"
},
{
"step": "activateTheme",
"themeFolderName": THEME_NAME
}
]
}
# 3. Save blueprint.json
with open('blueprint.json', 'w') as f:
json.dump(blueprint, f, indent=2)
# 4. Encode for URL - CRITICAL: use compact JSON, minimal escaping
blueprint_compact = json.dumps(blueprint, separators=(',', ':'))
encoded_blueprint = blueprint_compact.replace(' ', '%20').replace('"', '%22')
playground_url = f"https://playground.wordpress.net/#{encoded_blueprint}"
# 5. Generate open-in-playground.html with theme styling
html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{THEME_NAME} Theme Preview</title>
<style>
body {{ font-family: system-ui, sans-serif; max-width: 800px; margin: 4rem auto; padding: 2rem; }}
h1 {{ margin-bottom: 0.5rem; }}
.btn {{ display: inline-block; padding: 1rem 2rem; background: #007cba; color: white; text-decoration: none; border-radius: 4px; margin: 0.5rem 0.5rem 0.5rem 0; }}
.btn:hover {{ background: #005a87; }}
.btn.secondary {{ background: #50575e; }}
</style>
</head>
<body>
<h1>{THEME_NAME}</h1>
<p>WordPress Block Theme</p>
<h2>Preview</h2>
<a href="{playground_url}" class="btn" target="_blank">Open in WordPress Playground</a>
<h2>Download</h2>
<a href="data:application/zip;base64,{theme_data}" download="{THEME_NAME}.zip" class="btn secondary">Download theme.zip</a>
</body>
</html>'''
with open('open-in-playground.html', 'w') as f:
f.write(html)
print(f"Created: blueprint.json, open-in-playground.html")
print(f"Theme data size: {len(theme_data)} chars")
PYTHON_EOF
URL Encoding Rules:
json.dumps(blueprint, separators=(',', ':')).replace(' ', '%20').replace('"', '%22')urllib.parse.quote() — it over-encodes and breaks PlaygroundWorkflow:
open-in-playground.html with the blueprint embedded in the URL hashcomputer:// link to open the HTML fileIMPORTANT: The computer:// protocol only works for files, not directories.
Example response to user:
[Open Playground Preview](computer:///path/to/open-in-playground.html)
WordPress themes must have a screenshot.png (1200x900 recommended) for display in the theme selector.
Use Pillow to generate a screenshot that represents the design.
When sourcing/generating images, match the subject matter to the site:
See Absolute Rules > IMAGE COLOR COHESION for sourcing workflow.
# Check active theme
wp theme list --status=active
# Export theme with customizations
wp theme export
# Clear transients/cache
wp transient delete --all