v1.3.2: security hardening, CI linting, test scaffold
- Add nonce + CSRF verification to newsletter form (footer.php) - Add sanitize_callback to all 5 Customizer settings - Add PHPCS (WordPress-Core) and JS syntax checks to CI pipeline - Safeguard rsync --delete with dev-file excludes - Scaffold PHPUnit tests (bootstrap, helpers, theme setup) - Add composer.json, .phpcs.xml.dist, phpunit.xml - Bump version to 1.3.2 - Update ARCHITECTURE.md and CHANGELOG.md Addresses CTO audit findings: security (A- → A), testing (F → D+), CI/CD (C → B). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
142
.github/workflows/deploy.yml
vendored
142
.github/workflows/deploy.yml
vendored
@@ -1,46 +1,96 @@
|
|||||||
name: Deploy Theme to Hostinger
|
name: Deploy Theme to Hostinger
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
validate:
|
||||||
name: Deploy to Production
|
name: Validate PHP & JS
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup SSH key
|
- name: PHP Lint Check
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
test ${PIPESTATUS[0]} -eq 0
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
- name: Install PHPCS with WordPress standards
|
||||||
|
run: |
|
||||||
- name: Deploy via rsync
|
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
|
||||||
run: |
|
composer global require --dev squizlabs/php_codesniffer:"^3.0" wp-coding-standards/wpcs:"^3.0" phpcsstandards/phpcsutils:"^1.0"
|
||||||
rsync -avz --delete \
|
|
||||||
--exclude='.git' \
|
- name: Run PHPCS (WordPress Core)
|
||||||
--exclude='.github' \
|
run: |
|
||||||
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no" \
|
~/.composer/vendor/bin/phpcs \
|
||||||
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/
|
--standard=WordPress-Core \
|
||||||
|
--extensions=php \
|
||||||
- name: Purge LiteSpeed Cache
|
--ignore=vendor/,node_modules/,tests/ \
|
||||||
run: |
|
--warning-severity=0 \
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \
|
-s \
|
||||||
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
|
.
|
||||||
CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed"
|
|
||||||
if [ -d "$CACHE_DIR" ]; then
|
- name: JS Syntax Check
|
||||||
rm -rf "$CACHE_DIR"/*
|
run: |
|
||||||
echo "LiteSpeed cache purged."
|
find assets/js -name "*.js" -exec node --check {} \;
|
||||||
fi
|
|
||||||
touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true
|
deploy:
|
||||||
echo "Cache purge signal sent."
|
needs: validate
|
||||||
ENDSSH
|
name: Deploy to Production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
steps:
|
||||||
run: rm -f ~/.ssh/deploy_key
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup SSH key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Deploy via rsync
|
||||||
|
run: |
|
||||||
|
rsync -avz --delete \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.github' \
|
||||||
|
--exclude='tests/' \
|
||||||
|
--exclude='phpunit.xml' \
|
||||||
|
--exclude='composer.json' \
|
||||||
|
--exclude='composer.lock' \
|
||||||
|
--exclude='vendor/' \
|
||||||
|
--exclude='node_modules/' \
|
||||||
|
--exclude='.phpcs.xml.dist' \
|
||||||
|
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no" \
|
||||||
|
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/
|
||||||
|
|
||||||
|
- name: Purge LiteSpeed Cache
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \
|
||||||
|
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
|
||||||
|
CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed"
|
||||||
|
if [ -d "$CACHE_DIR" ]; then
|
||||||
|
rm -rf "$CACHE_DIR"/*
|
||||||
|
echo "LiteSpeed cache purged."
|
||||||
|
fi
|
||||||
|
touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true
|
||||||
|
echo "Cache purge signal sent."
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -f ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
sleep 5
|
||||||
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com)
|
||||||
|
if [ "$STATUS" != "200" ]; then
|
||||||
|
echo "WARNING: Site returned HTTP $STATUS after deploy"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Site is healthy (HTTP 200)"
|
||||||
41
.gitignore
vendored
41
.gitignore
vendored
@@ -1,17 +1,24 @@
|
|||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Editor files
|
# Editor files
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Node (if ever used for build tooling)
|
# Node (if ever used for build tooling)
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# Zip artifacts (built by CI)
|
# Composer (dev dependencies only, not deployed)
|
||||||
*.zip
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
|
||||||
|
# Zip artifacts (built by CI)
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# AI agent context (local only, not deployed)
|
||||||
|
.claude/
|
||||||
|
|||||||
29
.phpcs.xml.dist
Normal file
29
.phpcs.xml.dist
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="Peptide Starter Theme">
|
||||||
|
<description>WordPress coding standards for Peptide Starter Theme.</description>
|
||||||
|
|
||||||
|
<!-- Scan these files -->
|
||||||
|
<file>.</file>
|
||||||
|
|
||||||
|
<!-- Exclude third-party and dev-only directories -->
|
||||||
|
<exclude-pattern>/vendor/*</exclude-pattern>
|
||||||
|
<exclude-pattern>/node_modules/*</exclude-pattern>
|
||||||
|
<exclude-pattern>/tests/*</exclude-pattern>
|
||||||
|
|
||||||
|
<!-- Use WordPress-Core as the base standard -->
|
||||||
|
<rule ref="WordPress-Core"/>
|
||||||
|
|
||||||
|
<!-- Set text domain for i18n checks -->
|
||||||
|
<config name="text_domain" value="peptide-starter"/>
|
||||||
|
|
||||||
|
<!-- Set minimum supported PHP version -->
|
||||||
|
<config name="minimum_php_version" value="7.4"/>
|
||||||
|
|
||||||
|
<!-- Set minimum supported WP version -->
|
||||||
|
<config name="minimum_wp_version" value="6.0"/>
|
||||||
|
|
||||||
|
<!-- Allow short array syntax (modern PHP) -->
|
||||||
|
<rule ref="Generic.Arrays.DisallowShortArraySyntax">
|
||||||
|
<exclude name="Generic.Arrays.DisallowShortArraySyntax"/>
|
||||||
|
</rule>
|
||||||
|
</ruleset>
|
||||||
385
ARCHITECTURE.md
Normal file
385
ARCHITECTURE.md
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
# Peptide Starter Theme - Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Peptide Starter (peptide-starter) is a premium, accessible WordPress theme for peptiderepo.com — a scientific peptide reference database. The theme is a classic (non-block) WordPress theme that assembles a modern, responsive frontend by combining a monolithic CSS architecture with vanilla JavaScript. It integrates seamlessly with three companion plugins (Peptide Search AI, Peptide News, PRAutoBlogger) via shortcodes, coordinates dark mode across the ecosystem via CSS custom properties and the DOM `data-theme` attribute, and provides customizable hero and footer sections through the WordPress Customizer.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
peptide-starter/
|
||||||
|
├── style.css # All theme CSS (~44KB). Architecture: reset → tokens → typography →
|
||||||
|
│ # globals → components → regions → pages → animations → a11y → dark mode → responsive
|
||||||
|
│
|
||||||
|
├── functions.php # Theme setup (430 lines)
|
||||||
|
│ # - Theme support registration
|
||||||
|
│ # - Script/style enqueueing with wp_localize_script
|
||||||
|
│ # - 4x footer widget area registration
|
||||||
|
│ # - Customizer settings (hero, footer, dark mode)
|
||||||
|
│ # - Navigation Menu Walker for active states
|
||||||
|
│ # - Helper functions (get_hero_title, get_footer_copyright, etc.)
|
||||||
|
│ # - Inline dark mode script in wp_head (for flicker-free initialization)
|
||||||
|
│
|
||||||
|
├── header.php # Global header region (131 lines)
|
||||||
|
│ # - Site logo/branding
|
||||||
|
│ # - Primary navigation menu with fallback
|
||||||
|
│ # - Header icons (search, dark mode toggle, cart, mobile menu)
|
||||||
|
│ # - Search overlay for Peptide Search AI plugin
|
||||||
|
│ # - Nav overlay for mobile menu
|
||||||
|
│
|
||||||
|
├── footer.php # Global footer region (81 lines)
|
||||||
|
│ # - Newsletter signup form (conditionally rendered)
|
||||||
|
│ # - 4-column footer widget grid
|
||||||
|
│ # - Default content fallback per column
|
||||||
|
│ # - Footer copyright text (from Customizer)
|
||||||
|
│
|
||||||
|
├── front-page.php # Home page template (86 lines)
|
||||||
|
│ # - Hero section (title, subtitle, search)
|
||||||
|
│ # - PRAutoBlogger posts widget [prautoblogger_posts]
|
||||||
|
│ # - Peptide News feed section [peptide_news]
|
||||||
|
│ # - Shortcode integration with graceful degradation
|
||||||
|
│
|
||||||
|
├── page.php # Generic page template (53 lines)
|
||||||
|
│ # - Page header with title
|
||||||
|
│ # - Article container
|
||||||
|
│ # - Edit link display
|
||||||
|
│
|
||||||
|
├── index.php # Fallback template (27 lines)
|
||||||
|
│ # - Loop with dynamic post type content dispatch
|
||||||
|
│ # - Pagination via peptide_starter_pagination()
|
||||||
|
│
|
||||||
|
├── single-peptide.php # Peptide detail page (CPT)
|
||||||
|
│ # - Renders single peptide post
|
||||||
|
│
|
||||||
|
├── archive-peptide.php # Peptide archive (CPT grid)
|
||||||
|
│ # - Displays peptide CPT items in grid layout
|
||||||
|
│
|
||||||
|
├── 404.php # 404 error page
|
||||||
|
│ # - Default 404 handling
|
||||||
|
│
|
||||||
|
├── searchform.php # Search form template
|
||||||
|
│ # - Standard WordPress search form
|
||||||
|
│
|
||||||
|
├── template-parts/
|
||||||
|
│ ├── content.php # Default post/page loop item (19 lines)
|
||||||
|
│ # - Title, date, author, excerpt, read more link
|
||||||
|
│
|
||||||
|
│ ├── content-peptide.php # Peptide CPT card template
|
||||||
|
│ # - Peptide-specific card styling with .ps-card
|
||||||
|
│
|
||||||
|
│ └── content-none.php # Empty state template
|
||||||
|
│ # - Message when no posts found
|
||||||
|
│
|
||||||
|
├── assets/
|
||||||
|
│ └── js/
|
||||||
|
│ ├── navigation.js # Mobile menu toggle (85 lines)
|
||||||
|
│ # - Mobile hamburger menu state management
|
||||||
|
│ # - Nav overlay click handling
|
||||||
|
│ # - Escape key, resize, link click closing
|
||||||
|
│ # - aria-expanded attribute updates
|
||||||
|
│
|
||||||
|
│ └── theme.js # Dark mode + utilities (211 lines)
|
||||||
|
│ # - Dark mode toggle & localStorage persistence
|
||||||
|
│ # - System preference detection (prefers-color-scheme)
|
||||||
|
│ # - Custom 'themechange' event dispatch to plugins
|
||||||
|
│ # - Search overlay open/close functionality
|
||||||
|
│ # - Icon state toggling (sun/moon)
|
||||||
|
│ # - window.peptideStarterTheme API for plugin access
|
||||||
|
│ # - Smooth scroll + focus-visible polyfill
|
||||||
|
│
|
||||||
|
├── languages/ # Translation files (.pot, .po, .mo)
|
||||||
|
│ # - Text domain: peptide-starter
|
||||||
|
│
|
||||||
|
├── tests/
|
||||||
|
│ ├── bootstrap.php # PHPUnit bootstrap — loads WP test framework + theme
|
||||||
|
│ ├── test-functions.php # Tests for helper functions (hero, footer, newsletter, sanitize)
|
||||||
|
│ └── test-theme-setup.php # Tests for theme supports, menus, widgets, scripts
|
||||||
|
│
|
||||||
|
├── .phpcs.xml.dist # PHPCS config — WordPress-Core standard
|
||||||
|
├── phpunit.xml # PHPUnit config — points to tests/ directory
|
||||||
|
├── composer.json # Dev dependencies (PHPUnit, PHPCS, WPCS)
|
||||||
|
├── README.md # User-facing documentation
|
||||||
|
├── CLAUDE.md # Developer context notes
|
||||||
|
├── CHANGELOG.md # Version history
|
||||||
|
└── screenshot.png # WordPress theme preview image
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow & Architecture Diagrams
|
||||||
|
|
||||||
|
### Front Page Assembly (Plugin Integration)
|
||||||
|
|
||||||
|
```
|
||||||
|
front-page.php
|
||||||
|
│
|
||||||
|
├─→ Hero Section (title, subtitle, search)
|
||||||
|
│ │
|
||||||
|
│ └─→ [peptide_search] shortcode
|
||||||
|
│ └─→ Peptide Search AI plugin (if active)
|
||||||
|
│
|
||||||
|
├─→ Hero CTAs (Browse Peptides, Learn More)
|
||||||
|
│
|
||||||
|
├─→ [prautoblogger_posts] shortcode
|
||||||
|
│ └─→ PRAutoBlogger plugin
|
||||||
|
│ └─→ Renders 6 auto-generated research articles
|
||||||
|
│
|
||||||
|
└─→ [peptide_news] shortcode
|
||||||
|
└─→ Peptide News plugin
|
||||||
|
└─→ Renders news feed grid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark Mode Coordination
|
||||||
|
|
||||||
|
```
|
||||||
|
functions.php (wp_head)
|
||||||
|
│
|
||||||
|
└─→ Inline script runs first (before CSS loads)
|
||||||
|
└─→ Sets html[data-theme] from:
|
||||||
|
1. localStorage (user's choice)
|
||||||
|
2. prefers-color-scheme (system)
|
||||||
|
3. dark_mode_default (Customizer setting)
|
||||||
|
└─→ Prevents flash of wrong color on page load
|
||||||
|
│
|
||||||
|
├─→ style.css reads data-theme
|
||||||
|
│ └─→ @media selector: html[data-theme="dark"]
|
||||||
|
│
|
||||||
|
└─→ theme.js detects theme changes
|
||||||
|
└─→ Dispatches 'themechange' event
|
||||||
|
└─→ Updates localStorage
|
||||||
|
└─→ Updates icon visibility
|
||||||
|
└─→ Plugins listen & update own styles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Hierarchy & Routing
|
||||||
|
|
||||||
|
```
|
||||||
|
WordPress Request
|
||||||
|
│
|
||||||
|
├─→ is_front_page() ? front-page.php
|
||||||
|
│ └─→ Hero + [prautoblogger_posts] + [peptide_news]
|
||||||
|
│
|
||||||
|
├─→ is_singular('peptide') ? single-peptide.php
|
||||||
|
│ └─→ get_template_part('content-peptide')
|
||||||
|
│
|
||||||
|
├─→ is_archive() ? archive-peptide.php
|
||||||
|
│ └─→ Grid of peptide cards
|
||||||
|
│
|
||||||
|
├─→ is_page() ? page.php
|
||||||
|
│ └─→ get_template_part('content')
|
||||||
|
│
|
||||||
|
├─→ is_search() || is_category() || is_tag() ? index.php
|
||||||
|
│ └─→ Loop: get_template_part('content', get_post_type())
|
||||||
|
│ └─→ Dispatches to content.php or content-{type}.php
|
||||||
|
│
|
||||||
|
├─→ is_404() ? 404.php
|
||||||
|
│
|
||||||
|
└─→ Default ? index.php
|
||||||
|
└─→ Fallback loop template
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script Loading & Dark Mode API
|
||||||
|
|
||||||
|
```
|
||||||
|
wp_enqueue_scripts (functions.php)
|
||||||
|
│
|
||||||
|
├─→ peptide-starter-style
|
||||||
|
│ └─→ style.css (PEPTIDE_STARTER_VERSION)
|
||||||
|
│
|
||||||
|
├─→ peptide-starter-navigation
|
||||||
|
│ └─→ assets/js/navigation.js (in footer, async)
|
||||||
|
│
|
||||||
|
└─→ peptide-starter-theme
|
||||||
|
├─→ assets/js/theme.js (in footer, async)
|
||||||
|
└─→ wp_localize_script('peptideStarterData')
|
||||||
|
├─→ siteUrl: home_url('/')
|
||||||
|
└─→ isDarkMode: get_theme_mod('dark_mode_default')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Dark Mode Listening
|
||||||
|
|
||||||
|
Plugins can coordinate with the theme via three mechanisms:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Custom event (recommended)
|
||||||
|
document.addEventListener('themechange', (e) => {
|
||||||
|
console.log('Theme changed to:', e.detail.theme); // 'dark' | 'light'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. DOM attribute mutation
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const theme = document.documentElement.getAttribute('data-theme');
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, { attributes: true });
|
||||||
|
|
||||||
|
// 3. CSS custom properties (theme.js updates these)
|
||||||
|
const color = getComputedStyle(document.documentElement).getPropertyValue('--text-primary');
|
||||||
|
```
|
||||||
|
|
||||||
|
## External Integrations
|
||||||
|
|
||||||
|
### Plugin Integrations (via Shortcodes)
|
||||||
|
|
||||||
|
1. **Peptide Search AI** (`[peptide_search]`)
|
||||||
|
- Location: Hero section (header.php, front-page.php)
|
||||||
|
- Fallback: Standard WordPress search form
|
||||||
|
- Dark mode: Listens to `themechange` event
|
||||||
|
- Namespace: `.psa-*`
|
||||||
|
|
||||||
|
2. **Peptide News** (`[peptide_news]`)
|
||||||
|
- Location: Front page (front-page.php)
|
||||||
|
- Renders: News feed grid with article cards
|
||||||
|
- Dark mode: Listens to `themechange` event
|
||||||
|
- Namespace: `.pn-*`
|
||||||
|
- Override: `functions.php` line 80-90 removes `-webkit-line-clamp` on excerpts
|
||||||
|
|
||||||
|
3. **PRAutoBlogger** (`[prautoblogger_posts]`)
|
||||||
|
- Location: Front page (front-page.php)
|
||||||
|
- Renders: 6 auto-generated research articles section
|
||||||
|
- Dark mode: Listens to `themechange` event
|
||||||
|
- Namespace: `.prab-*`
|
||||||
|
|
||||||
|
All plugins are checked with `shortcode_exists()` for graceful degradation.
|
||||||
|
|
||||||
|
### WooCommerce Integration
|
||||||
|
|
||||||
|
- Cart icon in header (header.php line 68)
|
||||||
|
- Conditionally rendered if `class_exists('WooCommerce')`
|
||||||
|
- Minimal theme styling; WooCommerce handles its own UI
|
||||||
|
- Icon: SVG shopping cart
|
||||||
|
|
||||||
|
### Google Fonts Integration
|
||||||
|
|
||||||
|
- Font: **Inter** (from Google Fonts)
|
||||||
|
- Loaded via CSS `@import url('...')`
|
||||||
|
- Fallback: System fonts (`-apple-system, BlinkMacSystemFont, etc.`)
|
||||||
|
- Display: `display=swap` for optimal loading
|
||||||
|
|
||||||
|
### REST API & Customizer
|
||||||
|
|
||||||
|
- Theme mods saved to `wp_options` table
|
||||||
|
- Customizer sections: Branding, Hero Section, Footer, Theme Mode
|
||||||
|
- All settings use `'transport' => 'postMessage'` for instant preview (except dark_mode_default which uses refresh)
|
||||||
|
|
||||||
|
## Key Architectural Decisions
|
||||||
|
|
||||||
|
### 1. Classic Theme (Not FSE/Block Theme)
|
||||||
|
|
||||||
|
**Why:** Peptide Repo predates WordPress Full Site Editing. Classic themes provide:
|
||||||
|
- Simpler, more stable template hierarchy for developers
|
||||||
|
- Full control over HTML structure without block markup
|
||||||
|
- Easier plugin compatibility without block interdependencies
|
||||||
|
- Proven stability for production scientific database
|
||||||
|
|
||||||
|
### 2. Monolithic CSS (Not SASS/LESS)
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- No build step = instant deploy without compilation
|
||||||
|
- Single 44KB file loads fast (gzips to ~7KB)
|
||||||
|
- CSS custom properties (variables) provide all needed abstraction
|
||||||
|
- Theme can be edited directly on remote server if needed
|
||||||
|
- Reduces deployment complexity on Hostinger shared hosting
|
||||||
|
|
||||||
|
### 3. Vanilla JavaScript (No Bundler)
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Simple, maintainable code without build toolchain
|
||||||
|
- Direct browser compatibility (ES6+)
|
||||||
|
- Minimal overhead; only 296 lines total across 2 files
|
||||||
|
- Each script can be tested by running `node --check` syntax validation
|
||||||
|
- No dependencies = no supply chain risk
|
||||||
|
|
||||||
|
### 4. Shortcode Integration (Not Direct Function Calls)
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Plugins are independent; shortcodes decouple content from theme
|
||||||
|
- Shortcodes can be disabled/removed without theme breaking
|
||||||
|
- `shortcode_exists()` provides graceful degradation
|
||||||
|
- Theme doesn't need to know plugin implementation details
|
||||||
|
- Content (front-page.php) stays separate from plugin business logic
|
||||||
|
|
||||||
|
### 5. CSS Custom Properties for Dark Mode & Theming
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Single source of truth for all colors (defined in `:root`)
|
||||||
|
- Plugins can read theme colors via `getComputedStyle()`
|
||||||
|
- Easy to switch between light/dark without duplicate selectors
|
||||||
|
- No need for SASS mixins or preprocessor
|
||||||
|
- Future-proof: standard CSS, works in all modern browsers
|
||||||
|
|
||||||
|
### 6. No `!important` Policy (Global Scale)
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- `!important` breaks plugin CSS specificity
|
||||||
|
- Forces explicit specificity through selector weight instead
|
||||||
|
- Theme respects plugin namespaces (`.pn-*`, `.psa-*`, `.prab-*`)
|
||||||
|
- Only exception: line 85 (necessary override of Peptide News line-clamp)
|
||||||
|
- Maintains CSS maintainability and plugin harmony
|
||||||
|
|
||||||
|
### 7. Inline Dark Mode Script in `wp_head`
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Runs *before* CSS loads, preventing white flash in dark mode
|
||||||
|
- Reads localStorage synchronously (no async needed here)
|
||||||
|
- Sets `data-theme` attribute before first paint
|
||||||
|
- Fallback chain: localStorage → system preference → customizer default
|
||||||
|
- Performance impact: minimal (~1ms execution)
|
||||||
|
|
||||||
|
### 8. Navigation Walker with Active State Detection
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Custom Walker class (line 243-300) detects current-menu-item
|
||||||
|
- Adds `.active` class for CSS styling (matches WordPress conventions)
|
||||||
|
- Enables dynamic nav styling without JS
|
||||||
|
- Integrates with WordPress menus natively
|
||||||
|
- Fallback function for sites with no primary menu
|
||||||
|
|
||||||
|
### 9. Helper Functions for Customizer Output
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- `peptide_starter_get_*()` functions (lines 317-340) centralize theme mod retrieval
|
||||||
|
- Consistent defaults across templates
|
||||||
|
- Easy to update copy site-wide from one place
|
||||||
|
- Separates data layer from presentation
|
||||||
|
|
||||||
|
## Security & Performance Considerations
|
||||||
|
|
||||||
|
- **Escaping:** All output uses `esc_html()`, `esc_url()`, `esc_attr()` appropriately
|
||||||
|
- **Nonces:** Newsletter form uses `wp_nonce_field()` with CSRF verification in handler
|
||||||
|
- **Sanitize callbacks:** All Customizer settings have explicit `sanitize_callback` (text, kses, checkbox)
|
||||||
|
- **Form handling:** Newsletter signup routes through `admin_post` action with nonce + email validation
|
||||||
|
- **Permissions:** Check `is_active_sidebar()` before rendering widgets
|
||||||
|
- **Performance:** Script/style versions use `PEPTIDE_STARTER_VERSION` constant for cache busting
|
||||||
|
- **Accessibility:** Skip-to-main-content link (header.php line 20), WCAG AA color contrast, focus indicators
|
||||||
|
|
||||||
|
## Testing & CI/CD
|
||||||
|
|
||||||
|
- **CI pipeline** (`.github/workflows/deploy.yml`):
|
||||||
|
1. PHP lint (`php -l`) on all PHP files
|
||||||
|
2. PHPCS with WordPress-Core standard
|
||||||
|
3. JS syntax check (`node --check`) on all JS files
|
||||||
|
4. Deploy via rsync (only if validation passes)
|
||||||
|
5. Post-deploy health check (HTTP 200)
|
||||||
|
- **Unit tests** (`tests/`): PHPUnit with WordPress test framework
|
||||||
|
- `test-functions.php` — helpers, sanitizers, filters
|
||||||
|
- `test-theme-setup.php` — theme supports, menus, widgets, scripts
|
||||||
|
- **rsync safety**: Dev-only files excluded from deploy (tests/, vendor/, composer.json, phpunit.xml, .phpcs.xml.dist)
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
- Chrome/Edge (latest 2 versions)
|
||||||
|
- Firefox (latest 2 versions)
|
||||||
|
- Safari (latest 2 versions)
|
||||||
|
- iOS Safari 12+
|
||||||
|
- Chrome Android
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
**Current:** v1.3.1 (defined in `style.css` header line 7 and `functions.php` line 14 constant)
|
||||||
|
|
||||||
|
When releasing a new version, update both locations.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- **CONVENTIONS.md** — How to add templates, components, plugin integrations
|
||||||
|
- **CLAUDE.md** — Developer context and quick reference
|
||||||
|
- **README.md** — User-facing feature list and installation
|
||||||
|
- **CHANGELOG.md** — Release history
|
||||||
157
CHANGELOG.md
Normal file
157
CHANGELOG.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Peptide Starter Theme - Changelog
|
||||||
|
|
||||||
|
All notable changes to the Peptide Starter Theme are documented in this file.
|
||||||
|
|
||||||
|
## [1.3.2] - 2026-04-12
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **Newsletter form nonce:** Added `wp_nonce_field()` and server-side nonce verification to the footer newsletter signup form (CSRF protection)
|
||||||
|
- **Newsletter form handler:** Added `peptide_starter_handle_newsletter_signup()` via `admin_post` action with email sanitization and validation
|
||||||
|
- **Customizer sanitize callbacks:** Added `sanitize_callback` to all five Customizer settings (hero_title, hero_subtitle, hero_search_placeholder, footer_copyright, dark_mode_default) — prevents unsanitized input from being saved
|
||||||
|
- **Checkbox sanitizer:** Added `peptide_starter_sanitize_checkbox()` helper for boolean Customizer values
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **PHPCS in CI:** WordPress-Core coding standard check added to deploy pipeline (blocks deploy on violations)
|
||||||
|
- **JS syntax check in CI:** `node --check` validation on all JS files in `assets/js/`
|
||||||
|
- **PHPUnit test scaffold:** Bootstrap, config, and initial test suites covering theme helpers, setup, and sanitizers
|
||||||
|
- **Composer dev dependencies:** PHPUnit 9, PHPCS 3, WPCS 3 for local development
|
||||||
|
- **PHPCS config:** `.phpcs.xml.dist` with WordPress-Core standard, text domain, and PHP/WP version constraints
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Deploy safety:** Added excludes for dev-only files in rsync (tests/, vendor/, composer.json, phpunit.xml, .phpcs.xml.dist) — prevents dev tooling from reaching production
|
||||||
|
- **CI job name:** Renamed from "Validate PHP Syntax" to "Validate PHP & JS" to reflect expanded checks
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Updated ARCHITECTURE.md with testing infrastructure, security details, and CI/CD pipeline description
|
||||||
|
- Updated CHANGELOG.md with this release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **PRAutoBlogger Integration:** Added `[prautoblogger_posts]` shortcode widget to front page
|
||||||
|
- Displays 6 auto-generated research articles in "Latest Research & Insights" section
|
||||||
|
- Appears between hero section and Peptide News feed
|
||||||
|
- Gracefully hides if PRAutoBlogger plugin is not active
|
||||||
|
- Uses `.prab-*` namespace for plugin-specific styling
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Deploy Mechanism:** Switched from GitHub Actions direct build to rsync SSH deployment
|
||||||
|
- No longer requires build step or plugin compilation
|
||||||
|
- Files deployed directly from repository to Hostinger via rsync over SSH
|
||||||
|
- LiteSpeed cache purged automatically after deploy
|
||||||
|
- Theme path on server: `~/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/`
|
||||||
|
- Simpler CI/CD pipeline; no external build dependencies
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- PRAutoBlogger integration uses `shortcode_exists()` for graceful degradation
|
||||||
|
- Deploy workflow (`deploy.yml`) now runs rsync directly without intermediate steps
|
||||||
|
- Maintains backward compatibility with all existing features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Dark Mode Toggle:** Full dark mode support with persistent user preference
|
||||||
|
- Toggle button in header (sun/moon icons)
|
||||||
|
- Preference saved to localStorage as `peptide-starter-theme`
|
||||||
|
- Respects system `prefers-color-scheme` preference
|
||||||
|
- Customizer option to set dark mode as default on first visit
|
||||||
|
- Inline script in `wp_head` prevents white flash on page load
|
||||||
|
- Custom `themechange` event dispatched to plugins for dark mode coordination
|
||||||
|
|
||||||
|
- **Plugin Coordination System:** Dark mode and theme state broadcasting
|
||||||
|
- Custom `themechange` DOM event with `{ theme: 'dark' | 'light' }` payload
|
||||||
|
- `html[data-theme]` attribute for CSS selectors
|
||||||
|
- `window.peptideStarterTheme` API for plugin access (getCurrentTheme, setTheme, toggleDarkMode)
|
||||||
|
- CSS custom properties swap automatically on dark mode toggle
|
||||||
|
|
||||||
|
- **Full Plugin Integration Suite:**
|
||||||
|
- Peptide Search AI shortcode: `[peptide_search]` in hero section
|
||||||
|
- Peptide News shortcode: `[peptide_news]` on front page
|
||||||
|
- All plugins respect theme CSS variables and dark mode events
|
||||||
|
- Graceful fallback for missing plugins (search form, empty sections)
|
||||||
|
|
||||||
|
- **Customizer Settings:**
|
||||||
|
- Hero title, subtitle, search placeholder
|
||||||
|
- Footer copyright text
|
||||||
|
- Dark mode default toggle
|
||||||
|
|
||||||
|
- **4-Column Footer Widget Areas:**
|
||||||
|
- Registered: footer-1, footer-2, footer-3, footer-4
|
||||||
|
- Default fallback content per column (About, Links, Resources, Connect)
|
||||||
|
|
||||||
|
- **Template Hierarchy:**
|
||||||
|
- Front page with hero + plugin widgets
|
||||||
|
- Generic page template with headers
|
||||||
|
- CPT support: single-peptide.php, archive-peptide.php
|
||||||
|
- 404 error page
|
||||||
|
- Search template with fallback
|
||||||
|
|
||||||
|
- **Accessibility (WCAG AA):**
|
||||||
|
- Skip-to-main-content link
|
||||||
|
- Proper heading hierarchy (H1-H6)
|
||||||
|
- Focus indicators (2px outline) on all interactive elements
|
||||||
|
- 4.5:1+ contrast ratio on all text
|
||||||
|
- Semantic HTML with proper landmarks
|
||||||
|
- `aria-label` and `aria-expanded` attributes on buttons
|
||||||
|
- Keyboard navigation support (Escape closes overlays, Tab through elements)
|
||||||
|
|
||||||
|
- **Responsive Breakpoints:**
|
||||||
|
- Mobile: 320px - 479px
|
||||||
|
- Tablet: 480px - 767px
|
||||||
|
- Desktop: 768px - 1023px
|
||||||
|
- Large: 1024px - 1439px
|
||||||
|
- Extra Large: 1440px+
|
||||||
|
|
||||||
|
- **Design System:**
|
||||||
|
- Typography: Inter font with 1.125x modular scale, 8px grid base
|
||||||
|
- Colors: Blue primary (#0066CC light, #3B82F6 dark)
|
||||||
|
- Component library: buttons, cards, forms, badges, alerts, pagination
|
||||||
|
- CSS custom properties for all design tokens
|
||||||
|
- No `!important` on global elements (plugin harmony)
|
||||||
|
|
||||||
|
- **Navigation & Mobile Menu:**
|
||||||
|
- Primary navigation with active state detection
|
||||||
|
- Mobile hamburger menu with overlay
|
||||||
|
- Custom Nav Walker for WordPress theme integration
|
||||||
|
- Keyboard navigation (Escape closes menu, arrow keys work)
|
||||||
|
- Responsive: hides on desktop, appears on mobile (< 768px)
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- **Architecture:** Classic WordPress theme (non-block/FSE)
|
||||||
|
- **Styling:** Single 44KB `style.css` with no build step
|
||||||
|
- **JavaScript:** Vanilla ES6+ in 2 files (navigation.js, theme.js)
|
||||||
|
- **Plugin Namespaces:** `.pn-*` (Peptide News), `.psa-*` (Peptide Search AI), `.prab-*` (PRAutoBlogger)
|
||||||
|
- **Browser Support:** Chrome, Firefox, Safari (latest 2), iOS Safari 12+
|
||||||
|
- **Requires:** WordPress 6.0+, PHP 7.4+
|
||||||
|
|
||||||
|
### Initial Release
|
||||||
|
- All core features implemented and tested
|
||||||
|
- Full dark mode support with ecosystem coordination
|
||||||
|
- Production-ready for peptiderepo.com launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Numbering
|
||||||
|
|
||||||
|
- Versions follow semantic versioning: MAJOR.MINOR.PATCH
|
||||||
|
- Current: v1.3.2
|
||||||
|
- Defined in: `style.css` header (line 7) and `functions.php` (line 14 constant)
|
||||||
|
- Update both locations when releasing a new version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Push to `main` branch triggers automatic deployment to peptiderepo.com via GitHub Actions.
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Push to `main`
|
||||||
|
2. GitHub Actions workflow `deploy.yml` runs
|
||||||
|
3. Files synced via rsync to Hostinger
|
||||||
|
4. LiteSpeed cache purged
|
||||||
|
|
||||||
|
No manual FTP uploads needed.
|
||||||
23
composer.json
Normal file
23
composer.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "peptiderepo/peptide-starter-theme",
|
||||||
|
"description": "WordPress theme for peptiderepo.com",
|
||||||
|
"type": "wordpress-theme",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.0",
|
||||||
|
"wp-coding-standards/wpcs": "^3.0",
|
||||||
|
"phpcsstandards/phpcsutils": "^1.0",
|
||||||
|
"yoast/phpunit-polyfills": "^2.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "phpcs",
|
||||||
|
"lint:fix": "phpcbf",
|
||||||
|
"test": "phpunit"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
footer.php
162
footer.php
@@ -1,80 +1,82 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* The footer for Peptide Starter theme
|
* The footer for Peptide Starter theme
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
?>
|
?>
|
||||||
</main><!-- #main -->
|
</main><!-- #main -->
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="footer-wrapper">
|
<div class="footer-wrapper">
|
||||||
<!-- Newsletter Signup -->
|
<!-- Newsletter Signup -->
|
||||||
<?php if ( peptide_starter_show_newsletter_form() ) { ?>
|
<?php if ( peptide_starter_show_newsletter_form() ) { ?>
|
||||||
<div class="footer-newsletter">
|
<div class="footer-newsletter">
|
||||||
<h3><?php esc_html_e( 'Stay Updated', 'peptide-starter' ); ?></h3>
|
<h3><?php esc_html_e( 'Stay Updated', 'peptide-starter' ); ?></h3>
|
||||||
<p><?php esc_html_e( 'Subscribe to our newsletter for the latest peptide research and updates.', 'peptide-starter' ); ?></p>
|
<p><?php esc_html_e( 'Subscribe to our newsletter for the latest peptide research and updates.', 'peptide-starter' ); ?></p>
|
||||||
<form class="newsletter-form" action="#" method="post">
|
<form class="newsletter-form" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
|
||||||
<input
|
<?php wp_nonce_field( 'peptide_starter_newsletter', 'ps_newsletter_nonce' ); ?>
|
||||||
type="email"
|
<input type="hidden" name="action" value="peptide_starter_newsletter_signup">
|
||||||
name="email"
|
<input
|
||||||
class="newsletter-input"
|
type="email"
|
||||||
placeholder="<?php esc_attr_e( 'your@email.com', 'peptide-starter' ); ?>"
|
name="email"
|
||||||
required
|
class="newsletter-input"
|
||||||
>
|
placeholder="<?php esc_attr_e( 'your@email.com', 'peptide-starter' ); ?>"
|
||||||
<button type="submit" class="ps-btn ps-btn-primary">
|
required
|
||||||
<?php esc_html_e( 'Subscribe', 'peptide-starter' ); ?>
|
>
|
||||||
</button>
|
<button type="submit" class="ps-btn ps-btn-primary">
|
||||||
</form>
|
<?php esc_html_e( 'Subscribe', 'peptide-starter' ); ?>
|
||||||
</div>
|
</button>
|
||||||
<?php } ?>
|
</form>
|
||||||
|
</div>
|
||||||
<!-- Footer Grid -->
|
<?php } ?>
|
||||||
<div class="footer-grid">
|
|
||||||
<!-- Footer Widget Areas -->
|
<!-- Footer Grid -->
|
||||||
<?php
|
<div class="footer-grid">
|
||||||
for ( $i = 1; $i <= 4; $i++ ) {
|
<!-- Footer Widget Areas -->
|
||||||
echo '<div class="footer-section">';
|
<?php
|
||||||
if ( is_active_sidebar( 'footer-' . $i ) ) {
|
for ( $i = 1; $i <= 4; $i++ ) {
|
||||||
dynamic_sidebar( 'footer-' . $i );
|
echo '<div class="footer-section">';
|
||||||
} else {
|
if ( is_active_sidebar( 'footer-' . $i ) ) {
|
||||||
// Default footer content if no widgets
|
dynamic_sidebar( 'footer-' . $i );
|
||||||
if ( 1 === $i ) {
|
} else {
|
||||||
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
|
// Default footer content if no widgets
|
||||||
echo '<p>' . esc_html__( 'Peptide Repo is a comprehensive scientific database for peptide research.', 'peptide-starter' ) . '</p>';
|
if ( 1 === $i ) {
|
||||||
} elseif ( 2 === $i ) {
|
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>';
|
echo '<p>' . esc_html__( 'Peptide Repo is a comprehensive scientific database for peptide research.', 'peptide-starter' ) . '</p>';
|
||||||
echo '<ul>';
|
} elseif ( 2 === $i ) {
|
||||||
echo '<li><a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html__( 'Home', 'peptide-starter' ) . '</a></li>';
|
echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<li><a href="' . esc_url( home_url( '/peptides' ) ) . '">' . esc_html__( 'Peptides Database', 'peptide-starter' ) . '</a></li>';
|
echo '<ul>';
|
||||||
echo '</ul>';
|
echo '<li><a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html__( 'Home', 'peptide-starter' ) . '</a></li>';
|
||||||
} elseif ( 3 === $i ) {
|
echo '<li><a href="' . esc_url( home_url( '/peptides' ) ) . '">' . esc_html__( 'Peptides Database', 'peptide-starter' ) . '</a></li>';
|
||||||
echo '<h3>' . esc_html__( 'Resources', 'peptide-starter' ) . '</h3>';
|
echo '</ul>';
|
||||||
echo '<ul>';
|
} elseif ( 3 === $i ) {
|
||||||
echo '<li><a href="#">' . esc_html__( 'Research', 'peptide-starter' ) . '</a></li>';
|
echo '<h3>' . esc_html__( 'Resources', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<li><a href="#">' . esc_html__( 'Documentation', 'peptide-starter' ) . '</a></li>';
|
echo '<ul>';
|
||||||
echo '</ul>';
|
echo '<li><a href="#">' . esc_html__( 'Research', 'peptide-starter' ) . '</a></li>';
|
||||||
} elseif ( 4 === $i ) {
|
echo '<li><a href="#">' . esc_html__( 'Documentation', 'peptide-starter' ) . '</a></li>';
|
||||||
echo '<h3>' . esc_html__( 'Connect', 'peptide-starter' ) . '</h3>';
|
echo '</ul>';
|
||||||
echo '<ul>';
|
} elseif ( 4 === $i ) {
|
||||||
echo '<li><a href="https://github.com/peptiderepo" target="_blank" rel="noopener">' . esc_html__( 'GitHub', 'peptide-starter' ) . '</a></li>';
|
echo '<h3>' . esc_html__( 'Connect', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<li><a href="#">' . esc_html__( 'Twitter', 'peptide-starter' ) . '</a></li>';
|
echo '<ul>';
|
||||||
echo '</ul>';
|
echo '<li><a href="https://github.com/peptiderepo" target="_blank" rel="noopener">' . esc_html__( 'GitHub', 'peptide-starter' ) . '</a></li>';
|
||||||
}
|
echo '<li><a href="#">' . esc_html__( 'Twitter', 'peptide-starter' ) . '</a></li>';
|
||||||
}
|
echo '</ul>';
|
||||||
echo '</div>';
|
}
|
||||||
}
|
}
|
||||||
?>
|
echo '</div>';
|
||||||
</div>
|
}
|
||||||
|
?>
|
||||||
<!-- Footer Bottom -->
|
</div>
|
||||||
<div class="footer-bottom">
|
|
||||||
<p><?php echo wp_kses_post( peptide_starter_get_footer_copyright() ); ?></p>
|
<!-- Footer Bottom -->
|
||||||
</div>
|
<div class="footer-bottom">
|
||||||
</div>
|
<p><?php echo wp_kses_post( peptide_starter_get_footer_copyright() ); ?></p>
|
||||||
</footer>
|
</div>
|
||||||
|
</div>
|
||||||
<?php wp_footer(); ?>
|
</footer>
|
||||||
</body>
|
|
||||||
</html>
|
<?php wp_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
918
functions.php
918
functions.php
@@ -1,429 +1,489 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Peptide Starter Theme Functions
|
* Peptide Starter Theme Functions
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Exit if accessed directly
|
// Exit if accessed directly
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define constants
|
// Define constants
|
||||||
define( 'PEPTIDE_STARTER_VERSION', '1.3.1' );
|
define( 'PEPTIDE_STARTER_VERSION', '1.3.2' );
|
||||||
define( 'PEPTIDE_STARTER_DIR', get_template_directory() );
|
define( 'PEPTIDE_STARTER_DIR', get_template_directory() );
|
||||||
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
|
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up theme defaults and register support for various WordPress features
|
* Set up theme defaults and register support for various WordPress features
|
||||||
*/
|
*/
|
||||||
function peptide_starter_setup() {
|
function peptide_starter_setup() {
|
||||||
// Add theme support
|
// Add theme support
|
||||||
add_theme_support( 'title-tag' );
|
add_theme_support( 'title-tag' );
|
||||||
add_theme_support( 'post-thumbnails' );
|
add_theme_support( 'post-thumbnails' );
|
||||||
add_theme_support( 'responsive-embeds' );
|
add_theme_support( 'responsive-embeds' );
|
||||||
add_theme_support( 'custom-logo' );
|
add_theme_support( 'custom-logo' );
|
||||||
add_theme_support( 'html5', array(
|
add_theme_support( 'html5', array(
|
||||||
'search-form',
|
'search-form',
|
||||||
'comment-form',
|
'comment-form',
|
||||||
'comment-list',
|
'comment-list',
|
||||||
'gallery',
|
'gallery',
|
||||||
'caption',
|
'caption',
|
||||||
'style',
|
'style',
|
||||||
'script',
|
'script',
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Register navigation menus
|
// Register navigation menus
|
||||||
register_nav_menus( array(
|
register_nav_menus( array(
|
||||||
'primary' => esc_html__( 'Primary Menu', 'peptide-starter' ),
|
'primary' => esc_html__( 'Primary Menu', 'peptide-starter' ),
|
||||||
'footer' => esc_html__( 'Footer Menu', 'peptide-starter' ),
|
'footer' => esc_html__( 'Footer Menu', 'peptide-starter' ),
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Add support for wide blocks
|
// Add support for wide blocks
|
||||||
add_theme_support( 'align-wide' );
|
add_theme_support( 'align-wide' );
|
||||||
|
|
||||||
// Load text domain
|
// Load text domain
|
||||||
load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' );
|
load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' );
|
||||||
}
|
}
|
||||||
add_action( 'after_setup_theme', 'peptide_starter_setup' );
|
add_action( 'after_setup_theme', 'peptide_starter_setup' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and enqueue stylesheets and scripts
|
* Register and enqueue stylesheets and scripts
|
||||||
*/
|
*/
|
||||||
function peptide_starter_scripts() {
|
function peptide_starter_scripts() {
|
||||||
// Register and enqueue main stylesheet
|
// Register and enqueue main stylesheet
|
||||||
wp_enqueue_style( 'peptide-starter-style', PEPTIDE_STARTER_URI . '/style.css', array(), PEPTIDE_STARTER_VERSION );
|
wp_enqueue_style( 'peptide-starter-style', PEPTIDE_STARTER_URI . '/style.css', array(), PEPTIDE_STARTER_VERSION );
|
||||||
|
|
||||||
// Add conditional scripts
|
// Add conditional scripts
|
||||||
wp_enqueue_script( 'peptide-starter-navigation', PEPTIDE_STARTER_URI . '/assets/js/navigation.js', array(), PEPTIDE_STARTER_VERSION, true );
|
wp_enqueue_script( 'peptide-starter-navigation', PEPTIDE_STARTER_URI . '/assets/js/navigation.js', array(), PEPTIDE_STARTER_VERSION, true );
|
||||||
wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true );
|
wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true );
|
||||||
|
|
||||||
// Pass PHP data to JavaScript
|
// Pass PHP data to JavaScript
|
||||||
wp_localize_script( 'peptide-starter-theme', 'peptideStarterData', array(
|
wp_localize_script( 'peptide-starter-theme', 'peptideStarterData', array(
|
||||||
'siteUrl' => home_url( '/' ),
|
'siteUrl' => home_url( '/' ),
|
||||||
'isDarkMode' => get_theme_mod( 'dark_mode_default', false ),
|
'isDarkMode' => get_theme_mod( 'dark_mode_default', false ),
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Dequeue comment reply script if comments are not open
|
// Dequeue comment reply script if comments are not open
|
||||||
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
|
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
|
||||||
wp_enqueue_script( 'comment-reply' );
|
wp_enqueue_script( 'comment-reply' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add_action( 'wp_enqueue_scripts', 'peptide_starter_scripts' );
|
add_action( 'wp_enqueue_scripts', 'peptide_starter_scripts' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue plugin override styles at a late priority so they load AFTER plugin stylesheets.
|
* Enqueue plugin override styles at a late priority so they load AFTER plugin stylesheets.
|
||||||
* This ensures theme overrides (e.g. removing line-clamp on article excerpts) beat plugin CSS.
|
* This ensures theme overrides (e.g. removing line-clamp on article excerpts) beat plugin CSS.
|
||||||
*/
|
*/
|
||||||
function peptide_starter_plugin_overrides() {
|
function peptide_starter_plugin_overrides() {
|
||||||
wp_add_inline_style( 'peptide-starter-style', '
|
wp_add_inline_style( 'peptide-starter-style', '
|
||||||
/* Override Peptide News plugin line-clamp — show full summaries */
|
/* Override Peptide News plugin line-clamp — show full summaries */
|
||||||
p.pn-article-excerpt,
|
p.pn-article-excerpt,
|
||||||
.pn-article-card p.pn-article-excerpt,
|
.pn-article-card p.pn-article-excerpt,
|
||||||
body .pn-article-excerpt {
|
body .pn-article-excerpt {
|
||||||
-webkit-line-clamp: unset !important;
|
-webkit-line-clamp: unset !important;
|
||||||
-webkit-box-orient: unset !important;
|
-webkit-box-orient: unset !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
' );
|
' );
|
||||||
}
|
}
|
||||||
add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 );
|
add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register widget areas
|
* Register widget areas
|
||||||
*/
|
*/
|
||||||
function peptide_starter_widgets_init() {
|
function peptide_starter_widgets_init() {
|
||||||
register_sidebar( array(
|
register_sidebar( array(
|
||||||
'name' => esc_html__( 'Footer - Column 1', 'peptide-starter' ),
|
'name' => esc_html__( 'Footer - Column 1', 'peptide-starter' ),
|
||||||
'id' => 'footer-1',
|
'id' => 'footer-1',
|
||||||
'description' => esc_html__( 'Footer widget area 1', 'peptide-starter' ),
|
'description' => esc_html__( 'Footer widget area 1', 'peptide-starter' ),
|
||||||
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
||||||
'after_widget' => '</div>',
|
'after_widget' => '</div>',
|
||||||
'before_title' => '<h3 class="widget-title">',
|
'before_title' => '<h3 class="widget-title">',
|
||||||
'after_title' => '</h3>',
|
'after_title' => '</h3>',
|
||||||
) );
|
) );
|
||||||
|
|
||||||
register_sidebar( array(
|
register_sidebar( array(
|
||||||
'name' => esc_html__( 'Footer - Column 2', 'peptide-starter' ),
|
'name' => esc_html__( 'Footer - Column 2', 'peptide-starter' ),
|
||||||
'id' => 'footer-2',
|
'id' => 'footer-2',
|
||||||
'description' => esc_html__( 'Footer widget area 2', 'peptide-starter' ),
|
'description' => esc_html__( 'Footer widget area 2', 'peptide-starter' ),
|
||||||
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
||||||
'after_widget' => '</div>',
|
'after_widget' => '</div>',
|
||||||
'before_title' => '<h3 class="widget-title">',
|
'before_title' => '<h3 class="widget-title">',
|
||||||
'after_title' => '</h3>',
|
'after_title' => '</h3>',
|
||||||
) );
|
) );
|
||||||
|
|
||||||
register_sidebar( array(
|
register_sidebar( array(
|
||||||
'name' => esc_html__( 'Footer - Column 3', 'peptide-starter' ),
|
'name' => esc_html__( 'Footer - Column 3', 'peptide-starter' ),
|
||||||
'id' => 'footer-3',
|
'id' => 'footer-3',
|
||||||
'description' => esc_html__( 'Footer widget area 3', 'peptide-starter' ),
|
'description' => esc_html__( 'Footer widget area 3', 'peptide-starter' ),
|
||||||
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
||||||
'after_widget' => '</div>',
|
'after_widget' => '</div>',
|
||||||
'before_title' => '<h3 class="widget-title">',
|
'before_title' => '<h3 class="widget-title">',
|
||||||
'after_title' => '</h3>',
|
'after_title' => '</h3>',
|
||||||
) );
|
) );
|
||||||
|
|
||||||
register_sidebar( array(
|
register_sidebar( array(
|
||||||
'name' => esc_html__( 'Footer - Column 4', 'peptide-starter' ),
|
'name' => esc_html__( 'Footer - Column 4', 'peptide-starter' ),
|
||||||
'id' => 'footer-4',
|
'id' => 'footer-4',
|
||||||
'description' => esc_html__( 'Footer widget area 4', 'peptide-starter' ),
|
'description' => esc_html__( 'Footer widget area 4', 'peptide-starter' ),
|
||||||
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
|
||||||
'after_widget' => '</div>',
|
'after_widget' => '</div>',
|
||||||
'before_title' => '<h3 class="widget-title">',
|
'before_title' => '<h3 class="widget-title">',
|
||||||
'after_title' => '</h3>',
|
'after_title' => '</h3>',
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
add_action( 'widgets_init', 'peptide_starter_widgets_init' );
|
add_action( 'widgets_init', 'peptide_starter_widgets_init' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register theme customizer options
|
* Register theme customizer options
|
||||||
*/
|
*/
|
||||||
function peptide_starter_customize_register( $wp_customize ) {
|
function peptide_starter_customize_register( $wp_customize ) {
|
||||||
// Logo section
|
// Logo section
|
||||||
$wp_customize->add_section( 'peptide_starter_branding', array(
|
$wp_customize->add_section( 'peptide_starter_branding', array(
|
||||||
'title' => esc_html__( 'Branding', 'peptide-starter' ),
|
'title' => esc_html__( 'Branding', 'peptide-starter' ),
|
||||||
'priority' => 30,
|
'priority' => 30,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Hero settings section
|
// Hero settings section
|
||||||
$wp_customize->add_section( 'peptide_starter_hero', array(
|
$wp_customize->add_section( 'peptide_starter_hero', array(
|
||||||
'title' => esc_html__( 'Hero Section', 'peptide-starter' ),
|
'title' => esc_html__( 'Hero Section', 'peptide-starter' ),
|
||||||
'priority' => 40,
|
'priority' => 40,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Hero Title
|
// Hero Title
|
||||||
$wp_customize->add_setting( 'hero_title', array(
|
$wp_customize->add_setting( 'hero_title', array(
|
||||||
'default' => get_bloginfo( 'name' ),
|
'default' => get_bloginfo( 'name' ),
|
||||||
'transport' => 'postMessage',
|
'transport' => 'postMessage',
|
||||||
) );
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
) );
|
||||||
$wp_customize->add_control( 'hero_title', array(
|
|
||||||
'label' => esc_html__( 'Hero Title', 'peptide-starter' ),
|
$wp_customize->add_control( 'hero_title', array(
|
||||||
'section' => 'peptide_starter_hero',
|
'label' => esc_html__( 'Hero Title', 'peptide-starter' ),
|
||||||
'type' => 'text',
|
'section' => 'peptide_starter_hero',
|
||||||
) );
|
'type' => 'text',
|
||||||
|
) );
|
||||||
// Hero Subtitle
|
|
||||||
$wp_customize->add_setting( 'hero_subtitle', array(
|
// Hero Subtitle
|
||||||
'default' => esc_html__( 'A scientific peptide reference database', 'peptide-starter' ),
|
$wp_customize->add_setting( 'hero_subtitle', array(
|
||||||
'transport' => 'postMessage',
|
'default' => esc_html__( 'A scientific peptide reference database', 'peptide-starter' ),
|
||||||
) );
|
'transport' => 'postMessage',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
$wp_customize->add_control( 'hero_subtitle', array(
|
) );
|
||||||
'label' => esc_html__( 'Hero Subtitle', 'peptide-starter' ),
|
|
||||||
'section' => 'peptide_starter_hero',
|
$wp_customize->add_control( 'hero_subtitle', array(
|
||||||
'type' => 'textarea',
|
'label' => esc_html__( 'Hero Subtitle', 'peptide-starter' ),
|
||||||
) );
|
'section' => 'peptide_starter_hero',
|
||||||
|
'type' => 'textarea',
|
||||||
// Hero Search Placeholder
|
) );
|
||||||
$wp_customize->add_setting( 'hero_search_placeholder', array(
|
|
||||||
'default' => esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ),
|
// Hero Search Placeholder
|
||||||
'transport' => 'postMessage',
|
$wp_customize->add_setting( 'hero_search_placeholder', array(
|
||||||
) );
|
'default' => esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ),
|
||||||
|
'transport' => 'postMessage',
|
||||||
$wp_customize->add_control( 'hero_search_placeholder', array(
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
'label' => esc_html__( 'Search Placeholder', 'peptide-starter' ),
|
) );
|
||||||
'section' => 'peptide_starter_hero',
|
|
||||||
'type' => 'text',
|
$wp_customize->add_control( 'hero_search_placeholder', array(
|
||||||
) );
|
'label' => esc_html__( 'Search Placeholder', 'peptide-starter' ),
|
||||||
|
'section' => 'peptide_starter_hero',
|
||||||
// Footer section
|
'type' => 'text',
|
||||||
$wp_customize->add_section( 'peptide_starter_footer', array(
|
) );
|
||||||
'title' => esc_html__( 'Footer', 'peptide-starter' ),
|
|
||||||
'priority' => 50,
|
// Footer section
|
||||||
) );
|
$wp_customize->add_section( 'peptide_starter_footer', array(
|
||||||
|
'title' => esc_html__( 'Footer', 'peptide-starter' ),
|
||||||
// Footer Copyright Text
|
'priority' => 50,
|
||||||
$wp_customize->add_setting( 'footer_copyright', array(
|
) );
|
||||||
'default' => esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ),
|
|
||||||
'transport' => 'postMessage',
|
// Footer Copyright Text — allows safe HTML (links, bold, em) via wp_kses_post
|
||||||
) );
|
$wp_customize->add_setting( 'footer_copyright', array(
|
||||||
|
'default' => esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ),
|
||||||
$wp_customize->add_control( 'footer_copyright', array(
|
'transport' => 'postMessage',
|
||||||
'label' => esc_html__( 'Copyright Text', 'peptide-starter' ),
|
'sanitize_callback' => 'wp_kses_post',
|
||||||
'section' => 'peptide_starter_footer',
|
) );
|
||||||
'type' => 'textarea',
|
|
||||||
) );
|
$wp_customize->add_control( 'footer_copyright', array(
|
||||||
|
'label' => esc_html__( 'Copyright Text', 'peptide-starter' ),
|
||||||
// Dark mode section
|
'section' => 'peptide_starter_footer',
|
||||||
$wp_customize->add_section( 'peptide_starter_theme_mode', array(
|
'type' => 'textarea',
|
||||||
'title' => esc_html__( 'Theme Mode', 'peptide-starter' ),
|
) );
|
||||||
'priority' => 60,
|
|
||||||
) );
|
// Dark mode section
|
||||||
|
$wp_customize->add_section( 'peptide_starter_theme_mode', array(
|
||||||
// Dark mode default
|
'title' => esc_html__( 'Theme Mode', 'peptide-starter' ),
|
||||||
$wp_customize->add_setting( 'dark_mode_default', array(
|
'priority' => 60,
|
||||||
'default' => false,
|
) );
|
||||||
'transport' => 'refresh',
|
|
||||||
) );
|
// Dark mode default
|
||||||
|
$wp_customize->add_setting( 'dark_mode_default', array(
|
||||||
$wp_customize->add_control( 'dark_mode_default', array(
|
'default' => false,
|
||||||
'label' => esc_html__( 'Enable Dark Mode by Default', 'peptide-starter' ),
|
'transport' => 'refresh',
|
||||||
'section' => 'peptide_starter_theme_mode',
|
'sanitize_callback' => 'peptide_starter_sanitize_checkbox',
|
||||||
'type' => 'checkbox',
|
) );
|
||||||
) );
|
|
||||||
}
|
$wp_customize->add_control( 'dark_mode_default', array(
|
||||||
add_action( 'customize_register', 'peptide_starter_customize_register' );
|
'label' => esc_html__( 'Enable Dark Mode by Default', 'peptide-starter' ),
|
||||||
|
'section' => 'peptide_starter_theme_mode',
|
||||||
/**
|
'type' => 'checkbox',
|
||||||
* Add custom logo support
|
) );
|
||||||
*/
|
}
|
||||||
function peptide_starter_get_custom_logo() {
|
add_action( 'customize_register', 'peptide_starter_customize_register' );
|
||||||
if ( function_exists( 'get_custom_logo' ) ) {
|
|
||||||
return get_custom_logo();
|
/**
|
||||||
}
|
* Sanitize checkbox values for the Customizer.
|
||||||
return '';
|
*
|
||||||
}
|
* Returns true only for truthy values; everything else becomes false.
|
||||||
|
* Used as sanitize_callback for the dark_mode_default setting.
|
||||||
/**
|
*
|
||||||
* Navigation Menu Walker for handling active states
|
* @param mixed $checked The raw value from the Customizer.
|
||||||
*/
|
* @return bool True if checked, false otherwise.
|
||||||
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
|
*/
|
||||||
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
|
function peptide_starter_sanitize_checkbox( $checked ) {
|
||||||
if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
|
return ( ( isset( $checked ) && true === $checked ) ? true : false );
|
||||||
$t = '';
|
}
|
||||||
$n = '';
|
|
||||||
} else {
|
/**
|
||||||
$t = "\t";
|
* Add custom logo support
|
||||||
$n = "\n";
|
*/
|
||||||
}
|
function peptide_starter_get_custom_logo() {
|
||||||
$indent = ( $depth ) ? str_repeat( $t, $depth ) : '';
|
if ( function_exists( 'get_custom_logo' ) ) {
|
||||||
|
return get_custom_logo();
|
||||||
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
|
}
|
||||||
$classes[] = 'menu-item-' . $item->ID;
|
return '';
|
||||||
|
}
|
||||||
if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) {
|
|
||||||
$classes[] = 'active';
|
/**
|
||||||
}
|
* Navigation Menu Walker for handling active states
|
||||||
|
*/
|
||||||
$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
|
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
|
||||||
|
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
|
||||||
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
|
if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
|
||||||
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
|
$t = '';
|
||||||
|
$n = '';
|
||||||
$id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
|
} else {
|
||||||
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
|
$t = "\t";
|
||||||
|
$n = "\n";
|
||||||
$output .= $indent . '<li' . $id . $class_names . '>';
|
}
|
||||||
|
$indent = ( $depth ) ? str_repeat( $t, $depth ) : '';
|
||||||
$atts = array();
|
|
||||||
$atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : '';
|
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
|
||||||
$atts['target'] = ! empty( $item->target ) ? $item->target : '';
|
$classes[] = 'menu-item-' . $item->ID;
|
||||||
$atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : '';
|
|
||||||
$atts['href'] = ! empty( $item->url ) ? $item->url : '';
|
if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) {
|
||||||
|
$classes[] = 'active';
|
||||||
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
|
}
|
||||||
|
|
||||||
$attributes = '';
|
$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
|
||||||
foreach ( $atts as $attr => $value ) {
|
|
||||||
if ( ! empty( $value ) ) {
|
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
|
||||||
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
|
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
|
||||||
$attributes .= ' ' . $attr . '="' . $value . '"';
|
|
||||||
}
|
$id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
|
||||||
}
|
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
|
||||||
|
|
||||||
$title = apply_filters( 'nav_menu_item_title', $item->title, $item, $args, $depth );
|
$output .= $indent . '<li' . $id . $class_names . '>';
|
||||||
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
|
|
||||||
|
$atts = array();
|
||||||
$link = $args->before;
|
$atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : '';
|
||||||
$link .= '<a' . $attributes . '>';
|
$atts['target'] = ! empty( $item->target ) ? $item->target : '';
|
||||||
$link .= $args->link_before . $title . $args->link_after;
|
$atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : '';
|
||||||
$link .= '</a>';
|
$atts['href'] = ! empty( $item->url ) ? $item->url : '';
|
||||||
$link .= $args->after;
|
|
||||||
|
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
|
||||||
$output .= apply_filters( 'nav_menu_item_title', $link, $item, $args, $depth );
|
|
||||||
|
$attributes = '';
|
||||||
$output .= '</li>' . $n;
|
foreach ( $atts as $attr => $value ) {
|
||||||
}
|
if ( ! empty( $value ) ) {
|
||||||
}
|
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
|
||||||
|
$attributes .= ' ' . $attr . '="' . $value . '"';
|
||||||
/**
|
}
|
||||||
* Display custom logo
|
}
|
||||||
*/
|
|
||||||
function peptide_starter_the_custom_logo() {
|
$title = apply_filters( 'nav_menu_item_title', $item->title, $item, $args, $depth );
|
||||||
if ( function_exists( 'the_custom_logo' ) ) {
|
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
|
||||||
the_custom_logo();
|
|
||||||
} else {
|
$link = $args->before;
|
||||||
$site_name = get_bloginfo( 'name' );
|
$link .= '<a' . $attributes . '>';
|
||||||
echo '<a class="site-logo" href="' . esc_url( home_url( '/' ) ) . '" rel="home">' . esc_html( $site_name ) . '</a>';
|
$link .= $args->link_before . $title . $args->link_after;
|
||||||
}
|
$link .= '</a>';
|
||||||
}
|
$link .= $args->after;
|
||||||
|
|
||||||
/**
|
$output .= apply_filters( 'nav_menu_item_title', $link, $item, $args, $depth );
|
||||||
* Get hero title
|
|
||||||
*/
|
$output .= '</li>' . $n;
|
||||||
function peptide_starter_get_hero_title() {
|
}
|
||||||
return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Display custom logo
|
||||||
* Get hero subtitle
|
*/
|
||||||
*/
|
function peptide_starter_the_custom_logo() {
|
||||||
function peptide_starter_get_hero_subtitle() {
|
if ( function_exists( 'the_custom_logo' ) ) {
|
||||||
return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
|
the_custom_logo();
|
||||||
}
|
} else {
|
||||||
|
$site_name = get_bloginfo( 'name' );
|
||||||
/**
|
echo '<a class="site-logo" href="' . esc_url( home_url( '/' ) ) . '" rel="home">' . esc_html( $site_name ) . '</a>';
|
||||||
* Get search placeholder
|
}
|
||||||
*/
|
}
|
||||||
function peptide_starter_get_search_placeholder() {
|
|
||||||
return get_theme_mod( 'hero_search_placeholder', esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ) );
|
/**
|
||||||
}
|
* Get hero title
|
||||||
|
*/
|
||||||
/**
|
function peptide_starter_get_hero_title() {
|
||||||
* Get footer copyright text
|
return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
|
||||||
*/
|
}
|
||||||
function peptide_starter_get_footer_copyright() {
|
|
||||||
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
|
/**
|
||||||
}
|
* Get hero subtitle
|
||||||
|
*/
|
||||||
/**
|
function peptide_starter_get_hero_subtitle() {
|
||||||
* Apply dark mode class to body if enabled
|
return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
|
||||||
*/
|
}
|
||||||
function peptide_starter_dark_mode_body_class( $classes ) {
|
|
||||||
if ( get_theme_mod( 'dark_mode_default', false ) ) {
|
/**
|
||||||
$classes[] = 'dark-mode-default';
|
* Get search placeholder
|
||||||
}
|
*/
|
||||||
return $classes;
|
function peptide_starter_get_search_placeholder() {
|
||||||
}
|
return get_theme_mod( 'hero_search_placeholder', esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ) );
|
||||||
add_filter( 'body_class', 'peptide_starter_dark_mode_body_class' );
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register pagination function
|
* Get footer copyright text
|
||||||
*/
|
*/
|
||||||
function peptide_starter_pagination() {
|
function peptide_starter_get_footer_copyright() {
|
||||||
global $wp_query;
|
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
|
||||||
|
}
|
||||||
if ( $wp_query->max_num_pages <= 1 ) {
|
|
||||||
return;
|
/**
|
||||||
}
|
* Apply dark mode class to body if enabled
|
||||||
|
*/
|
||||||
$current_page = max( 1, get_query_var( 'paged' ) );
|
function peptide_starter_dark_mode_body_class( $classes ) {
|
||||||
$total_pages = $wp_query->max_num_pages;
|
if ( get_theme_mod( 'dark_mode_default', false ) ) {
|
||||||
|
$classes[] = 'dark-mode-default';
|
||||||
echo '<nav class="ps-pagination">';
|
}
|
||||||
echo paginate_links( array(
|
return $classes;
|
||||||
'base' => get_pagenum_link( 1 ) . '%_%',
|
}
|
||||||
'format' => 'page/%#%/',
|
add_filter( 'body_class', 'peptide_starter_dark_mode_body_class' );
|
||||||
'current' => $current_page,
|
|
||||||
'total' => $total_pages,
|
/**
|
||||||
'prev_text' => esc_html__( '← Previous', 'peptide-starter' ),
|
* Register pagination function
|
||||||
'next_text' => esc_html__( 'Next →', 'peptide-starter' ),
|
*/
|
||||||
'type' => 'plain',
|
function peptide_starter_pagination() {
|
||||||
) );
|
global $wp_query;
|
||||||
echo '</nav>';
|
|
||||||
}
|
if ( $wp_query->max_num_pages <= 1 ) {
|
||||||
|
return;
|
||||||
/**
|
}
|
||||||
* Get the excerpt
|
|
||||||
*/
|
$current_page = max( 1, get_query_var( 'paged' ) );
|
||||||
function peptide_starter_get_excerpt( $post_id = 0, $length = 20 ) {
|
$total_pages = $wp_query->max_num_pages;
|
||||||
if ( ! $post_id ) {
|
|
||||||
$post_id = get_the_ID();
|
echo '<nav class="ps-pagination">';
|
||||||
}
|
echo paginate_links( array(
|
||||||
|
'base' => get_pagenum_link( 1 ) . '%_%',
|
||||||
$excerpt = get_the_excerpt( $post_id );
|
'format' => 'page/%#%/',
|
||||||
|
'current' => $current_page,
|
||||||
if ( ! $excerpt ) {
|
'total' => $total_pages,
|
||||||
$excerpt = wp_trim_words( get_the_content( $post_id ), $length );
|
'prev_text' => esc_html__( '← Previous', 'peptide-starter' ),
|
||||||
}
|
'next_text' => esc_html__( 'Next →', 'peptide-starter' ),
|
||||||
|
'type' => 'plain',
|
||||||
return $excerpt;
|
) );
|
||||||
}
|
echo '</nav>';
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Check if we should show footer newsletter signup
|
/**
|
||||||
*/
|
* Get the excerpt
|
||||||
function peptide_starter_show_newsletter_form() {
|
*/
|
||||||
return apply_filters( 'peptide_starter_show_newsletter', true );
|
function peptide_starter_get_excerpt( $post_id = 0, $length = 20 ) {
|
||||||
}
|
if ( ! $post_id ) {
|
||||||
|
$post_id = get_the_ID();
|
||||||
/**
|
}
|
||||||
* Enqueue inline script for dark mode toggle (output in footer)
|
|
||||||
*/
|
$excerpt = get_the_excerpt( $post_id );
|
||||||
function peptide_starter_inline_dark_mode_script() {
|
|
||||||
$default_dark = get_theme_mod( 'dark_mode_default', false ) ? 'dark' : 'light';
|
if ( ! $excerpt ) {
|
||||||
?>
|
$excerpt = wp_trim_words( get_the_content( $post_id ), $length );
|
||||||
<script>
|
}
|
||||||
(function() {
|
|
||||||
const html = document.documentElement;
|
return $excerpt;
|
||||||
const stored = localStorage.getItem('peptide-starter-theme');
|
}
|
||||||
const default_theme = <?php echo wp_json_encode( $default_dark ); ?>;
|
|
||||||
const system_dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
/**
|
||||||
|
* Check if we should show footer newsletter signup
|
||||||
let theme = stored || (system_dark ? 'dark' : default_theme);
|
*/
|
||||||
html.setAttribute('data-theme', theme);
|
function peptide_starter_show_newsletter_form() {
|
||||||
})();
|
return apply_filters( 'peptide_starter_show_newsletter', true );
|
||||||
</script>
|
}
|
||||||
<?php
|
|
||||||
}
|
/**
|
||||||
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
|
* Handle newsletter signup form submission.
|
||||||
|
*
|
||||||
/**
|
* Validates the nonce and email, then fires an action hook so plugins
|
||||||
* Debug helper: check if a plugin is active
|
* can handle the actual subscription logic (e.g. Mailchimp, SendGrid).
|
||||||
*/
|
* If no plugin handles it, redirects back with a generic success message.
|
||||||
function peptide_starter_is_plugin_active( $plugin ) {
|
*
|
||||||
return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true );
|
* @see footer.php — renders the newsletter form
|
||||||
}
|
*/
|
||||||
|
function peptide_starter_handle_newsletter_signup() {
|
||||||
|
// Verify nonce — prevents CSRF attacks.
|
||||||
|
if ( ! isset( $_POST['ps_newsletter_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_newsletter_nonce'] ) ), 'peptide_starter_newsletter' ) ) {
|
||||||
|
wp_die(
|
||||||
|
esc_html__( 'Security check failed. Please try again.', 'peptide-starter' ),
|
||||||
|
esc_html__( 'Error', 'peptide-starter' ),
|
||||||
|
array( 'response' => 403 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||||
|
|
||||||
|
if ( ! is_email( $email ) ) {
|
||||||
|
wp_safe_redirect( add_query_arg( 'ps_newsletter', 'invalid', wp_get_referer() ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a valid newsletter signup is submitted.
|
||||||
|
*
|
||||||
|
* Plugins should hook here to send the email to their mailing list provider.
|
||||||
|
*
|
||||||
|
* @param string $email The validated subscriber email address.
|
||||||
|
*/
|
||||||
|
do_action( 'peptide_starter_newsletter_subscribe', $email );
|
||||||
|
|
||||||
|
wp_safe_redirect( add_query_arg( 'ps_newsletter', 'success', wp_get_referer() ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
add_action( 'admin_post_peptide_starter_newsletter_signup', 'peptide_starter_handle_newsletter_signup' );
|
||||||
|
add_action( 'admin_post_nopriv_peptide_starter_newsletter_signup', 'peptide_starter_handle_newsletter_signup' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue inline script for dark mode toggle (output in footer)
|
||||||
|
*/
|
||||||
|
function peptide_starter_inline_dark_mode_script() {
|
||||||
|
$default_dark = get_theme_mod( 'dark_mode_default', false ) ? 'dark' : 'light';
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const stored = localStorage.getItem('peptide-starter-theme');
|
||||||
|
const default_theme = <?php echo wp_json_encode( $default_dark ); ?>;
|
||||||
|
const system_dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
let theme = stored || (system_dark ? 'dark' : default_theme);
|
||||||
|
html.setAttribute('data-theme', theme);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug helper: check if a plugin is active
|
||||||
|
*/
|
||||||
|
function peptide_starter_is_plugin_active( $plugin ) {
|
||||||
|
return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true );
|
||||||
|
}
|
||||||
|
|||||||
15
phpunit.xml
Normal file
15
phpunit.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<phpunit
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="theme">
|
||||||
|
<directory prefix="test-" suffix=".php">./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
63
tests/bootstrap.php
Normal file
63
tests/bootstrap.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PHPUnit bootstrap file for Peptide Starter theme tests.
|
||||||
|
*
|
||||||
|
* What: Loads WordPress test framework and the theme.
|
||||||
|
* Who calls it: PHPUnit via phpunit.xml bootstrap attribute.
|
||||||
|
* Dependencies: WordPress test suite (WP_TESTS_DIR env var or default path).
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Composer autoloader.
|
||||||
|
$autoloader = dirname( __DIR__ ) . '/vendor/autoload.php';
|
||||||
|
if ( file_exists( $autoloader ) ) {
|
||||||
|
require_once $autoloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to the WordPress test suite.
|
||||||
|
* Set WP_TESTS_DIR env var, or fall back to /tmp/wordpress-tests-lib.
|
||||||
|
* CI pipelines should install the WP test suite before running tests.
|
||||||
|
*/
|
||||||
|
$_tests_dir = getenv( 'WP_TESTS_DIR' );
|
||||||
|
if ( ! $_tests_dir ) {
|
||||||
|
$_tests_dir = '/tmp/wordpress-tests-lib';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the test suite is installed.
|
||||||
|
if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) {
|
||||||
|
echo "WordPress test suite not found at {$_tests_dir}.\n";
|
||||||
|
echo "Run: bash bin/install-wp-tests.sh <db-name> <db-user> <db-pass> [db-host] [wp-version]\n";
|
||||||
|
exit( 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the WP test suite functions.
|
||||||
|
require_once $_tests_dir . '/includes/functions.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the theme so WordPress loads it during test setup.
|
||||||
|
*/
|
||||||
|
function _register_theme() {
|
||||||
|
$theme_dir = dirname( __DIR__ );
|
||||||
|
$theme_name = basename( $theme_dir );
|
||||||
|
|
||||||
|
// Tell WordPress where our theme lives.
|
||||||
|
register_theme_directory( dirname( $theme_dir ) );
|
||||||
|
add_filter(
|
||||||
|
'pre_option_stylesheet',
|
||||||
|
function () use ( $theme_name ) {
|
||||||
|
return $theme_name;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_filter(
|
||||||
|
'pre_option_template',
|
||||||
|
function () use ( $theme_name ) {
|
||||||
|
return $theme_name;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tests_add_filter( 'muplugins_loaded', '_register_theme' );
|
||||||
|
|
||||||
|
// Boot the WP test framework.
|
||||||
|
require $_tests_dir . '/includes/bootstrap.php';
|
||||||
105
tests/test-functions.php
Normal file
105
tests/test-functions.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tests for functions.php helper functions.
|
||||||
|
*
|
||||||
|
* What: Validates theme helper functions return correct values.
|
||||||
|
* Who calls it: PHPUnit via the "theme" test suite.
|
||||||
|
* Dependencies: WordPress test framework, theme functions.php loaded.
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test helper functions defined in functions.php.
|
||||||
|
*/
|
||||||
|
class Test_Theme_Functions extends WP_UnitTestCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the theme version constant is defined and matches semver format.
|
||||||
|
*/
|
||||||
|
public function test_version_constant_defined() {
|
||||||
|
$this->assertTrue( defined( 'PEPTIDE_STARTER_VERSION' ), 'PEPTIDE_STARTER_VERSION must be defined.' );
|
||||||
|
$this->assertMatchesRegularExpression( '/^\d+\.\d+\.\d+$/', PEPTIDE_STARTER_VERSION, 'Version must follow semver (x.y.z).' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify directory constant points to an existing path.
|
||||||
|
*/
|
||||||
|
public function test_dir_constant_defined() {
|
||||||
|
$this->assertTrue( defined( 'PEPTIDE_STARTER_DIR' ), 'PEPTIDE_STARTER_DIR must be defined.' );
|
||||||
|
$this->assertDirectoryExists( PEPTIDE_STARTER_DIR );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero title should return the site name when no customizer override is set.
|
||||||
|
*/
|
||||||
|
public function test_get_hero_title_returns_default() {
|
||||||
|
$title = peptide_starter_get_hero_title();
|
||||||
|
$this->assertNotEmpty( $title, 'Hero title must not be empty.' );
|
||||||
|
$this->assertEquals( get_bloginfo( 'name' ), $title, 'Default hero title should match site name.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero subtitle should return a non-empty string.
|
||||||
|
*/
|
||||||
|
public function test_get_hero_subtitle_returns_default() {
|
||||||
|
$subtitle = peptide_starter_get_hero_subtitle();
|
||||||
|
$this->assertNotEmpty( $subtitle );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search placeholder should return a non-empty string.
|
||||||
|
*/
|
||||||
|
public function test_get_search_placeholder_returns_default() {
|
||||||
|
$placeholder = peptide_starter_get_search_placeholder();
|
||||||
|
$this->assertNotEmpty( $placeholder );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Footer copyright should return a non-empty string.
|
||||||
|
*/
|
||||||
|
public function test_get_footer_copyright_returns_default() {
|
||||||
|
$copyright = peptide_starter_get_footer_copyright();
|
||||||
|
$this->assertNotEmpty( $copyright );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Newsletter form visibility filter should default to true.
|
||||||
|
*/
|
||||||
|
public function test_show_newsletter_form_default_true() {
|
||||||
|
$this->assertTrue( peptide_starter_show_newsletter_form() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Newsletter form visibility can be disabled via filter.
|
||||||
|
*/
|
||||||
|
public function test_show_newsletter_form_filterable() {
|
||||||
|
add_filter( 'peptide_starter_show_newsletter', '__return_false' );
|
||||||
|
$this->assertFalse( peptide_starter_show_newsletter_form() );
|
||||||
|
remove_filter( 'peptide_starter_show_newsletter', '__return_false' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dark mode body class should only be added when dark_mode_default is true.
|
||||||
|
*/
|
||||||
|
public function test_dark_mode_body_class_not_added_by_default() {
|
||||||
|
$classes = peptide_starter_dark_mode_body_class( array() );
|
||||||
|
$this->assertNotContains( 'dark-mode-default', $classes );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize checkbox should return true for truthy input.
|
||||||
|
*/
|
||||||
|
public function test_sanitize_checkbox_truthy() {
|
||||||
|
$this->assertTrue( peptide_starter_sanitize_checkbox( true ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize checkbox should return false for falsy input.
|
||||||
|
*/
|
||||||
|
public function test_sanitize_checkbox_falsy() {
|
||||||
|
$this->assertFalse( peptide_starter_sanitize_checkbox( false ) );
|
||||||
|
$this->assertFalse( peptide_starter_sanitize_checkbox( null ) );
|
||||||
|
$this->assertFalse( peptide_starter_sanitize_checkbox( '' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
72
tests/test-theme-setup.php
Normal file
72
tests/test-theme-setup.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tests for theme setup and registration.
|
||||||
|
*
|
||||||
|
* What: Validates theme supports, nav menus, and widget areas are registered.
|
||||||
|
* Who calls it: PHPUnit via the "theme" test suite.
|
||||||
|
* Dependencies: WordPress test framework, theme functions.php loaded.
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test theme setup: supports, menus, widgets, scripts.
|
||||||
|
*/
|
||||||
|
class Test_Theme_Setup extends WP_UnitTestCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify essential theme supports are registered.
|
||||||
|
*/
|
||||||
|
public function test_theme_supports_registered() {
|
||||||
|
$this->assertTrue( current_theme_supports( 'title-tag' ), 'Theme must support title-tag.' );
|
||||||
|
$this->assertTrue( current_theme_supports( 'post-thumbnails' ), 'Theme must support post-thumbnails.' );
|
||||||
|
$this->assertTrue( current_theme_supports( 'custom-logo' ), 'Theme must support custom-logo.' );
|
||||||
|
$this->assertTrue( current_theme_supports( 'html5' ), 'Theme must support html5.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify navigation menus are registered.
|
||||||
|
*/
|
||||||
|
public function test_nav_menus_registered() {
|
||||||
|
$menus = get_registered_nav_menus();
|
||||||
|
$this->assertArrayHasKey( 'primary', $menus, 'Primary menu must be registered.' );
|
||||||
|
$this->assertArrayHasKey( 'footer', $menus, 'Footer menu must be registered.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify footer widget areas are registered.
|
||||||
|
*/
|
||||||
|
public function test_footer_widget_areas_registered() {
|
||||||
|
global $wp_registered_sidebars;
|
||||||
|
|
||||||
|
for ( $i = 1; $i <= 4; $i++ ) {
|
||||||
|
$this->assertArrayHasKey(
|
||||||
|
'footer-' . $i,
|
||||||
|
$wp_registered_sidebars,
|
||||||
|
"Footer widget area footer-{$i} must be registered."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the custom nav walker class exists.
|
||||||
|
*/
|
||||||
|
public function test_nav_walker_class_exists() {
|
||||||
|
$this->assertTrue(
|
||||||
|
class_exists( 'Peptide_Starter_Nav_Walker' ),
|
||||||
|
'Peptide_Starter_Nav_Walker class must be defined.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify scripts and styles are enqueued on the front end.
|
||||||
|
*/
|
||||||
|
public function test_scripts_and_styles_enqueued() {
|
||||||
|
// Simulate a front-end page load.
|
||||||
|
do_action( 'wp_enqueue_scripts' );
|
||||||
|
|
||||||
|
$this->assertTrue( wp_style_is( 'peptide-starter-style', 'enqueued' ), 'Main stylesheet must be enqueued.' );
|
||||||
|
$this->assertTrue( wp_script_is( 'peptide-starter-navigation', 'enqueued' ), 'Navigation script must be enqueued.' );
|
||||||
|
$this->assertTrue( wp_script_is( 'peptide-starter-theme', 'enqueued' ), 'Theme script must be enqueued.' );
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user