# 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 "" --arg 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.