# Peptide Starter Theme - Conventions & Development Guide ## Naming Conventions ### PHP Functions All custom PHP functions are prefixed with `peptide_starter_`: ```php // 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-`: ```css /* 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`: ```css /* 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:** ```css .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:** ```php 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: ```php // 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 ## Step-by-Step Guides ### How to Add a New Template **Scenario:** Create a custom landing page template 1. Create file: `landing-{name}.php` ```php
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 `
` - 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

``` 2. **In functions.php**, optionally add inline overrides if plugin CSS needs adjustment: ```php 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:** ```css /* 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): ```css 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): ```css @media (max-width: 767px) { .ps-btn-pill { padding: var(--spacing-xs) var(--spacing-xs); } } ``` 5. **Usage in templates:** ```html Beta ``` 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: ```php $wp_customize->add_section( 'peptide_starter_colors', array( 'title' => esc_html__( 'Colors', 'peptide-starter' ), 'priority' => 45, ) ); ``` 2. **Add a setting:** ```php $wp_customize->add_setting( 'primary_accent_color', array( 'default' => '#0066CC', 'transport' => 'postMessage', 'sanitize_callback' => 'sanitize_hex_color', ) ); ``` 3. **Add a control:** ```php $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:** ```php 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): ```php ``` Or better, use inline style in `wp_head`: ```php function peptide_starter_custom_colors() { $accent = peptide_starter_get_primary_accent_color(); ?> 'postMessage'` — Instant preview (needs JS listener) - `'transport' => 'refresh'` — Full page refresh For color/text changes, use `postMessage` with a JS listener in theme.js: ```javascript 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** ```php // 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:** ```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:** ```php if ( is_active_sidebar( 'footer-1' ) ) { dynamic_sidebar( 'footer-1' ); } else { // Render default content echo '

' . esc_html__( 'About', 'peptide-starter' ) . '

'; } ``` ### Class Existence Check **For WooCommerce:** ```php if ( class_exists( 'WooCommerce' ) ) { // Render cart icon } ``` **For post types:** ```php if ( post_type_exists( 'peptide' ) ) { echo '' . __( 'Peptides' ) . ''; } ``` ### Input Validation All user-facing output must be escaped: ```php // 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:** ```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:** ```css 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: ```javascript 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: ```javascript 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: ```css .plugin-card { color: var(--text-primary); background-color: var(--bg-secondary); } ``` ### CSS Variables for Dark Mode All available variables (defined in `:root`): ```css --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:** ```bash node --check assets/js/theme.js node --check assets/js/navigation.js ``` **View theme version:** ```php 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:** ```php // In functions.php or template $active = get_option( 'active_plugins' ); error_log( print_r( $active, true ) ); ``` **View Customizer settings:** ```php echo get_theme_mod( 'hero_title' ); echo get_theme_mod( 'dark_mode_default', false ); ``` ## Security Patterns (v1.5.1) ### Rate-limit a handler Every new public POST handler must be rate-limited. ```php // 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 ``` 2. In the handler, right after nonce verification: ```php 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()`: ```php 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`: ```php 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. ## Related Documentation - **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/-` for agent-authored work, `fix/` or `feat/` 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`: ```bash 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//pulls" \ -d "$(jq -cn --arg 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.