- Overview
- Purpose & Design Philosophy
- Installation
- Features
- Killer Pads Users
WACK Foundation is a battle-tested parent theme designed specifically for headless WordPress deployments. It provides a curated set of features that disable unnecessary functionality by default while offering granular control through WordPress filters to re-enable features as needed.
By using this as a parent theme, you gain:
- Security hardening out of the box (XML-RPC disabled, REST API access control, etc.)
- Block editor optimization for headless workflows (reduced block types, disabled Quick Edit, etc.)
- Filter-based extensibility for all features
- Zero configuration required for sensible defaults
This theme is built for headless WordPress setups where:
- Content is managed in WordPress but rendered on a separate frontend
- The block editor (Gutenberg) is used for structured content authoring
- Security and performance are prioritized over convenience features
- Child themes extend functionality through filters rather than overriding templates
- Deny by default, allow by exception: Features are disabled unless explicitly enabled
- Filter-driven configuration: All behavior is customizable via WordPress filters
- Minimal footprint: No bloat, no legacy compatibility, no frontend assets
- Developer-friendly: Clear APIs, comprehensive documentation, predictable behavior
This theme is available only through Composer.
Installation steps:
- Require the theme package:
composer require kodansha/wack-foundation-
The theme will be installed to
web/app/themes/wack-foundation(when using Bedrock) orwp-content/themes/wack-foundation(standard WordPress). -
Activate the theme or use it as a parent theme for your child theme.
To use WACK Foundation as a parent theme, specify it in your child theme's style.css:
/*
Theme Name: Your Child Theme Name
Template: wack-foundation
Version: 1.0.0
*/The Template field must match the directory name of the WACK Foundation theme.
Automatically sets custom favicons for the WordPress admin dashboard and login page by detecting favicon files in your theme's root directory.
Supported files (priority order):
favicon.ico- ICO format (recommended for compatibility)favicon.png- PNG format (modern browsers)favicon.svg- SVG format (scalable vector graphics)
How it works:
- Place a favicon file (
favicon.ico,favicon.png, orfavicon.svg) in your theme's root directory - The class automatically detects and outputs the appropriate
<link>tag - MIME types are auto-detected from file extension
- Child themes inherit and can override parent theme favicons
No configuration needed - just add the file to your theme directory.
Completely disables comments and trackbacks functionality for headless WordPress.
What it disables:
- Comment support for all public post types
- Comment-related admin UI (menus, dashboard modules, profile shortcuts)
- REST API comment endpoints (
/wp/v2/comments, etc.) - XML-RPC comment methods (
wp.newComment,wp.editComment, etc.) - Comment widgets and admin bar items
What it preserves:
- Logged-in users with
edit_postscapability retain full access (for Gutenberg) - Frontend template rendering (this is admin/API-only)
No filters available - comments are always disabled by this theme. This feature cannot be turned off.
Disables the WordPress admin dashboard (index.php) and redirects users to a more useful admin page.
Default behavior:
- Redirects all users to the posts list (
edit.php) - Removes the "Dashboard" menu item from the admin sidebar
Filters:
Customize the redirect destination URL.
<?php
// Redirect to pages list instead of posts
add_filter('wack_dashboard_redirect_url', fn() => 'edit.php?post_type=page');
// Redirect to a custom page
add_filter('wack_dashboard_redirect_url', fn() => 'admin.php?page=my-custom-page');Parameters:
- (none) - Returns a string URL path relative to
admin_url()
Default: 'edit.php' (posts list)
Allow specific user capabilities to access the dashboard.
<?php
// Allow administrators to see the dashboard
add_filter('wack_dashboard_allowed_capabilities', function($capabilities) {
$capabilities[] = 'manage_options';
return $capabilities;
});
// Allow editors and admins
add_filter('wack_dashboard_allowed_capabilities', function($capabilities) {
$capabilities[] = 'manage_options';
$capabilities[] = 'edit_others_posts';
return $capabilities;
});Parameters:
array $capabilities- Array of capability strings
Default: [] (no users can access dashboard)
Controls which Gutenberg blocks are available in the block editor. Uses a minimal whitelist approach.
Default allowed blocks:
core/headingcore/imagecore/listcore/list-itemcore/paragraph
All other blocks (including media, embeds, widgets, etc.) are disabled by default.
Filter:
Extend the list of allowed block types.
<?php
// Add additional core blocks
add_filter('wack_block_type_enabled_types', fn($default_blocks) => array_merge($default_blocks, [
'core/table',
'core/video',
'core/gallery',
'core/quote',
'core/code',
]));
// Replace entirely (not recommended)
add_filter('wack_block_type_enabled_types', fn() => [
'core/paragraph',
'core/heading',
'my-custom/block',
]);Parameters:
array $default_blocks- Array of default block type names
Default: BlockType::DEFAULT_ALLOWED_BLOCK_TYPES
Block type reference: Primary (PHP – suitable for CLI, diagnostics, CI):
$registry = \WP_Block_Type_Registry::get_instance();
$all_blocks = $registry->get_all_registered(); // array keyed by block name
foreach ($all_blocks as $name => $block) {
// $name example: 'core/paragraph'
// Access meta: $block->title, $block->category, $block->supports
}Secondary (browser console quick lookup): wp.blocks.getBlockTypes()
Reference docs: https://developer.wordpress.org/block-editor/reference-guides/core-blocks/
Controls which block styles are available for core and custom blocks. Disables non-default styles by default.
Default behavior:
- Only "default" block styles are enabled
- Non-default styles (like "outline" button, "fill" quote, etc.) are disabled
Filter:
Specify which block styles should be available.
<?php
// Enable specific non-default styles
add_filter('wack_block_style_enabled_styles', fn($styles) => [
'core/button:outline',
'core/quote:fancy-quote',
'core/separator:wide',
'core/separator:dots',
]);Parameters:
array $styles- Array of block style identifiers in'blockName:styleName'format
Default: [] (all non-default styles disabled, only default styles available)
Block style reference:
Default styles (like core/button:fill, core/image:default) are always available and cannot be disabled.
Primary (PHP – enumerating styles):
$registry = \WP_Block_Type_Registry::get_instance();
$styles = [];
foreach ($registry->get_all_registered() as $block) {
if (!empty($block->styles)) {
$styles[$block->name] = $block->styles; // each style array has keys like 'name', 'label', 'isDefault'
}
}
// $styles maps block name => array of style meta arraysSecondary (browser console quick lookup):
wp.blocks.getBlockTypes().filter(b => b.styles?.length).map(b => ({block: b.name, styles: b.styles}))
Controls which text formatting options are available in the Rich Text toolbar (bold, italic, link, etc.).
Default behavior:
- All text formats are disabled by default (empty array)
- Use the filter to enable specific formats
Filter:
Specify which text formats should be enabled.
<?php
// Enable basic formatting
add_filter('wack_text_format_enabled_types', fn() => [
'core/bold',
'core/italic',
'core/link',
]);
// Enable extended formatting
add_filter('wack_text_format_enabled_types', fn() => [
'core/bold',
'core/code',
'core/italic',
'core/link',
'core/strikethrough',
'core/underline',
]);
// Enable all available formats
add_filter('wack_text_format_enabled_types', fn() => [
'core/bold',
'core/code',
'core/italic',
'core/link',
'core/strikethrough',
'core/subscript',
'core/superscript',
'core/text-color',
'core/underline',
]);Parameters:
array $formats- Array of format type identifiers
Default: [] (all formats disabled)
Format type reference:
- Full list: https://github.com/WordPress/gutenberg/tree/trunk/packages/format-library
- Runtime inspection: Run
wp.data.select('core/rich-text').getFormatTypes()in browser console
Controls which embed block variations (YouTube, Twitter, Vimeo, etc.) are available in the block editor.
Default behavior:
- All embed variations are disabled by default (empty array)
Filter:
Specify which embed providers should be available.
<?php
// Enable YouTube and Vimeo embeds only
add_filter('wack_embed_block_enabled_variations', fn() => [
'youtube',
'vimeo',
]);
// Enable social media embeds
add_filter('wack_embed_block_enabled_variations', fn() => [
'twitter',
'facebook',
'instagram',
]);Parameters:
array $variations- Array of embed variation slugs
Default: [] (all embeds disabled)
Embed variation reference:
- Common providers:
youtube,vimeo,twitter,facebook,instagram,spotify, etc. - Full list available in WordPress core source
Hides the content editor (title and content fields) for specific post types where structured content is managed entirely through ACF or custom fields.
Default behavior:
- No post types have the editor disabled (empty array)
Why not remove 'editor' support?
While you can disable the content editor by removing 'editor' from a post type's supports array, this prevents the Gutenberg UI from loading entirely. This class allows you to hide the content editor while maintaining the block editor interface, ensuring a consistent UI across all post types.
Filter:
Specify which post types should have their content editor hidden.
<?php
// Hide content editor for 'author' and 'product' post types
add_filter('wack_content_editor_disabled_post_types', fn() => [
'author',
'product',
]);
// Hide for a single post type
add_filter('wack_content_editor_disabled_post_types', fn() => ['landing-page']);Parameters:
array $post_types- Array of post type slugs
Default: [] (no post types affected)
How it works:
- Loads a CSS file that hides the editor interface
- Only loads on the specified post types
- Content is still stored in the database; only the UI is hidden
- Preserves the Gutenberg block editor UI for consistency
Disables the "Quick Edit" inline editing functionality in WordPress admin post lists.
Default behavior:
- Quick Edit is disabled for all post types by default
Filter:
Specify which post types should have Quick Edit enabled.
<?php
// Enable Quick Edit for posts and pages
add_filter('wack_quick_edit_enabled_post_types', fn() => [
'post',
'page',
]);
// Enable for custom post type
add_filter('wack_quick_edit_enabled_post_types', fn($types) => array_merge(
$types,
['product', 'event']
));Parameters:
array $post_types- Array of post type slugs where Quick Edit should be enabled
Default: [] (Quick Edit disabled for all post types)
Why disable Quick Edit?
- Prevents inconsistent data when using structured content (ACF, custom fields)
- Forces editors to use the full editor where validation rules apply
- Reduces accidental edits in headless workflows
Controls which WordPress image sizes are automatically generated when images are uploaded. Uses a whitelist approach.
Default behavior:
- All WordPress default image sizes are disabled (thumbnail, medium, large, etc.)
- All auto-generated sizes are disabled
- Big image auto-resize threshold is disabled
Filter:
Define custom image sizes to generate.
<?php
// Define custom image sizes
add_filter('wack_image_size_control_custom_sizes', fn() => [
// Format: 'size-name' => [width, height, crop]
// All parameters are optional except width
// Fixed size with crop
'card-thumbnail' => [400, 300, true],
// Fixed size without crop (default)
'hero-banner' => [1200, 600],
// Width only (height auto-calculated)
'content-width' => [800],
// Width and height, no crop
'gallery-large' => [1024, 768, false],
// Width only with soft crop
'post-thumbnail' => [600, 0, false],
]);Parameters:
array $sizes- Associative array mapping size names to[width, height, crop]arrayswidth(int, required): Image width in pixelsheight(int, optional): Image height in pixels (0 = auto)crop(bool, optional): Whether to crop to exact dimensions (default:false)
Default: [] (no sizes generated)
Size format:
- Array values:
[width],[width, height], or[width, height, crop] - Crop can be
true(center crop) orfalse(proportional resize) - Height of
0means auto-calculate based on aspect ratio
What's disabled:
- WordPress default sizes:
thumbnail,medium,medium_large,large - Theme-defined sizes via
add_image_size() - Big image threshold (no auto-resize of large originals)
Automatically generates clean, unique filenames for uploaded media files using UUIDv7.
Default behavior:
- Original filenames are replaced with UUIDv7 identifiers
- File extensions are preserved
- Filenames are URL-safe and collision-resistant
Example:
my photo (1).jpg→01933b6e-8f12-7890-abcd-ef1234567890.jpgスクリーンショット 2024.png→01933b6e-9a3c-7def-1234-567890abcdef.png
Filter:
Provide a custom filename generation function.
<?php
// Use timestamp with random string
add_filter('media_filename_generator', fn($default, $original, $ext) =>
date('Y-m-d-His') . '-' . wp_generate_password(8, false), 10, 3);
// Use sanitized original filename (not recommended for security)
add_filter('media_filename_generator', fn($default, $original, $ext) =>
sanitize_file_name($original), 10, 3);
// Custom format with prefix
add_filter('media_filename_generator', fn($default, $original, $ext) =>
'media-' . uniqid() . '-' . time(), 10, 3);Parameters:
string $default_filename- The default UUIDv7-based filename (without extension)string $original_filename- The original uploaded filename (without extension)string $extension- The file extension (e.g.,'jpg','png')
Returns: string - New filename (without extension)
Default: UUIDv7 identifier
Why UUIDv7?
- Time-ordered for better database indexing
- Globally unique (collision-resistant)
- URL-safe characters only
- No personal information leakage
- Consistent length and format
Controls access to WordPress REST API endpoints using namespace whitelisting and route blacklisting.
Default behavior:
- All REST API access is denied by default (empty whitelist)
- Logged-in users with
edit_postscapability have full access (required for Gutenberg)
Security model:
- Check user capability → Allow if
edit_posts(admin/editor users bypass all restrictions) - Check route blacklist → Deny if route is explicitly forbidden
- Check namespace whitelist → Allow if namespace is whitelisted
- Default deny → Reject all other requests
Filters:
Add namespaces that should be publicly accessible.
<?php
// Allow WordPress core API for public posts/pages
add_filter('wack_rest_api_namespace_whitelist', function($namespaces) {
$namespaces[] = 'wp/v2';
return $namespaces;
});
// Allow custom plugin API
add_filter('wack_rest_api_namespace_whitelist', function($namespaces) {
$namespaces[] = 'my-plugin/v1';
$namespaces[] = 'wc/v3'; // WooCommerce
return $namespaces;
});Parameters:
array $namespaces- Array of namespace prefixes (e.g.,'wp/v2','my-plugin/v1')
Default: [] (no public access)
How namespaces work:
- Namespaces are matched by prefix:
'wp/v2'matches/wp/v2/posts,/wp/v2/pages, etc. - Leading slash is optional:
'wp/v2'and'/wp/v2'are equivalent
Block specific routes even if their namespace is whitelisted.
<?php
// Block user enumeration
add_filter('wack_rest_api_forbidden_routes', function($routes) {
$routes[] = '/wp/v2/users';
return $routes;
});
// Block multiple sensitive endpoints
add_filter('wack_rest_api_forbidden_routes', function($routes) {
return array_merge($routes, [
'/wp/v2/users',
'/wp/v2/plugins',
'/wp/v2/themes',
'/wp/v2/settings',
]);
});Parameters:
array $routes- Array of route paths to block
Default: [] (no routes explicitly forbidden)
Recommended blacklist for security:
add_filter('wack_rest_api_forbidden_routes', fn($routes) => array_merge($routes, [
'/wp/v2/users', // User enumeration
'/wp/v2/plugins', // Plugin disclosure
'/wp/v2/themes', // Theme disclosure
'/wp/v2/settings', // Site settings
'/wp/v2/comments', // Comments (if disabled)
]));Gutenberg compatibility:
- Logged-in users with
edit_postscapability bypass all restrictions - This ensures the block editor works without additional configuration
- Public API access requires explicit whitelisting
Completely disables XML-RPC functionality to prevent legacy API attacks.
What it disables:
- XML-RPC API endpoint (
xmlrpc.php) - All XML-RPC methods
- X-Pingback header
- Pingback functionality
Security benefits:
- Prevents brute force attacks via
system.multicall - Blocks DDoS attacks via pingback
- Reduces attack surface for headless WordPress
No filters available - XML-RPC is completely disabled. If you need XML-RPC for legacy integrations, do not instantiate this class.
Note: XML-RPC is a legacy API and is not needed for modern WordPress usage. The REST API and Application Passwords provide better alternatives.
Abstract base class for registering custom post types with sensible defaults for headless WordPress.
Features:
- Simplified post type registration with minimal boilerplate
- Headless-friendly defaults (no frontend templates)
- Automatic REST API exposure
- Support for Gutenberg editor
- Automatic label generation with locale support (English/Japanese)
Basic Usage:
<?php
namespace MyTheme\PostTypes;
use WackFoundation\PostType\BasePostType;
class AuthorPostType extends BasePostType
{
public static function postTypeName(): string
{
return 'author';
}
public static function postTypeLabel(): string
{
return '著者'; // Or 'Author' for English
}
public function __construct()
{
$this->menu_position = 21;
$this->menu_icon = 'dashicons-admin-users';
$this->extra_args = [
'supports' => ['title', 'editor', 'thumbnail'],
];
}
}
// Register the post type
new AuthorPostType()->register();Label Generation: Labels are automatically generated based on the site's locale:
- Japanese locale (
ja*): Uses Japanese label templates (e.g., "新規追加", "編集") - Other locales: Uses English label templates (e.g., "Add New", "Edit")
The post type label from postTypeLabel() is automatically inserted into the templates.
Customizing Labels:
Override the createLabels() method in child classes. Use buildLabelsFromTemplates() for partial customization:
<?php
protected function createLabels(): array
{
$labels = $this->buildLabelsFromTemplates(static::postTypeLabel());
$labels['add_new'] = 'Create New'; // Override specific label
$labels['edit_item'] = 'Modify';
return $labels;
}Required Methods:
postTypeName(): Return the post type slug (e.g., 'product', 'author')postTypeLabel(): Return the singular label (e.g., 'Product', '商品')__construct(): Initialize properties (menu_position, menu_icon, extra_args, etc.)
Customizable Properties:
$menu_icon: Dashicon class or custom URL (default:null)$menu_position: Admin menu position (default:20)$public: Public visibility (default:true)$publicly_queryable: Publicly queryable (default:true)$show_ui: Show admin UI (default:true)$show_in_rest: REST API enabled (default:true)$has_archive: Enable archive (default:true)$extra_args: Additionalregister_post_type()arguments (default:[])- Common usage:
supports,taxonomies,rewrite,capability_type, etc.
- Common usage:
Abstract base class for registering custom taxonomies with sensible defaults for headless WordPress.
Features:
- Simplified taxonomy registration with minimal boilerplate
- Headless-friendly defaults
- REST API exposure enabled by default
- Hierarchical (category-style) by default
- Automatic label generation with locale support (English/Japanese)
Basic Usage:
<?php
namespace MyTheme\Taxonomies;
use WackFoundation\Taxonomy\BaseTaxonomy;
class GenreTaxonomy extends BaseTaxonomy
{
public static function taxonomyKey(): string
{
return 'genre';
}
public static function taxonomyLabel(): string
{
return 'ジャンル'; // Or 'Genre' for English
}
public function __construct()
{
$this->extra_args = [
'rewrite' => ['slug' => 'genres'],
'show_in_nav_menus' => true,
];
}
}
// Register the taxonomy for specific post types
new GenreTaxonomy()->register(['post', 'article']);Non-hierarchical (tag-style) taxonomy:
<?php
class TagTaxonomy extends BaseTaxonomy
{
public static function taxonomyKey(): string
{
return 'custom_tag';
}
public static function taxonomyLabel(): string
{
return 'カスタムタグ';
}
public function __construct()
{
// Override to make it non-hierarchical (tag-style)
$this->hierarchical = false;
$this->extra_args = [
'rewrite' => ['slug' => 'tags'],
'show_tagcloud' => true,
];
}
}Label Generation: Labels are automatically generated based on the site's locale and taxonomy type:
- Hierarchical taxonomies: Use category-style labels
- Japanese: "カテゴリー一覧", "カテゴリーを追加", etc.
- English: "All Categories", "Add Category", etc.
- Non-hierarchical taxonomies: Use tag-style labels
- Japanese: "すべてのタグ", "タグを追加", "人気のタグ", etc.
- English: "All Tags", "Add Tag", "Popular Tags", etc.
The taxonomy label from taxonomyLabel() is automatically inserted into the templates.
Customizing Labels:
Override the createLabels() method in child classes. Use buildLabelsFromTemplates() for partial customization:
<?php
protected function createLabels(): array
{
$labels = $this->buildLabelsFromTemplates(static::taxonomyLabel());
$labels['add_new_item'] = 'Create New'; // Override specific label
$labels['search_items'] = 'Find';
return $labels;
}Required Methods:
taxonomyKey(): Return the taxonomy slug (e.g., 'genre', 'product_tag')taxonomyLabel(): Return the singular label (e.g., 'Genre', 'ジャンル')__construct(): Initialize properties (hierarchical, extra_args, etc.)
Customizable Properties:
$hierarchical: Whether hierarchical (category-style) or flat (tag-style) (default:true)$show_in_rest: REST API enabled (default:true)$extra_args: Additionalregister_taxonomy()arguments (default:[])- Common usage:
rewrite,show_in_nav_menus,show_admin_column,show_tagcloud, etc.
- Common usage:
Abstract base class for implementing custom editor validation in Gutenberg.
Purpose: Enforce content quality rules at the editor level (e.g., require featured image, enforce title length, validate custom fields).
Features:
- JavaScript-based validation in the block editor
- Custom error messages
- Pre-publish checks
- Integration with Gutenberg publish panel
Usage:
<?php
namespace MyTheme\Validations;
use WackFoundation\Validation\BaseValidation;
class PostValidation extends BaseValidation
{
protected string $handle = 'post-validation';
protected string $script_file = 'validation.js';
protected array $target_post_types = [
'post',
'article',
];
protected function getScriptData(): array
{
return [
'requiredFields' => ['title', 'excerpt', 'featured_image'],
'minTitleLength' => 10,
'maxTitleLength' => 60,
];
}
}
// Register validation
new PostValidation();JavaScript validation example:
// validation.js
import { lockEditor } from '/app/themes/wack-foundation/src/Validation/assets/validation-lock-utility.js'; // Change path as needed
/**
* Validate post title length
*/
const validateTitle = () => {
const title = wp.data.select('core/editor').getEditedPostAttribute('title') || '';
const config = validationConfig; // Passed from PHP via wp_localize_script
lockEditor(
title.length < config.minTitleLength,
'title-length-lock',
`Title must be at least ${config.minTitleLength} characters`
);
};
/**
* Validate featured image
*/
const validateFeaturedImage = () => {
const featuredMedia = wp.data.select('core/editor').getEditedPostAttribute('featured_media');
lockEditor(
!featuredMedia,
'featured-image-lock',
'Featured image is required'
);
};
/**
* Run all validation checks
*/
const validate = () => {
validateTitle();
validateFeaturedImage();
};
/**
* Initialize validation
*/
wp.domReady(() => {
validate();
wp.data.subscribe(validate);
});Methods to override:
getTargetPostTypes(): Define which post types to validategetScriptData(): Pass configuration to JavaScriptgetScriptDependencies(): Define JavaScript dependencies
When to use:
- Enforce required fields (featured image, excerpt, etc.)
- Validate title/content length
- Check custom field values
- Prevent publishing of incomplete content
If you previously relied on the Killer Pads project (https://github.com/kodansha/killer-pads), this parent theme fully replaces its intended optimization scope (dashboard reduction, editor hardening, security tightening). Do NOT install or activate Killer Pads when using WACK Foundation as a parent theme; running both would duplicate or conflict on the same WordPress hooks.
Not included here (you must implement separately if needed):
- Post revision limiting or disabling
- Autosave disabling
Those concerns are intentionally left out to avoid enforcing irreversible editorial constraints. Add them in a child theme or a small mu‑plugin if your workflow demands it.
Summary:
- Killer Pads: not required
- Revisions & autosave: unmanaged — handle manually if you need stricter control