Files
peptide-starter-theme/CONVENTIONS.md
peptiderepo a1917d62c2 chore: remove WooCommerce from theme — not used on peptiderepo.com (v1.7.0)
Remove all WC asset dequeue logic, cart icon conditional, and WC-specific tests from the theme. Eliminates the class of fatal errors seen in the 2026-04-23 incident where is_woocommerce() was called after WC deactivation.

Version: 1.6.x → 1.7.0
Reviewed-by: peptiderepo (CTO)
QA: qa-reviews/peptide-starter-theme/2026-04-23-d239bd1e.md
2026-04-24 01:37:31 +07:00

24 KiB
Raw Permalink Blame History

Peptide Starter Theme - Conventions & Development Guide

Naming Conventions

PHP Functions

All custom PHP functions are prefixed with peptide_starter_:

// Good
function peptide_starter_get_hero_title() { }
function peptide_starter_setup() { }
function peptide_starter_widgets_init() { }
function peptide_starter_pagination() { }

// Bad
function get_hero_title() { }
function setup_theme() { }

Pattern: peptide_starter_{verb}_{noun} or peptide_starter_get_{option}

Common function types:

  • peptide_starter_get_*() — Retrieve theme mod or option
  • peptide_starter_{noun}_init() — Initialization functions
  • peptide_starter_is_*() — Boolean checks
  • peptide_starter_render_*() — Output HTML

CSS Classes

All theme CSS classes are prefixed with .ps-:

/* Components */
.ps-btn               /* Button component */
.ps-btn-primary       /* Button variant */
.ps-btn-sm            /* Button size */
.ps-card              /* Card component */
.ps-badge             /* Badge component */
.ps-alert-info        /* Alert variant */

/* Utilities */
.ps-container         /* Max-width wrapper */
.ps-pagination        /* Pagination nav */
.ps-search-form       /* Search form */
.ps-search-input      /* Search input */

/* Regions */
.site-header          /* No prefix; global region */
.site-footer          /* No prefix; global region */
.site-main            /* No prefix; global region */
.hero-section         /* Hero region specific */

Pattern: .ps-{component} for components, .ps-{component}-{variant} for variants

Reserved prefixes (do not use):

  • .pn-* — Peptide News plugin
  • .psa-* — Peptide Search AI plugin
  • .prab-* — PRAutoBlogger plugin

CSS Variables (Custom Properties)

All design tokens are defined in style.css :root:

/* Colors */
--text-primary        /* Main text color */
--text-secondary      /* Secondary text color */
--bg-primary          /* Main background */
--bg-secondary        /* Secondary background */

/* Spacing (8px grid base) */
--spacing-xs: 4px     /* 0.5 × base */
--spacing-sm: 8px     /* 1 × base */
--spacing-md: 16px    /* 2 × base */
--spacing-lg: 24px    /* 3 × base */
--spacing-xl: 32px    /* 4 × base */

/* Typography */
--font-sans           /* Primary font family */
--font-mono           /* Monospace font */
--text-h1             /* H1 size */
--text-h1-weight      /* H1 weight */

Usage:

.ps-card {
  padding: var(--spacing-md);
  color: var(--text-primary);
}

Template Part Naming

Template parts in template-parts/ follow WordPress convention:

template-parts/content.php              /* Default loop item */
template-parts/content-{post-type}.php  /* CPT-specific */
template-parts/content-none.php         /* Empty state */
template-parts/{section}-{name}.php     /* Specific parts (if added) */

Calling template parts:

get_template_part( 'template-parts/content', get_post_type() );
get_template_part( 'template-parts/content', 'none' );

Hook Names

Custom hooks use the peptide_starter_ prefix:

// Filters
apply_filters( 'peptide_starter_show_newsletter', true );

// Actions (if any added in future)
do_action( 'peptide_starter_hero_before' );

