원클릭으로
wp-theme-standards
WordPress legacy theme best practices — proper enqueueing, escaping, hooks, security, and performance
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
WordPress legacy theme best practices — proper enqueueing, escaping, hooks, security, and performance
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Rank Math SEO configuration reference, schema JSON-LD templates, meta patterns, and SEO seeding commands
Audit criteria, severity definitions, report JSON schema, and quality thresholds for wp-audit agents
WP-CLI-first principle — use WP-CLI instead of generating PHP code whenever possible, with command reference, ACF seeding patterns, and environment-aware execution
Environment detection, configuration, and WP-CLI wrapper selection for local WordPress development
Demo HTML creation methodology — single-file demos with section comments for 1:1 WordPress conversion
Bilingual/multilingual i18n methodology using ACF _suffix pattern with transparent translation helpers
| name | wp-theme-standards |
| description | WordPress legacy theme best practices — proper enqueueing, escaping, hooks, security, and performance |
| user-invocable | false |
This skill defines the mandatory standards for building WordPress themes using the legacy (classic) theme architecture. No block themes, no Full Site Editing (FSE), no theme.json.
Every theme MUST include these files:
| File | Purpose |
|---|---|
style.css | Theme declaration with required headers |
index.php | Fallback template (required by WordPress) |
functions.php | Theme setup, hooks, enqueuing, helpers |
screenshot.png | Theme thumbnail (1200x900px recommended) |
The style.css file MUST begin with the theme declaration comment. This is how WordPress identifies the theme.
/*
Theme Name: Starter Theme
Theme URI: https://example.com
Author: Developer Name
Author URI: https://example.com
Description: A custom legacy WordPress theme.
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: starter
*/
Note: Do not put actual styles in
style.css. Use it only for the header declaration. All styles go inassets/css/styles.css(or similar), enqueued viafunctions.php.
theme-name/
├── assets/
│ ├── css/
│ │ └── styles.css # Main design system stylesheet
│ ├── js/
│ │ └── main.js # Main client-side JS
│ └── images/ # Theme images (logo fallback, icons, etc.)
├── inc/
│ ├── theme-setup.php # Theme supports, nav menus, content width
│ ├── i18n.php # Internationalization helpers (if bilingual)
│ └── scf-fields.php # SCF/ACF custom field definitions
├── template-parts/
│ ├── section-hero.php # Reusable section templates
│ ├── section-services.php
│ └── ...
├── functions.php # Main functions file (requires inc/ files)
├── header.php # Site header
├── footer.php # Site footer
├── front-page.php # Homepage template
├── index.php # Fallback template
├── style.css # Theme declaration (headers only)
└── screenshot.png # Theme preview image
NEVER add <link> or <script> tags directly in templates. Always use WordPress enqueueing functions.
function prefix_scripts() {
// Google Fonts (external, no version needed)
wp_enqueue_style(
'prefix-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
array(),
null
);
// Main stylesheet with filemtime() cache busting
wp_enqueue_style(
'prefix-style',
get_template_directory_uri() . '/assets/css/styles.css',
array('prefix-fonts'),
filemtime(get_template_directory() . '/assets/css/styles.css')
);
}
add_action('wp_enqueue_scripts', 'prefix_scripts');
// Main JavaScript — loaded in footer (last param = true)
wp_enqueue_script(
'prefix-main',
get_template_directory_uri() . '/assets/js/main.js',
array(),
filemtime(get_template_directory() . '/assets/js/main.js'),
true
);
Always use filemtime() for the version parameter on local assets. This forces browsers to re-download the file whenever it changes, without manual version bumping.
filemtime(get_template_directory() . '/assets/css/styles.css')
For external assets (CDN fonts, libraries), pass null as the version to omit the query string.
Use wp_localize_script() to safely pass PHP data (dynamic content, URLs, translations) to JavaScript.
// Pass calculator data to JS on the pricing page
if (is_page('pricing')) {
$calculator_data = prefix_get_calculator_data();
wp_localize_script('prefix-main', 'prefixCalculator', $calculator_data);
}
// Pass i18n data to JS for all pages
wp_localize_script('prefix-main', 'prefixI18n', array(
'currentLang' => prefix_get_current_lang(),
'strings' => prefix_get_js_translations(),
));
In JavaScript, access the data via the global variable name:
console.log(prefixCalculator.setupOptions);
console.log(prefixI18n.currentLang); // 'en' or 'es'
Load page-specific CSS and JS only when needed using is_page_template() or is_page().
function prefix_scripts() {
// ... main styles/scripts ...
// Software page: dedicated CSS + JS
if (is_page_template('page-software.php')) {
wp_enqueue_style(
'prefix-software-style',
get_template_directory_uri() . '/assets/css/software.css',
array('prefix-style'),
filemtime(get_template_directory() . '/assets/css/software.css')
);
wp_enqueue_script(
'prefix-software-js',
get_template_directory_uri() . '/assets/js/software.js',
array('prefix-main'),
filemtime(get_template_directory() . '/assets/js/software.js'),
true
);
}
}
add_action('wp_enqueue_scripts', 'prefix_scripts');
Register all required theme features inside an after_setup_theme hook.
function prefix_setup() {
// Let WordPress manage the document <title>
add_theme_support('title-tag');
// Enable featured images
add_theme_support('post-thumbnails');
// Custom logo support
add_theme_support('custom-logo', array(
'height' => 100,
'width' => 340,
'flex-height' => true,
'flex-width' => true,
));
// HTML5 markup for core elements
add_theme_support('html5', array(
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script',
));
// RSS feed links in <head>
add_theme_support('automatic-feed-links');
// Register navigation menus
register_nav_menus(array(
'primary' => __('Primary Navigation', 'theme-slug'),
'footer' => __('Footer Navigation', 'theme-slug'),
));
}
add_action('after_setup_theme', 'prefix_setup');
Set the global content width for embeds and images.
function prefix_content_width() {
$GLOBALS['content_width'] = apply_filters('prefix_content_width', 1280);
}
add_action('after_setup_theme', 'prefix_content_width', 0);
Every dynamic value rendered in HTML MUST be escaped. No exceptions.
| Function | Use When | Example |
|---|---|---|
esc_html() | Outputting text content inside HTML tags | <h1><?php echo esc_html($title); ?></h1> |
esc_url() | Outputting URLs in href, src, action attributes | <a href="<?php echo esc_url($link); ?>"> |
esc_attr() | Outputting values inside HTML attributes | <div class="<?php echo esc_attr($class); ?>"> |
wp_kses_post() | Outputting rich text/HTML that should allow safe tags | <div><?php echo wp_kses_post($content); ?></div> |
esc_html()esc_url()esc_attr()wp_kses_post()get_field(), $_GET, $_POST, or any user input without escapingSanitize all input before saving to the database.
| Function | Use For |
|---|---|
sanitize_text_field() | Single-line text input |
sanitize_textarea_field() | Multi-line text input |
sanitize_email() | Email addresses |
absint() | Positive integers |
sanitize_file_name() | File names |
wp_kses_post() | Rich HTML (on save) |
sanitize_url() | URL input |
// Example: sanitize URL parameter
if (isset($_GET['lang'])) {
$lang = sanitize_text_field($_GET['lang']);
}
The hooks below are the ones most commonly used in theme development, listed in the order they typically fire.
| Hook | Type | When to Use |
|---|---|---|
after_setup_theme | Action | Register theme supports, nav menus, content width |
init | Action | Register post types, taxonomies, disable emojis |
acf/init | Action | Register ACF/SCF options pages |
wp_enqueue_scripts | Action | Enqueue all frontend CSS and JS |
wp_head | Action | Add meta tags, preconnect hints, schema markup, preload LCP |
wp_footer | Action | Add inline scripts before </body> |
body_class | Filter | Add custom CSS classes to <body> |
upload_mimes | Filter | Allow additional file types (SVG) |
NEVER use query_posts(). It modifies the main query and causes bugs.
ALWAYS use WP_Query for custom queries.
$args = array(
'post_type' => 'post',
'posts_per_page' => 6,
'orderby' => 'date',
'order' => 'DESC',
);
$query = new WP_Query($args);
if ($query->have_posts()) :
while ($query->have_posts()) : $query->the_post();
get_template_part('template-parts/content', get_post_type());
endwhile;
wp_reset_postdata();
endif;
Always call wp_reset_postdata() after a custom WP_Query loop.
<style> blocks in PHP templates<script> blocks in PHP templatesstyle="" attributes on elementsAll CSS goes in .css files. All JS goes in .js files. Both are enqueued via wp_enqueue_scripts.
The only exceptions are:
wp_head actions for <meta> tags, <link rel="preload">, and <script type="application/ld+json"> (schema markup)admin_head (minimal inline style)Add preconnect hints for external font providers to speed up loading.
function prefix_add_preconnect() {
echo '<link rel="preconnect" href="https://fonts.googleapis.com">' . "\n";
echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' . "\n";
}
add_action('wp_head', 'prefix_add_preconnect', 1);
Preload the Largest Contentful Paint (LCP) element (usually the hero image) on the homepage.
function prefix_preload_lcp_image() {
if (is_front_page()) {
$hero_image = prefix_get_field('hero_image');
$hero_image_url = $hero_image ? $hero_image['url'] : prefix_asset('images/hero-image.png');
echo '<link rel="preload" as="image" href="' . esc_url($hero_image_url) . '">' . "\n";
}
}
add_action('wp_head', 'prefix_preload_lcp_image', 2);
Remove the emoji detection script and styles that WordPress loads on every page.
function prefix_disable_emojis() {
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('admin_print_scripts', 'print_emoji_detection_script');
remove_action('wp_print_styles', 'print_emoji_styles');
remove_action('admin_print_styles', 'print_emoji_styles');
remove_filter('the_content_feed', 'wp_staticize_emoji');
remove_filter('comment_text_rss', 'wp_staticize_emoji');
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
}
add_action('init', 'prefix_disable_emojis');
Remove the generator meta tag that exposes the WordPress version.
remove_action('wp_head', 'wp_generator');
Allow SVG file uploads in the WordPress media library.
function prefix_allow_svg_upload($mimes) {
$mimes['svg'] = 'image/svg+xml';
$mimes['svgz'] = 'image/svg+xml';
return $mimes;
}
add_filter('upload_mimes', 'prefix_allow_svg_upload');
function prefix_fix_svg_display() {
echo '<style>
.attachment-266x266, .thumbnail img {
width: 100% !important;
height: auto !important;
}
</style>';
}
add_action('admin_head', 'prefix_fix_svg_display');
function prefix_check_filetype($data, $file, $filename, $mimes) {
$filetype = wp_check_filetype($filename, $mimes);
return array(
'ext' => $filetype['ext'],
'type' => $filetype['type'],
'proper_filename' => $data['proper_filename'],
);
}
add_filter('wp_check_filetype_and_ext', 'prefix_check_filetype', 10, 4);
Add contextual CSS classes to the <body> tag for page-specific styling.
function prefix_body_classes($classes) {
if (is_front_page()) {
$classes[] = 'home-page';
}
if (is_page('pricing')) {
$classes[] = 'pricing-page';
}
if (is_page_template('page-software.php')) {
$classes[] = 'software-page';
}
if (is_singular('post')) {
$classes[] = 'single-post-page';
}
if (is_archive()) {
$classes[] = 'archive-page';
}
return $classes;
}
add_filter('body_class', 'prefix_body_classes');
All themes built with this system use Secure Custom Fields (SCF) or Advanced Custom Fields (ACF) as the custom fields plugin. SCF is an ACF-compatible fork and uses the same API (get_field(), the_field(), have_rows(), etc.).
Register an options page for site-wide settings (logo, footer content, social links, etc.).
function prefix_register_options_page() {
if (function_exists('acf_add_options_page')) {
acf_add_options_page(array(
'page_title' => 'Site Settings',
'menu_title' => 'Site Settings',
'menu_slug' => 'prefix-settings',
'capability' => 'edit_posts',
'redirect' => false,
'icon_url' => 'dashicons-admin-generic',
'position' => 30,
));
}
}
add_action('acf/init', 'prefix_register_options_page');
// Options page fields use 'option' as the post ID
$logo = get_field('site_logo', 'option');
$footer_text = get_field('footer_copyright', 'option');
Create small utility functions to keep templates clean.
/**
* Get theme asset URL
*/
function prefix_asset($path) {
return get_template_directory_uri() . '/assets/' . ltrim($path, '/');
}
/**
* Get site logo with fallback
*/
function prefix_get_logo() {
$logo = get_field('site_logo', 'option');
if ($logo) {
return $logo;
}
return prefix_asset('images/logo.svg');
}
Add JSON-LD structured data for SEO and AI search optimization.
function prefix_output_schema_markup() {
$site_url = home_url();
$site_name = get_bloginfo('name');
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Organization',
'@id' => $site_url . '/#organization',
'name' => $site_name,
'url' => $site_url,
'description' => 'A brief description of the business.',
);
echo '<script type="application/ld+json">' . "\n";
echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
echo "\n" . '</script>' . "\n";
}
add_action('wp_head', 'prefix_output_schema_markup', 5);
Add meta description tags, but defer to SEO plugins if present.
function prefix_add_meta_description() {
// Skip if Yoast or Rank Math is active
if (defined('WPSEO_VERSION') || defined('RANK_MATH_VERSION')) {
return;
}
$description = '';
if (is_front_page()) {
$description = 'Your site description here.';
} elseif (is_singular('post')) {
$post = get_post();
$description = has_excerpt() ? get_the_excerpt($post) : wp_trim_words(strip_tags($post->post_content), 30, '...');
}
if ($description) {
echo '<meta name="description" content="' . esc_attr($description) . '">' . "\n";
}
}
add_action('wp_head', 'prefix_add_meta_description', 1);
prefix_ (replace with your theme slug, e.g., kairo_, starter_)section-*.php for content sections, footer-*.php for footer variantspage-*.php (e.g., page-pricing.php, page-software.php).block__element--modifiermain.js, software.jsThe nav walker (generates markup) and the header CSS (styles it) MUST agree on one shared set of class names. Use exactly these — do not invent alternates:
| Class | Purpose |
|---|---|
.nav | Root nav container |
.nav__menu | The <ul> menu list |
.nav__item | Each <li> menu item |
.nav__item--has-children | Modifier on items with a dropdown submenu |
.nav__link | The anchor for a plain (non-dropdown) menu item |
.nav__submenu | The nested <ul> dropdown menu |
.nav__toggle | The clickable/focusable element that opens a dropdown (e.g. PROPERTIES ▾) |
States: .is-open (added to .nav__item--has-children when its submenu is expanded) and aria-expanded ("true"/"false" attribute on .nav__toggle, kept in sync with .is-open).
Baseline alignment rule (mandatory): toggle items must sit on the same text baseline as plain links — never let the presence of a dropdown arrow shift a label up/down relative to its siblings. Apply display:flex; align-items:center to both .nav__link and .nav__toggle.
.nav__link,
.nav__toggle {
display: flex;
align-items: center;
}
style.css has required WordPress headerswp_enqueue_style() / wp_enqueue_script()filemtime() used for cache busting on all local assetsafter_setup_themequery_posts() anywhere