File Organization

  • Root: Theme core files (functions.php, header.php, footer.php, etc.)
  • assets/js/: Client-side scripts only; no bundling
  • assets/css/: (None; all CSS in style.css)
  • languages/: Translation files
  • template-parts/: Reusable template parts

How to add a performance policy rule

When adding a new asset-perf rule (e.g., a new plugin's CSS to dequeue on non-relevant pages, or a new font family to slim):

  1. For dequeues: edit inc/perf-asset-policy.php. Add the plugin's handle list to a new function (or extend the existing arrays via the matching filter — peptide_starter_perf_<plugin>_handles). Hook the new dequeue function on wp_enqueue_scripts priority 100, gated by PEPTIDE_STARTER_PERF_DEQUEUE and a context predicate (e.g., is_front_page(), ! is_woocommerce()).

  2. For font slimming: add the family to the default array in peptide_starter_perf_slim_google_fonts(), or override at runtime via the peptide_starter_perf_font_weights filter. Specify only weights actually used by the design system; defaulting to "all weights" defeats the purpose.

  3. For new resource hints: extend peptide_starter_perf_resource_hints() with the new domain in the preconnect array. WP renders the <link> tag automatically; don't echo your own.

  4. For deferring third-party scripts: add the script handle to the list in peptide_starter_perf_defer_cookie_notice() (or rename the function if it grows beyond cookie-notice).

  5. Always add a test case in tests/test-perf-asset-policy.php covering the new rule's happy path AND its negation (the page-context where the rule must NOT apply). The dequeue logic is silent if a handle name is wrong — only tests catch this before deploy.

  6. Verify on production handles before committing. Plugin handle names are not always what you'd guess. SSH to Hostinger and run wp eval 'global $wp_styles; do_action("wp_enqueue_scripts"); foreach($wp_styles->registered as $h=>$o){echo $h.PHP_EOL;}' to dump the actual handle names.

Step-by-Step Guides

How to Add a New Template

Scenario: Create a custom landing page template

  1. Create file: landing-{name}.php

    <?php
    /**
     * Landing Page: {Description}
     * Template Name: {Display Name}
     *
     * @package peptide-starter
     */
    
    get_header();
    ?>
    
    <main id="main" class="site-main">
      <!-- Your content here -->
    </main>
    
    <?php get_footer();
    
  2. Add template header comment:

    /**
     * Template Name: My Custom Landing Page
     * Template Post Type: page
     */
    
  3. The template will appear in WordPress Page > Template dropdown

  4. Test the template:

    • Create/edit a page and select the template
    • Verify hero, dark mode toggle, footer all appear correctly

Best practices:

  • Always include get_header() and get_footer()
  • Wrap main content in <main id="main" class="site-main">
  • Use .ps-container for consistent max-width
  • Use existing component classes (.ps-card, .ps-btn, etc.)
  • Keep templates under 150 lines; extract complex parts to template-parts/

How to Add a New Plugin Integration

Scenario: Integrate a new plugin (e.g., "Research Database") via shortcode

  1. In front-page.php, add a conditional shortcode block:

    <?php if ( shortcode_exists( 'research_db' ) ) { ?>
      <section class="research-db-section">
        <div class="ps-container">
          <h2><?php esc_html_e( 'Research Database', 'peptide-starter' ); ?></h2>
          <?php echo do_shortcode( '[research_db limit="10"]' ); ?>
        </div>
      </section>
    <?php } ?>
    
  2. In functions.php, optionally add inline overrides if plugin CSS needs adjustment:

    function peptide_starter_plugin_overrides() {
      wp_add_inline_style( 'peptide-starter-style', '
        /* Override plugin styles here */
        .rdb-card { /* example */ }
      ' );
    }
    add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 );
    
  3. Document the integration in CLAUDE.md:

    • Plugin namespace (e.g., .rdb-*)
    • Shortcode name and parameters
    • Expected styling classes
  4. If plugin needs dark mode coordination:

    • Ask plugin author to listen to themechange event (see ARCHITECTURE.md)
    • Or have plugin read html[data-theme] attribute
    • Provide plugin with CSS custom properties: getComputedStyle(document.documentElement).getPropertyValue('--text-primary')
  5. Test:

    • Activate plugin and visit front page
    • Verify shortcode renders
    • Toggle dark mode; verify plugin responds
    • Deactivate plugin; verify page still renders (fallback gracefully)

Important: Always wrap shortcodes in shortcode_exists() checks so theme doesn't break if plugin is missing.

How to Add a New CSS Component

Scenario: Create a new "pill" button style (small, rounded, inline)

  1. In style.css, find the Components section (around line 500)

  2. Add new component class:

    /* Pill buttons */
    .ps-btn-pill {
      display: inline-block;
      padding: var(--spacing-xs) var(--spacing-sm);
      border-radius: 9999px;
      font-size: 12px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }
    
    .ps-btn-pill.ps-btn-primary {
      background-color: var(--color-primary);
      color: white;
    }
    
    .ps-btn-pill.ps-btn-secondary {
      background-color: transparent;
      border: 1px solid var(--color-primary);
      color: var(--color-primary);
    }
    
  3. Add dark mode support (in Dark Mode section, around line 1200):

    html[data-theme="dark"] .ps-btn-pill.ps-btn-secondary {
      border-color: var(--color-primary-dark);
      color: var(--color-primary-dark);
    }
    
  4. Add responsive sizes if needed (in Responsive section, around line 1400):

    @media (max-width: 767px) {
      .ps-btn-pill {
        padding: var(--spacing-xs) var(--spacing-xs);
      }
    }
    
  5. Usage in templates:

    <span class="ps-btn ps-btn-pill ps-btn-primary">Beta</span>
    
  6. Test:

    • Light mode: verify colors, sizing, alignment
    • Dark mode: toggle and verify contrast
    • Mobile (480px): verify responsive adjustments
    • Keyboard: focus state should be visible

Best practices:

  • Use CSS variables (var(--spacing-*), var(--color-*)) not hardcoded values
  • Avoid !important unless absolutely necessary
  • Include dark mode support immediately (no .ps-* is light-only)
  • Group related rules with comments
  • Keep component definitions self-contained (<100 lines)

How to Add a New Customizer Setting

Scenario: Add a new "Primary Accent Color" customizer option

  1. In functions.php (around line 143), add a new section:

    $wp_customize->add_section( 'peptide_starter_colors', array(
      'title'    => esc_html__( 'Colors', 'peptide-starter' ),
      'priority' => 45,
    ) );
    
  2. Add a setting:

    $wp_customize->add_setting( 'primary_accent_color', array(
      'default'           => '#0066CC',
      'transport'         => 'postMessage',
      'sanitize_callback' => 'sanitize_hex_color',
    ) );
    
  3. Add a control:

    $wp_customize->add_control( new WP_Customize_Color_Control(
      $wp_customize,
      'primary_accent_color',
      array(
        'label'    => esc_html__( 'Primary Accent Color', 'peptide-starter' ),
        'section'  => 'peptide_starter_colors',
        'settings' => 'primary_accent_color',
      )
    ) );
    
  4. Create a getter function:

    function peptide_starter_get_primary_accent_color() {
      return get_theme_mod( 'primary_accent_color', '#0066CC' );
    }
    
  5. Output in front-end (e.g., in footer.php):

    <style>
      :root {
        --color-accent: <?php echo esc_attr( peptide_starter_get_primary_accent_color() ); ?>;
      }
    </style>
    

    Or better, use inline style in wp_head:

    function peptide_starter_custom_colors() {
      $accent = peptide_starter_get_primary_accent_color();
      ?>
      <style>
        :root {
          --color-accent: <?php echo esc_attr( $accent ); ?>;
        }
      </style>
      <?php
    }
    add_action( 'wp_head', 'peptide_starter_custom_colors' );
    
  6. Reference in CSS:

    .ps-btn-accent {
      background-color: var(--color-accent);
    }
    
  7. Test:

    • Appearance → Customize → Colors → Primary Accent Color
    • Change color and verify live preview
    • Save and reload page

Transport options:

  • 'transport' => 'postMessage' — Instant preview (needs JS listener)
  • 'transport' => 'refresh' — Full page refresh

For color/text changes, use postMessage with a JS listener in theme.js:

wp_customize.bind( 'setting:primary_accent_color', function( setting ) {
  setting.bind( function( newval ) {
    document.documentElement.style.setProperty('--color-accent', newval);
  });
});

Error Handling Patterns

Theme Doesn't Break If Plugin Missing

Pattern: Graceful Degradation

// Good — shortcode_exists check
if ( shortcode_exists( 'peptide_search' ) ) {
  echo do_shortcode( '[peptide_search]' );
} else {
  get_search_form();  // Fallback to WordPress default
}

// Bad — assumes plugin exists
echo do_shortcode( '[peptide_search]' );  // Renders [peptide_search] as text if missing

Menu Fallback

In header.php:

wp_nav_menu( array(
  'theme_location' => 'primary',
  'fallback_cb'    => 'peptide_starter_primary_menu_fallback',  // Function defined in header
  'walker'         => new Peptide_Starter_Nav_Walker(),
) );

function peptide_starter_primary_menu_fallback() {
  // Renders Home, About, Peptides links if no menu assigned
}

Widget Area Check

In footer.php:

if ( is_active_sidebar( 'footer-1' ) ) {
  dynamic_sidebar( 'footer-1' );
} else {
  // Render default content
  echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
}

Class Existence Check


**For post types:**
```php
if ( post_type_exists( 'peptide' ) ) {
  echo '<a href="' . get_post_type_archive_link( 'peptide' ) . '">' . __( 'Peptides' ) . '</a>';
}

Input Validation

All user-facing output must be escaped:

// Good
echo esc_html( get_the_title() );
echo esc_url( home_url( '/' ) );
echo esc_attr( $html_attribute_value );

// Bad
echo get_the_title();
echo home_url( '/' );

Dark Mode Implementation Guide

How Dark Mode Works in Peptide Starter

  1. Inline script in wp_head (functions.php, line 406-422)

    • Runs immediately before CSS loads
    • Prevents white flash by setting html[data-theme] early
    • Checks in order: localStorage → system pref → customizer default
  2. CSS selectors (style.css, line ~1200 "Dark Mode")

    • Light mode: default colors in :root
    • Dark mode: html[data-theme="dark"] selector overrides
  3. JavaScript listener (theme.js, line 47-63)

    • Responds to dark mode toggle button
    • Updates localStorage
    • Dispatches themechange event to plugins
    • Updates sun/moon icon visibility

Adding Dark Mode to a New Component

Example: New Alert Style

  1. Define light mode in CSS:

    .ps-alert-custom {
      padding: var(--spacing-md);
      background-color: #E3F2FD;  /* Light blue */
      color: #0D47A1;              /* Dark blue text */
      border: 1px solid #90CAF9;
    }
    
  2. Add dark mode override:

    html[data-theme="dark"] .ps-alert-custom {
      background-color: #1A237E;  /* Dark blue bg */
      color: #E3F2FD;              /* Light text */
      border-color: #3F51B5;       /* Darker border */
    }
    
  3. Test:

    • Load page in light mode → verify colors
    • Toggle dark mode → verify override colors are applied
    • Open DevTools → verify html[data-theme="dark"] attribute set
    • Check localStorage peptide-starter-theme is updated

Helping Plugins Support Dark Mode

If a plugin doesn't listen to dark mode changes:

  1. Ask plugin author to listen to the themechange event:

    document.addEventListener('themechange', (e) => {
      const theme = e.detail.theme;
      // Update plugin styles based on theme
    });
    
  2. Or use a MutationObserver to watch for data-theme changes:

    const observer = new MutationObserver(() => {
      const theme = document.documentElement.getAttribute('data-theme');
      // Update styles
    });
    observer.observe(document.documentElement, { attributes: true });
    
  3. Or read CSS variables in the plugin stylesheet:

    .plugin-card {
      color: var(--text-primary);
      background-color: var(--bg-secondary);
    }
    

CSS Variables for Dark Mode

All available variables (defined in :root):

--text-primary          /* Main text (light: #111827, dark: #F9FAFB) */
--text-secondary        /* Secondary text (light: #6B7280, dark: #D1D5DB) */
--bg-primary            /* Main background (light: #FFFFFF, dark: #111827) */
--bg-secondary          /* Secondary bg (light: #F3F4F6, dark: #1F2937) */
--color-primary         /* Primary accent (light: #0066CC, dark: #3B82F6) */
--border-color          /* Border color (light: #E5E7EB, dark: #374151) */

Testing Checklist

When modifying the theme, verify:

  • Light mode — All text readable, colors correct
  • Dark mode — Toggle to dark, verify colors swap, no white flash on reload
  • Mobile (480px) — Navigation collapses, search/dark mode icons present
  • Tablet (768px) — Layout adapts, footer 2-column if needed
  • Desktop (1024px+) — Full layout renders correctly
  • Plugins active — Shortcodes render, no JS errors in console
  • Plugins inactive — Graceful fallbacks work (search form, no broken layout)
  • Accessibility — Tab through page, all interactive elements reachable
  • Focus states — Every button/link has visible focus outline
  • Keyboard navigation — Escape closes overlays, arrow keys in menu if relevant

Common Tasks & Commands

Check JavaScript syntax:

node --check assets/js/theme.js
node --check assets/js/navigation.js

View theme version:

echo PEPTIDE_STARTER_VERSION;  // Currently: 1.3.1

Update theme version (when releasing):

  1. Edit style.css line 7: Version: X.Y.Z
  2. Edit functions.php line 14: define( 'PEPTIDE_STARTER_VERSION', 'X.Y.Z' )

Deploy to production:

  • Push to main branch on GitHub
  • GitHub Actions runs rsync to Hostinger via SSH
  • LiteSpeed cache clears automatically

Debug active plugins:

// In functions.php or template
$active = get_option( 'active_plugins' );
error_log( print_r( $active, true ) );

View Customizer settings:

echo get_theme_mod( 'hero_title' );
echo get_theme_mod( 'dark_mode_default', false );

Trust model for request headers (v1.5.2)

Never trust HTTP_X_FORWARDED_FOR, HTTP_CF_CONNECTING_IP, or any other client-controlled header without first validating the peer. Always read the client IP via peptide_starter_get_client_ip() — it handles the Cloudflare-peer check, the XFF opt-in, and the REMOTE_ADDR fallback.

If a new feature needs to trust a different proxy header (e.g. a new CDN in front of CF):

  1. Add the upstream proxy's IP ranges to a new file under inc/.
  2. Extend peptide_starter_get_client_ip() with a new validated branch guarded by a filter defaulted to off.
  3. Never accept a header whose source can't be validated at the TCP layer.

Refresh the Cloudflare range snapshot in inc/cloudflare-ips.php quarterly. Out-of-band bumps go through the peptide_starter_cloudflare_ip_ranges filter.

Security Patterns (v1.5.1)

Rate-limit a handler

Every new public POST handler must be rate-limited.

// After nonce + honeypot checks.
$identifier = peptide_starter_hash_identifier( strtolower( $email ) ); // or 'ip' for IP-only bucket
if ( ! Peptide_Starter_Rate_Limiter::check( 'login', $identifier ) ) {
    // Respond with the SAME message the real-validation-failure path uses.
    wp_send_json_error( array( 'message' => peptide_starter_login_failure_message() ) );
}
Peptide_Starter_Rate_Limiter::record( 'login', $identifier ); // on failure
// ...successful operation...
Peptide_Starter_Rate_Limiter::reset( 'login', $identifier ); // on success

Pick a short action slug. Register its limit in inc/config.php under rate_limits. Never hardcode numbers in the handler.

Honeypot a form

  1. Render the trap in the form template part:
<?php peptide_starter_render_honeypot( 'myform' ); ?>
  1. In the handler, right after nonce verification:
if ( peptide_starter_honeypot_triggered( 'ps_hp_myform' ) ) {
    // Fake-success response — do NOT return an error. Bots learn from errors.
    wp_send_json_success( array( 'message' => __( 'OK', 'peptide-starter' ) ) );
}

Gate a template

At the very top of the template, after the ABSPATH guard, before get_header():

peptide_starter_require_login();

This redirects unauthenticated users to /auth?redirect_to=current-uri and unverified users to /profile?verify_required=1. Do not reinvent this per-template.

CSV-safe export

Route every cell through the helper before fputcsv:

fputcsv( $output, array(
    peptide_starter_csv_safe( $email ),
    peptide_starter_csv_safe( $date ),
) );

Error unification

Login + registration failures must emit the same message regardless of which validation check failed. Use:

  • peptide_starter_login_failure_message() — "Invalid email or password."
  • peptide_starter_register_failure_message() — "Unable to create account..."

Branching on failure reason is an enumeration oracle. Don't do it.

Adding a new security threshold

Add a key to the defaults array in inc/config.php and always read it via peptide_starter_config_get() or the shortcut helpers. No magic numbers in handler files.

  • ARCHITECTURE.md — System design, data flows, integrations
  • CLAUDE.md — Developer context and quick reference
  • README.md — User-facing feature documentation
  • CHANGELOG.md — Version history

Git Workflow — PR-Gated, Soft-Enforced

This repo is private on GitHub's free plan, which does not support branch protection or rulesets. The review gate is enforced at the agent layer, not server-side. Every agent and human contributor follows these rules; the .github/workflows/main-push-audit.yml tripwire opens an audit issue on any direct push to main that did not come from a merged PR.

Rules

  1. Never push to main directly. Every change lands via a pull request that the maintainer merges. No self-merging. No force-pushing to main.

  2. Branch naming: claude/<scope>-<YYYYMMDD> for agent-authored work, fix/<scope> or feat/<scope> for human-authored. The scope is a 1–3 word kebab-case description.

  3. Commit trailer: every commit authored by an agent must end with an Agent-Session: trailer so commits can be correlated back to the conversation that produced them, even though all commits share the peptiderepo bot identity.

    feat: add per-request cost tracking
    
    Agent-Session: cowork-2026-04-14-cost-audit
    
  4. PR description template: every PR description covers

    • What changed (one paragraph)
    • Why (motivation or incident link)
    • Risk flags (schema changes, API contract changes, cost impact, compatibility)
    • Test plan (what was run locally, what to smoke-test after merge)
  5. Emergency push exception: if a situation genuinely requires pushing to main without a PR (site down, CI broken, one-line hotfix), surface it in the chat before doing it. Every emergency push gets a follow-up PR that commits the same changes through the normal flow so git stays the source of truth. The tripwire will open an audit issue — close it with a comment explaining why.

Opening a PR from an agent session

The gh CLI is not installed in the Cowork sandbox. Use curl with the PAT from the workspace .env.credentials:

GH_TOKEN=$(grep "^GITHUB_PAT=" "$WORKSPACE/.env.credentials" | cut -d= -f2)

git push -u origin HEAD

curl -s -X POST -H "Authorization: token $GH_TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/peptiderepo/<repo>/pulls" \
  -d "$(jq -cn --arg title "<title>" --arg head "<branch>" --arg body "<body>" \
      '{title:$title, head:$head, base:"main", body:$body}')"

When this changes

If the peptiderepo GitHub account is ever upgraded to Pro (or the repo goes public), replace this soft-enforcement section with a note pointing to the real branch protection rules in repo settings.