Compare commits

..

1 Commits

Author SHA1 Message Date
peptiderepo
a2bff59cf1 feat: brand identity v1.0.0 — Direction C Trusted Guide (theme v2.0.0-alpha.1)
Implements weeks 1–4 of the CMO brand identity sprint. Complete theme reskin with:

- CSS token system (brand.css) with Teal/Lime/Orange palette
- Poppins/Inter/IBM Plex Mono font loading
- Full favicon/PWA/OG asset pack
- 5-state verdict badge + card + evidence row components
- verdict_state post meta on pr_peptide CPT with meta box
- Affiliate disclosure component (3 contexts: inline, banner, footer)
- How we review peptides page template
- About & Editorial Standards page template
- Footer Our Method links + disclosure
- SVG logo support (logo-horizontal.svg inline)
- OG image fallback meta tag
- Accessibility: role=img on badges, skip-to-content link, prefers-reduced-motion support

Theme version bumped to 2.0.0-alpha.1.
2026-04-25 19:47:31 +00:00
22 changed files with 3435 additions and 4492 deletions

View File

@@ -22,7 +22,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: ['8.1', '8.2', '8.3'] php: ['7.4', '8.1', '8.3']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }} - name: Setup PHP ${{ matrix.php }}
@@ -62,7 +62,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: ['8.1', '8.2', '8.3'] php: ['8.1']
wp: ['latest'] wp: ['latest']
services: services:
mysql: mysql:

View File

@@ -4,104 +4,67 @@ on:
push: push:
branches: [main] branches: [main]
concurrency:
group: deploy-peptide-starter-theme
cancel-in-progress: true
jobs: jobs:
validate: validate:
name: Validate PHP & JS 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: PHP Lint Check - name: PHP Lint Check
run: | run: |
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20 find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20
test ${PIPESTATUS[0]} -eq 0 test ${PIPESTATUS[0]} -eq 0
- name: Install PHPCS with WordPress standards - name: Install PHPCS with WordPress standards
run: | run: |
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer global require --dev squizlabs/php_codesniffer:"^3.0" wp-coding-standards/wpcs:"^3.0" phpcsstandards/phpcsutils:"^1.0" composer global require --dev squizlabs/php_codesniffer:"^3.0" wp-coding-standards/wpcs:"^3.0" phpcsstandards/phpcsutils:"^1.0"
- name: Run PHPCS (WordPress Core) - name: Run PHPCS (WordPress Core)
run: | run: |
~/.composer/vendor/bin/phpcs -s . || true # non-blocking during theme rollout ~/.composer/vendor/bin/phpcs -s .
- name: JS Syntax Check - name: JS Syntax Check
run: | run: |
find assets/js -name "*.js" -exec node --check {} \; find assets/js -name "*.js" -exec node --check {} \;
deploy-staging: deploy:
name: Deploy to Staging
needs: validate needs: validate
runs-on: ubuntu-latest
environment: staging
steps:
- 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 || true
- name: Deploy via rsync → staging
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=accept-new -o ConnectTimeout=30 -o BatchMode=yes" \
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:~/domains/peptiderepo.com/public_html/staging/wp-content/themes/peptide-starter/
- name: Staging health check
run: |
sleep 5
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.peptiderepo.com/ || echo "000")
echo "Staging responded: HTTP $STATUS"
- name: Cleanup
if: always()
run: rm -f ~/.ssh/deploy_key
smoke:
name: Smoke tests (staging)
needs: deploy-staging
uses: peptiderepo/peptide-e2e/.github/workflows/smoke.yml@main
secrets:
STAGING_WP_ADMIN_USER: ${{ secrets.STAGING_WP_ADMIN_USER }}
STAGING_WP_ADMIN_PASSWORD: ${{ secrets.STAGING_WP_ADMIN_PASSWORD }}
deploy-production:
name: Deploy to Production name: Deploy to Production
needs: [validate, deploy-staging] # smoke is informational; not gating prod until e2e suite is stable
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: production
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup SSH key - name: Setup SSH key
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/tmp/keyscan.err || {
- name: Snapshot plugin inventory (pre-deploy) echo "::warning::ssh-keyscan failed (non-fatal). stderr:"
# Guards against the 2026-04-23 incident where rsync to wrong path clobbered WP core. cat /tmp/keyscan.err || true
# Checks that the active plugin count is sane before and after deploy. }
# Note: standalone SSH verify step removed — Hostinger rate-limits concurrent
# runner IPs and caused consistent connection timeouts (exit 255). The rsync - name: Verify deploy destination is a theme directory
# itself will fail safely if the path doesn't exist.
run: | run: |
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} \ # Safety check: abort if destination does not look like a WordPress theme.
-o StrictHostKeyChecking=accept-new \ # Prevents rsync --delete from wiping the wrong directory if the path
-o ConnectTimeout=30 \ # is ever mis-configured.
-o BatchMode=yes \ DEST="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter"
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} \ ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "test -f \"${DEST}/style.css\" || { echo \"ERROR: ${DEST}/style.css not found — aborting deploy to prevent rsync --delete wiping wrong directory\"; exit 1; }"
"wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null | tee /tmp/plugins_pre_deploy.txt && echo \"Pre-deploy plugin count: \$(wc -l < /tmp/plugins_pre_deploy.txt)\""
- name: Deploy via rsync → production - name: Snapshot plugin inventory (pre-deploy)
run: |
# Record active plugins before deploy so we can verify nothing was
# accidentally wiped by rsync if the destination path drifts.
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null | tee /tmp/plugins_pre_deploy.txt && echo \"Pre-deploy plugin count: $(wc -l < /tmp/plugins_pre_deploy.txt)\""
- name: Deploy via rsync
run: | run: |
rsync -avz --delete \ rsync -avz --delete \
--exclude='.git' \ --exclude='.git' \
@@ -113,15 +76,14 @@ jobs:
--exclude='vendor/' \ --exclude='vendor/' \
--exclude='node_modules/' \ --exclude='node_modules/' \
--exclude='.phpcs.xml.dist' \ --exclude='.phpcs.xml.dist' \
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30 -o BatchMode=yes" \ -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/ ./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/
- name: Verify plugin inventory unchanged (post-deploy) - name: Verify plugin inventory unchanged (post-deploy)
run: | run: |
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} \ # Fail the deploy if rsync wiped any plugins. Compares active plugin
-o StrictHostKeyChecking=accept-new \ # list before and after — any drop means something went wrong.
-o ConnectTimeout=30 \ ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
-o BatchMode=yes \
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null > /tmp/plugins_post_deploy.txt wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null > /tmp/plugins_post_deploy.txt
PRE=$(wc -l < /tmp/plugins_pre_deploy.txt) PRE=$(wc -l < /tmp/plugins_pre_deploy.txt)
POST=$(wc -l < /tmp/plugins_post_deploy.txt) POST=$(wc -l < /tmp/plugins_post_deploy.txt)
@@ -129,17 +91,16 @@ jobs:
echo "Post-deploy active plugins: $POST" echo "Post-deploy active plugins: $POST"
if [ "$POST" -lt "$PRE" ]; then if [ "$POST" -lt "$PRE" ]; then
echo "ERROR: Plugin count dropped from $PRE to $POST after deploy." echo "ERROR: Plugin count dropped from $PRE to $POST after deploy."
echo "Missing plugins:"
diff /tmp/plugins_pre_deploy.txt /tmp/plugins_post_deploy.txt || true diff /tmp/plugins_pre_deploy.txt /tmp/plugins_post_deploy.txt || true
exit 1 exit 1
fi fi
echo "Plugin inventory OK." echo "Plugin inventory OK."
ENDSSH ENDSSH
- name: Purge LiteSpeed Cache - name: Purge LiteSpeed Cache
run: | run: |
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} \ ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=30 \
-o BatchMode=yes \
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH' ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed" CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed"
if [ -d "$CACHE_DIR" ]; then if [ -d "$CACHE_DIR" ]; then
@@ -147,8 +108,14 @@ jobs:
echo "LiteSpeed cache purged." echo "LiteSpeed cache purged."
fi fi
touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true
echo "Cache purge signal sent."
ENDSSH ENDSSH
- name: Production health check
- name: Cleanup
if: always()
run: rm -f ~/.ssh/deploy_key
- name: Health check
run: | run: |
sleep 5 sleep 5
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com) STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com)
@@ -156,12 +123,4 @@ jobs:
echo "ERROR: Site returned unexpected HTTP $STATUS after deploy" echo "ERROR: Site returned unexpected HTTP $STATUS after deploy"
exit 1 exit 1
fi fi
echo "Production healthy (HTTP $STATUS)" echo "Site is responsive (HTTP $STATUS)"
- name: Cleanup
if: always()
run: rm -f ~/.ssh/deploy_key
- name: Deploy summary
run: |
echo "## Deployment Complete" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Deployed to:** peptiderepo.com/themes/peptide-starter" >> $GITHUB_STEP_SUMMARY

View File

@@ -29,22 +29,12 @@ jobs:
with: with:
script: | script: |
const sha = context.sha; const sha = context.sha;
// Squash-merge -> PR association is eventually-consistent; poll briefly
// before concluding this was a direct push (avoids false positives on merges).
async function findMergedPR() {
for (let attempt = 1; attempt <= 5; attempt++) {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
commit_sha: sha, commit_sha: sha,
}); });
const m = prs.find(p => p.merged_at); const merged = prs.find(p => p.merged_at);
if (m) return m;
if (attempt < 5) await new Promise(r => setTimeout(r, 10000));
}
return null;
}
const merged = await findMergedPR();
if (merged) { if (merged) {
core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`); core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`);
return; return;

View File

@@ -1,142 +1,132 @@
# Peptide Starter Theme - Changelog # Peptide Starter Theme - Changelog
## [2.2.2] - 2026-04-29 — Front page layout restructure
### Changed
- `template-parts/module-cards.php`: Removed Science Feed from the module card
grid. The grid is for interactive tools only; news is content. Grid is now
5 cards: Peptides, Calculator, Protocol Builder, Tracker, Subject Log. No card
in this set goes inactive-dimmed due to a plugin registration check.
- `front-page.php`: Replaced the full `[peptide_news]` inline feed with a
3-item teaser row (`[peptide_news count="3"]`) and a "View all research →"
CTA linking to `/news`. Reduces page weight and drives traffic to the
dedicated news page.
## [2.2.1] - 2026-04-29 — RCT inline abbreviation tooltip
### Added
- `.pr-abbr` tap-target on the term "RCT" in the Established verdict card on
the verdict legend strip (homepage). Tapping/clicking opens a small inline
popover with the full definition: "Randomized Controlled Trial — the gold
standard for clinical evidence." Closes on outside click, Escape key, or
focus leaving the element.
- `assets/js/abbr-tooltip.js` — new JS module (~130 lines) for popover open/
close, outside-click dismissal, keyboard accessibility (Enter/Space/Escape),
and ARIA attributes (`aria-expanded`, `aria-describedby`, `role="tooltip"`).
Enqueued on front page only (`is_front_page()`).
- `peptide_starter_render_desc( string $text, array $abbrs ): string` helper in
`inc/verdict-helpers.php`. Builds safe HTML from `esc_html`/`esc_attr`
components; verdict taxonomy items carry an optional `abbrs` map.
- `.pr-abbr` and `.pr-abbr-popover` CSS in `style.css` — dashed underline
affordance, fade-in animation, dark-mode-safe surface variables.
### Fixed
- `PEPTIDE_STARTER_VERSION` constant in `functions.php` was stale at
`2.0.0-alpha.3`; realigned to `2.2.1` to match `style.css` Version header.
## [2.1.8] - 2026-04-28 — Homepage verdict section spacing fix
### Fixed
- Added missing CSS for `hero-featured-verdicts`, `hero-verdict-card`, and
`hero-verdict-explainer` BEM classes used in `front-page.php`. These classes
were referenced in the template but had zero CSS rules, causing the Featured
Verdicts section to render with bare browser defaults (no grid layout, no card
padding, no visual separation between verdict entries).
- Added responsive 3-column grid for verdict cards at ≥900px breakpoint.
- Added 5-column grid for verdict explainer items at ≥900px, 3-column at ≥640px.
- Verdict card left-border now tints to match verdict state via CSS `:has()`.
All notable changes to the Peptide Starter Theme are documented in this file. All notable changes to the Peptide Starter Theme are documented in this file.
## [2.1.7] - 2026-04-27 — Newsletter signup infrastructure (Beehiiv) ## [2.0.0-alpha.1] - 2026-04-26 — Brand Identity v1.0.0 (Direction C: Trusted Guide)
### Added ### Added
- Brand identity CSS token system in `assets/css/brand.css` (Direction C palette: Teal/Lime/Orange)
- Poppins + Inter + IBM Plex Mono font loading (Google Fonts) with preload/preconnect
- Full favicon/PWA/OG asset pack from peptide-repo-brand v1.0.0:
- Logo variants: horizontal, horizontal-reverse, horizontal-on-teal, mark, mark-small, wordmark, wordmark-reverse, mono
- Favicons: ICO + PNG 16/32/48, apple-touch-icon
- PWA icons and manifest
- og-default.png (1200×630) for social sharing fallback
- `inc/verdict-meta.php` — Post meta registration for pr_peptide CPT:
- `verdict_state` (required): established|promising|investigational|insufficient|cautionary
- `signal_row_1/2/3` (optional): evidence signal labels
- Meta box in pr_peptide edit screen with sanitization and nonce protection
- Verdict badge component (`template-parts/verdict/badge.php`) with state-specific glyphs
- Verdict card component (`template-parts/verdict/card.php`) for monograph hero with evidence rows
- Evidence signal row sub-component (`template-parts/verdict/evidence-row.php`)
- Affiliate disclosure component (`template-parts/affiliate-disclosure.php`) with 3 contexts:
- Inline (adjacent to links)
- Banner (page-top disclosure)
- Footer (persistent sitewide)
- `page-how-we-review.php` — How We Review Peptides page template with ToC sidebar
- `page-about.php` — About & Editorial Standards page template with disclosure block
- Footer "Our Method" link section pointing to verdict explainer and editorial standards
- Footer affiliate disclosure via template-part (footer context)
- OG image fallback meta tag in header.php (og-default.png)
- Favicon and PWA manifest links in header.php with theme-color
- SVG logo support in header via inline `logo-horizontal.svg` (fallback to text if unavailable)
- Skip-to-content link in header.php (screen reader text)
- Reduced-motion media query in brand.css for button transitions
- `page-subscribe.php` — single-column conversion template for `/subscribe/` landing page. ### Changed
No sticky ToC; signup-form-centric layout. - `header.php`: Added font preconnect/preload/stylesheet links (Poppins, Inter, IBM Plex Mono)
- `single-peptide.php` — compact Beehiiv signup block inserted after monograph article, - `header.php`: Swapped logo function to use inline SVG `logo-horizontal.svg` if available
before prev/next navigation. Copy: "Want updates on monographs like this? One email a - `functions.php`: Updated version to 2.0.0-alpha.1
week, no spam." Form ID: `6edcd482-cbea-4767-8777-97ac17d6d609`. - `functions.php`: Added `require_once` for new `inc/verdict-meta.php`
- `footer.php` column 1 — "Get our weekly digest →" link to `/subscribe/` below brand blurb. - `functions.php`: Enqueued `assets/css/brand.css` before main stylesheet (dependency order)
- `functions.php` — `peptide_starter_enqueue_beehiiv_attribution()` loads Beehiiv's - `functions.php`: Footer widget area registration extended from 4 to 5 columns
`attribution.js` site-wide (footer, async) for UTM source capture across all placements. - `footer.php`: Added "Our Method" footer section with two links
- `footer.php`: Integrated affiliate disclosure component (footer context)
- `inc/helpers.php`: Updated `peptide_starter_the_custom_logo()` to prefer SVG logo from `assets/brand/logo-horizontal.svg`
### Styling
- Brand CSS tokens cascade into existing theme; all new components use semantic token variables
- Verdict states (5-state taxonomy) signaled by color AND glyph AND label (never color-only)
- Lime (`#7FD600`) restricted to badge/button backgrounds with ink foreground (WCAG AA compliant)
- Prefers-reduced-motion honored for chip hover transitions
- All component BEM classes use `.pr-` prefix (brand) or `.ps-` prefix (theme); no collision with plugin prefixes (`.pn-`, `.psa-`, `.prab-`)
### Accessibility
- Verdict badge component: `role="img"` + `aria-label="Verdict: {State}"` for screen readers
- Glyph marked `aria-hidden="true"` to avoid redundant announcement
- Affiliate disclosure component: `<aside aria-label="Affiliate disclosure">` for context
- Skip-to-content link: `.screen-reader-text` class, keyboard-focusable
- All form inputs in verdict meta box properly labeled
- Evidence rows and card CTAs properly labeled and semantic
### Notes ### Notes
- Requires brand assets to be copied into `assets/brand/`. All 9 SVG logos + 7 raster files included.
- Theme logo currently from hardcoded SVG; custom logo upload still supported via fallback.
- Verdict system is opt-in: card renders only if `verdict_state` meta is set; graceful fallback if not.
- Affiliate disclosure copy is hardcoded (not DB-driven) in v1. Can migrate to post meta or settings in v2.
- Footer extends to 5 columns; "Our Method" section can be replaced via widget for footer-5.
- Dark mode: verdict colors were validated for light mode only. Dark mode support planned for v1.1.
- Beehiiv form ID and embed script URL are compile-time constants in the templates. ## [1.7.0] - 2026-04-24 — Remove WooCommerce integration
If the form ID changes, update `single-peptide.php` and `page-subscribe.php`.
- `single-repo_daily.php` and `archive-repo_daily.php` signup placements deferred to
the Repo Daily sprint (templates don't exist yet).
- Sender: `repo-weekly@mail.peptiderepo.com`. DNS records (SPF/DKIM/DMARC for
`mail.peptiderepo.com`) to be added by Terence in Cloudflare when Beehiiv
sending-domain verification runs.
## [2.1.6] - 2026-04-27 — Our Methodology page template + footer link ## [1.7.0] - 2026-04-24 — Remove WooCommerce integration
### Added WooCommerce is not used on peptiderepo.com. Removing dead code reduces the asset
dequeue surface and eliminates the function_exists() guard introduced in the 2026-04-23
hotfix.
- `page-our-methodology.php` — two-column ToC template for the `/our-methodology/` ### Removed
editorial-standards page. Mirrors `page-how-we-review.php` layout; CSS classes - WooCommerce asset dequeue block from `inc/perf-asset-policy.php` (styles:
use `page-our-methodology` prefix. The `#last-verified` anchor on this page is the woocommerce-layout, woocommerce-smallscreen, woocommerce-general; scripts:
deep-link target for Phase 2 verdict-card "Last verified" lines. wc-add-to-cart, woocommerce, sourcebuster-js, wc-order-attribution)
- Footer "Our Method" section now includes **Our Methodology** link (`/our-methodology/`) - Cart icon in `header.php` (was conditional on `class_exists('WooCommerce')`)
alongside How We Review Peptides and Editorial Standards. - `peptide_starter_perf_woocommerce_styles` and `peptide_starter_perf_woocommerce_scripts`
filter hooks
### Notes - WooCommerce-specific tests from `tests/test-perf-asset-policy.php`
- WooCommerce section from README.md and CONVENTIONS.md
- No changes to existing templates, CSS, or JS.
- `footer.php` also backports the section-5 "Our Method" block from the v2.1.5 brand
sprint which was live on the server but not committed to the repo.
## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1 ## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1
Mobile optimization targeting LCP reduction through conditional asset dequeue, Mobile-perf optimization targeting LCP reduction through conditional asset
font weight slimming, and font server preconnect. Measured baseline LCP on dequeue, font weight slimming, and font server preconnect. Measured baseline
Moto G Power (Slow 4G): 5.4s; target: sub-4s after Phase 1 deployment. LCP on Moto G Power (Slow 4G): 5.4s; target after Phase 1: ~2.5s.
### Performance improvements ### Added
- **Conditional asset dequeue**: WooCommerce, Elementor, and USMI assets now - `inc/perf-asset-policy.php` — five-function module:
dequeue on pages that don't use them. Removes ~100KB from non-shop pages. - `peptide_starter_perf_dequeue_plugin_assets()` — dequeues WooCommerce,
- **Google Fonts slimming**: Roboto and Roboto Slab trimmed from 72 faces to 5 Elementor, and Ultimate Social Media Icons assets on pages that don't
(400/500/700 weights, no italics). Font CSS shrinks ~6KB → <1KB; woff2 payload use them. Hook: `wp_enqueue_scripts` priority 100.
reduced ~80%. - `peptide_starter_perf_slim_google_fonts()` — `style_loader_src` filter;
- **Preconnect hints**: `fonts.googleapis.com` and `fonts.gstatic.com` now rewrites Roboto and Roboto Slab `family=` queries to keep only weights
preconnect in `<head>`, eliminating separate TLS handshake latency on cold cache. actually used (400/500/700 + 400/700). Drops 72 font faces to 5.
- **Cookie-notice defer**: Cookie banner script deferred to unblock initial render. - `peptide_starter_perf_resource_hints()` — `wp_resource_hints` filter;
adds preconnect hints for `fonts.googleapis.com` and `fonts.gstatic.com`.
- `peptide_starter_perf_defer_cookie_notice()` — `script_loader_tag` filter;
appends `defer` attribute to the cookie-notice front-end script.
- `peptide_starter_page_uses_elementor( int $post_id )` — helper.
- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default `true`).
Define `false` in `wp-config.php` to disable all dequeues instantly.
- Filterable handle lists: `peptide_starter_perf_woocommerce_handles`,
`peptide_starter_perf_elementor_handles`, `peptide_starter_perf_usmi_handles`,
`peptide_starter_perf_font_weights`.
- `tests/test-perf-asset-policy.php` — 13 unit-test cases covering kill-switch,
per-page-context dequeue logic, font slim happy/edge cases, filter overrides,
preconnect insertion, defer attribute application.
### Technical ### Changed
- New module `inc/perf-asset-policy.php` (~245 lines, PHPUnit tested). - `style.css` Version header bumped 1.5.2 → 1.6.0.
- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default true) allows - `PEPTIDE_STARTER_VERSION` constant bumped to `1.6.0`.
disable via `wp-config.php` for rollback.
- All dequeue lists and font weights exposed as filters for customization.
- Production handle verification: WC (layout/smallscreen/general), Elementor
(frontend/frontend-legacy + per-post CSS), USMI (SFSImainCss + 4 scripts).
### Tests ### Why
- `test-perf-asset-policy.php` — 8 new unit cases covering dequeue conditions PageSpeed Insights mobile score on `https://peptiderepo.com/` was 74 with
(shop/non-shop/Elementor/USMI), font rewrite happy path and edge cases, LCP 5.4s, FCP 2.6s. Desktop scored ~95 — the gap was mobile-specific
preconnect addition, defer application, and kill-switch disable. (weaker CPU + slower network amplify the cost of 13 render-blocking CSS
files in `<head>`, of which the homepage actually needed at most 4).
### Out of scope Phase 1 trims the asset graph for non-shop pages without touching plugin
code or template files.
- Critical CSS inline (Phase 2 candidate after measuring Phase 1 lift).
- jQuery dequeue (WC/PSA/Elementor compat risk too high).
- Image optimizations (no images on homepage critical path).
- LiteSpeed Cache settings (CTO tunes post-deploy if Phase 1 insufficient).
### Breaking changes
None. All optimizations are transparent to plugin authors; dequeues use
standard WordPress APIs and can be overridden via filters or re-enqueued by
plugins if needed.
## [1.5.2] - 2026-04-14 — Security ## [1.5.2] - 2026-04-14 — Security

View File

@@ -1,134 +0,0 @@
/**
* abbr-tooltip.js — Inline abbreviation popover for verdict legend screen.
*
* What: Opens a small popover anchored to any .pr-abbr element when tapped,
* clicked, or activated via keyboard. Closes on outside click or Escape.
* Who triggers: Enqueued by functions.php on is_front_page() requests only.
* Depends on: .pr-abbr elements in the DOM with data-def attributes; CSS in
* style.css (.pr-abbr-popover).
*
* @see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr elements
* @see style.css — .pr-abbr, .pr-abbr-popover visual styles
* @package peptide-starter
*/
( function () {
'use strict';
var POPOVER_CLASS = 'pr-abbr-popover';
var ACTIVE_CLASS = 'pr-abbr--active';
var activePopover = null;
var activeAbbr = null;
/**
* Remove the currently open popover, if any.
*
* @return {void}
*/
function closePopover() {
if ( activePopover ) {
activePopover.remove();
activePopover = null;
}
if ( activeAbbr ) {
activeAbbr.classList.remove( ACTIVE_CLASS );
activeAbbr.removeAttribute( 'aria-expanded' );
activeAbbr = null;
}
}
/**
* Create and position a popover anchored to the given abbr element.
*
* @param {HTMLElement} abbr - The .pr-abbr element that was activated.
* @return {void}
*/
function openPopover( abbr ) {
closePopover();
var def = abbr.getAttribute( 'data-def' );
if ( ! def ) {
return;
}
var popover = document.createElement( 'div' );
popover.className = POPOVER_CLASS;
popover.textContent = def;
popover.setAttribute( 'role', 'tooltip' );
popover.setAttribute( 'id', POPOVER_CLASS + '-' + Date.now() );
document.body.appendChild( popover );
// Position below the abbr, clamped to viewport width.
var rect = abbr.getBoundingClientRect();
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
var top = rect.bottom + scrollTop + 6;
var left = rect.left + scrollLeft;
var maxLeft = window.innerWidth - popover.offsetWidth - 16;
popover.style.top = top + 'px';
popover.style.left = Math.max( 8, Math.min( left, maxLeft ) ) + 'px';
abbr.classList.add( ACTIVE_CLASS );
abbr.setAttribute( 'aria-expanded', 'true' );
abbr.setAttribute( 'aria-describedby', popover.id );
activePopover = popover;
activeAbbr = abbr;
}
/**
* Toggle the popover for an abbr element (open if closed, close if open).
*
* @param {HTMLElement} abbr - The .pr-abbr element that was activated.
* @return {void}
*/
function togglePopover( abbr ) {
if ( activeAbbr === abbr ) {
closePopover();
} else {
openPopover( abbr );
}
}
// -------------------------------------------------------------------------
// Event listeners
// -------------------------------------------------------------------------
document.addEventListener( 'click', function ( e ) {
var abbr = e.target.closest( '.pr-abbr' );
if ( abbr ) {
e.stopPropagation();
togglePopover( abbr );
return;
}
// Click outside any abbr — close.
closePopover();
} );
document.addEventListener( 'keydown', function ( e ) {
if ( e.key === 'Escape' ) {
closePopover();
return;
}
if ( e.key === 'Enter' || e.key === ' ' ) {
var abbr = e.target.closest( '.pr-abbr' );
if ( abbr ) {
e.preventDefault();
togglePopover( abbr );
}
}
} );
// Close when focus leaves the popover+abbr pair entirely.
document.addEventListener( 'focusin', function ( e ) {
if (
activeAbbr &&
! activeAbbr.contains( e.target ) &&
( ! activePopover || ! activePopover.contains( e.target ) )
) {
closePopover();
}
} );
} )();

View File

@@ -31,8 +31,7 @@
// Default footer content if no widgets. // Default footer content if no widgets.
if ( 1 === $i ) { if ( 1 === $i ) {
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>'; echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
echo '<p>' . esc_html__( 'Verdict-driven peptide guides for clinicians and informed readers. Clear about the evidence — including when there isn\'t any.', 'peptide-starter' ) . '</p>'; echo '<p>' . esc_html__( 'Peptide Repo is a comprehensive scientific database for peptide research.', 'peptide-starter' ) . '</p>';
echo '<p style="margin-top: var(--spacing-md); font-size: 0.9rem;"><a href="' . esc_url( home_url( '/subscribe' ) ) . '" style="color: var(--color-accent-green, #5a7c5a); font-weight: 600;">' . esc_html__( 'Get our weekly digest →', 'peptide-starter' ) . '</a></p>';
} elseif ( 2 === $i ) { } elseif ( 2 === $i ) {
echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>'; echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>';
echo '<ul>'; echo '<ul>';
@@ -57,7 +56,6 @@
echo '<h3>' . esc_html__( 'Our Method', 'peptide-starter' ) . '</h3>'; echo '<h3>' . esc_html__( 'Our Method', 'peptide-starter' ) . '</h3>';
echo '<ul>'; echo '<ul>';
echo '<li><a href="' . esc_url( home_url( '/how-we-review-peptides' ) ) . '">' . esc_html__( 'How We Review Peptides', 'peptide-starter' ) . '</a></li>'; echo '<li><a href="' . esc_url( home_url( '/how-we-review-peptides' ) ) . '">' . esc_html__( 'How We Review Peptides', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/our-methodology' ) ) . '">' . esc_html__( 'Our Methodology', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/about' ) ) . '">' . esc_html__( 'Editorial Standards', 'peptide-starter' ) . '</a></li>'; echo '<li><a href="' . esc_url( home_url( '/about' ) ) . '">' . esc_html__( 'Editorial Standards', 'peptide-starter' ) . '</a></li>';
echo '</ul>'; echo '</ul>';
} }

View File

@@ -2,38 +2,21 @@
/** /**
* The front page template * The front page template
* *
* Layout: Hero → Verdict Explainer → Featured Verdict Cards → Research Modules * Layout: Hero → Research Modules → PRAutoBlogger posts → Peptide News feed.
* → PRAutoBlogger posts → Peptide News feed. * Newsletter signup is handled by footer.php (via template part) on the front page.
*
* Brand v2.0.0: Direction C "The Trusted Guide". Hero uses the 5-state verdict
* taxonomy as the central explainer hook. Three featured verdict cards pull from
* real monographs (semaglutide / BPC-157 / Melanotan-II) via post meta.
* *
* @see template-parts/module-cards.php — research modules grid * @see template-parts/module-cards.php — research modules grid
* @see template-parts/verdict/badge.php — verdict badge component * @see template-parts/newsletter-signup.php — included via footer.php on front page
* @see template-parts/verdict/card.php — full verdict card (monograph hero) * @see functions.php — helper functions for hero content
* @see inc/helpers.php — hero title / subtitle getters
* @see assets/css/brand.css — verdict token definitions
* *
* @package peptide-starter * @package peptide-starter
*/ */
get_header(); get_header();
/** @see inc/verdict-helpers.php — peptide_starter_get_verdict_taxonomy() */
$verdict_taxonomy = peptide_starter_get_verdict_taxonomy();
/**
* Featured monograph post IDs. These are the three exemplar monographs
* loaded during brand sprint v2.0.0 (2026-04-26).
* Semaglutide=211 (established), BPC-157=36 (investigational), Melanotan-II=177 (cautionary).
*/
$featured_monograph_ids = array( 211, 36, 177 );
?> ?>
<main id="main" class="site-main"> <main id="main" class="site-main">
<!-- Hero Section -->
<!-- HERO SECTION -->
<section class="hero-section"> <section class="hero-section">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title"> <h1 class="hero-title">
@@ -77,134 +60,14 @@ $featured_monograph_ids = array( 211, 36, 177 );
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary"> <a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?> <?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?>
</a> </a>
<a href="<?php echo esc_url( home_url( '/how-we-review-peptides' ) ); ?>" class="ps-btn ps-btn-secondary"> <a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Our method', 'peptide-starter' ); ?> <?php esc_html_e( 'Documentation', 'peptide-starter' ); ?>
</a> </a>
</div> </div>
</div> </div>
</section> </section>
<!-- VERDICT TAXONOMY EXPLAINER --> <!-- Research Modules Grid (6 cards) -->
<section class="hero-verdict-explainer" aria-label="<?php esc_attr_e( 'Our verdict system', 'peptide-starter' ); ?>">
<div class="ps-container">
<p class="hero-verdict-explainer__heading">
<?php esc_html_e( 'Every peptide gets a verdict. Here\'s what they mean.', 'peptide-starter' ); ?>
</p>
<div class="hero-verdict-explainer__grid">
<?php foreach ( $verdict_taxonomy as $v ) : ?>
<div class="hero-verdict-explainer__item">
<span
class="pr-verdict pr-verdict--<?php echo esc_attr( $v['state'] ); ?>"
role="img"
aria-label="<?php echo esc_attr( sprintf( __( 'Verdict: %s', 'peptide-starter' ), $v['label'] ) ); ?>"
>
<span class="pr-verdict__glyph" aria-hidden="true"><?php echo esc_html( $v['glyph'] ); ?></span>
<span class="pr-verdict__label"><?php echo esc_html( $v['label'] ); ?></span>
</span>
<p class="hero-verdict-explainer__desc"><?php echo peptide_starter_render_desc( $v['description'], $v['abbrs'] ?? array() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is built from esc_html/esc_attr components inside render_desc(). ?></p>
</div>
<?php endforeach; ?>
</div>
<p class="hero-verdict-explainer__cta-line">
<a href="<?php echo esc_url( home_url( '/how-we-review-peptides' ) ); ?>" class="ps-btn ps-btn-tertiary ps-btn-sm">
<?php esc_html_e( 'How we assign verdicts →', 'peptide-starter' ); ?>
</a>
</p>
</div>
</section>
<!-- FEATURED VERDICT CARDS: semaglutide=211 (Established), BPC-157=36 (Investigational), Melanotan-II=177 (Cautionary) -->
<?php
$featured_posts = get_posts(
array(
'post__in' => $featured_monograph_ids,
'post_type' => 'peptide',
'posts_per_page' => 3,
'orderby' => 'post__in',
'post_status' => 'publish',
)
);
if ( ! empty( $featured_posts ) ) :
?>
<section class="hero-featured-verdicts" aria-label="<?php esc_attr_e( 'Featured peptide verdicts', 'peptide-starter' ); ?>">
<div class="ps-container">
<h2 class="hero-featured-verdicts__title"><?php esc_html_e( 'Featured verdicts', 'peptide-starter' ); ?></h2>
<p class="hero-featured-verdicts__subtitle">
<?php esc_html_e( 'Real assessments from our database — see the system in action.', 'peptide-starter' ); ?>
</p>
<div class="hero-featured-verdicts__grid">
<?php
foreach ( $featured_posts as $featured_post ) :
$post_id = $featured_post->ID;
$verdict_state = get_post_meta( $post_id, 'verdict_state', true );
$verdict_text = get_post_meta( $post_id, 'verdict_text', true );
$post_url = get_permalink( $post_id );
// Verdict state config — @see inc/verdict-helpers.php
$verdict_config = peptide_starter_get_verdict_config();
$vc = isset( $verdict_config[ $verdict_state ] ) ? $verdict_config[ $verdict_state ] : null;
// Signal rows (up to 3).
$signal_rows = array();
for ( $i = 1; $i <= 3; $i++ ) {
$row_label = get_post_meta( $post_id, 'signal_row_' . $i . '_label', true );
$row_glyph = get_post_meta( $post_id, 'signal_row_' . $i . '_glyph', true );
if ( $row_label ) {
$signal_rows[] = array(
'label' => $row_label,
'glyph' => $row_glyph ? $row_glyph : '•',
);
}
}
?>
<article class="hero-verdict-card" aria-label="<?php echo esc_attr( get_the_title( $post_id ) ); ?>">
<?php if ( $vc ) : ?>
<span
class="pr-verdict pr-verdict--<?php echo esc_attr( $verdict_state ); ?> hero-verdict-card__badge"
role="img"
aria-label="<?php echo esc_attr( sprintf( __( 'Verdict: %s', 'peptide-starter' ), $vc['label'] ) ); ?>"
>
<span class="pr-verdict__glyph" aria-hidden="true"><?php echo esc_html( $vc['glyph'] ); ?></span>
<span class="pr-verdict__label"><?php echo esc_html( $vc['label'] ); ?></span>
</span>
<?php endif; ?>
<h3 class="hero-verdict-card__title">
<a href="<?php echo esc_url( $post_url ); ?>"><?php echo esc_html( get_the_title( $post_id ) ); ?></a>
</h3>
<?php if ( $verdict_text ) : ?>
<p class="hero-verdict-card__summary"><?php echo esc_html( $verdict_text ); ?></p>
<?php endif; ?>
<?php if ( ! empty( $signal_rows ) ) : ?>
<ul class="hero-verdict-card__signals" aria-label="<?php esc_attr_e( 'Evidence signals', 'peptide-starter' ); ?>">
<?php foreach ( $signal_rows as $row ) : ?>
<li class="hero-verdict-card__signal-item">
<span class="hero-verdict-card__signal-glyph" aria-hidden="true"><?php echo esc_html( $row['glyph'] ); ?></span>
<?php echo esc_html( $row['label'] ); ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<a href="<?php echo esc_url( $post_url ); ?>" class="hero-verdict-card__link">
<?php esc_html_e( 'Read the full evidence →', 'peptide-starter' ); ?>
</a>
</article>
<?php endforeach; ?>
</div>
<div class="hero-featured-verdicts__footer">
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Browse all peptides', 'peptide-starter' ); ?>
</a>
</div>
</div>
</section>
<?php endif; ?>
<!-- Research Modules Grid (5 cards) -->
<?php get_template_part( 'template-parts/module', 'cards' ); ?> <?php get_template_part( 'template-parts/module', 'cards' ); ?>
<!-- PRAutoBlogger Posts Widget --> <!-- PRAutoBlogger Posts Widget -->
@@ -214,29 +77,24 @@ $featured_monograph_ids = array( 211, 36, 177 );
} }
?> ?>
<!-- News Teaser Section — 3 items + "View all" CTA --> <!-- News Feed Section -->
<?php <?php
if ( shortcode_exists( 'peptide_news' ) ) { if ( shortcode_exists( 'peptide_news' ) ) {
?> ?>
<section class="news-teaser-section"> <section class="news-feed-section">
<div class="ps-container"> <div class="ps-container">
<div class="news-teaser-header"> <div class="news-feed-header">
<div> <h2 class="news-feed-title"><?php esc_html_e( 'Latest Peptide Research', 'peptide-starter' ); ?></h2>
<h2><?php esc_html_e( 'Latest Peptide Research', 'peptide-starter' ); ?></h2> <p class="news-feed-description">
<p><?php esc_html_e( 'Recent publications from leading research sources.', 'peptide-starter' ); ?></p> <?php esc_html_e( 'Stay updated with the latest developments in peptide science and research.', 'peptide-starter' ); ?>
</p>
</div> </div>
<a href="<?php echo esc_url( home_url( '/news' ) ); ?>" class="ps-btn ps-btn-secondary"> <?php echo do_shortcode( '[peptide_news]' ); ?>
<?php esc_html_e( 'View all research →', 'peptide-starter' ); ?>
</a>
</div>
<?php echo do_shortcode( '[peptide_news count="3"]' ); ?>
</div> </div>
</section> </section>
<?php <?php
} }
?> ?>
</main> </main>
<?php get_footer(); ?> <?php get_footer(); ?>

View File

@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
// Theme constants. // Theme constants.
define( 'PEPTIDE_STARTER_VERSION', '2.2.2' ); define( 'PEPTIDE_STARTER_VERSION', '2.0.0-alpha.1' );
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() );
@@ -45,7 +45,6 @@ require_once PEPTIDE_STARTER_DIR . '/inc/newsletter-admin.php';
require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php'; require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php';
require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php'; require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php';
require_once PEPTIDE_STARTER_DIR . '/inc/verdict-meta.php'; require_once PEPTIDE_STARTER_DIR . '/inc/verdict-meta.php';
require_once PEPTIDE_STARTER_DIR . '/inc/verdict-helpers.php';
/** /**
* Set up theme defaults and register support for WordPress features. * Set up theme defaults and register support for WordPress features.
@@ -88,11 +87,6 @@ function peptide_starter_scripts() {
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 );
wp_enqueue_script( 'peptide-starter-settings-panel', PEPTIDE_STARTER_URI . '/assets/js/settings-panel.js', array(), PEPTIDE_STARTER_VERSION, true ); wp_enqueue_script( 'peptide-starter-settings-panel', PEPTIDE_STARTER_URI . '/assets/js/settings-panel.js', array(), PEPTIDE_STARTER_VERSION, true );
// Abbreviation popovers — only needed on the front page verdict legend strip.
if ( is_front_page() ) {
wp_enqueue_script( 'peptide-starter-abbr-tooltip', PEPTIDE_STARTER_URI . '/assets/js/abbr-tooltip.js', array(), PEPTIDE_STARTER_VERSION, true );
}
wp_localize_script( wp_localize_script(
'peptide-starter-theme', 'peptide-starter-theme',
'peptideStarterData', 'peptideStarterData',
@@ -249,20 +243,3 @@ function peptide_starter_inline_dark_mode_script() {
<?php <?php
} }
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' ); add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
/**
* Enqueue Beehiiv attribution script site-wide.
*
* Loads Beehiiv's attribution.js in the footer so all signup form placements
* (subscribe page iframe, single-peptide inline form) are tracked correctly
* under a single script include.
*
* Who calls it: wp_enqueue_scripts action (all front-end page loads).
* Dependencies: none.
*
* @return void
*/
function peptide_starter_enqueue_beehiiv_attribution() {
wp_enqueue_script( 'beehiiv-attribution', 'https://subscribe-forms.beehiiv.com/attribution.js', array(), null, true );
}
add_action( 'wp_enqueue_scripts', 'peptide_starter_enqueue_beehiiv_attribution' );

View File

@@ -118,8 +118,7 @@
</button> </button>
<!-- Dark Mode Toggle --> <!-- Dark Mode Toggle -->
<!-- Dark mode disabled for v1: verdict-color accessibility only validated for light mode. Re-enable in v1.1 after dark palette audit. --> <button class="header-icon-btn dark-mode-toggle" aria-label="<?php esc_attr_e( 'Toggle dark mode', 'peptide-starter' ); ?>">
<button class="header-icon-btn dark-mode-toggle" aria-label="<?php esc_attr_e( 'Toggle dark mode', 'peptide-starter' ); ?>" style="display:none;" aria-hidden="true">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none;"> <svg class="sun-icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none;">
<circle cx="10" cy="10" r="4" stroke="currentColor" stroke-width="2"/> <circle cx="10" cy="10" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M10 1V3M10 17V19M19 10H17M3 10H1M16.657 16.657L15.243 15.243M4.757 4.757L3.343 3.343M16.657 3.343L15.243 4.757M4.757 15.243L3.343 16.657" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M10 1V3M10 17V19M19 10H17M3 10H1M16.657 16.657L15.243 15.243M4.757 4.757L3.343 3.343M16.657 3.343L15.243 4.757M4.757 15.243L3.343 16.657" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>

View File

@@ -155,7 +155,7 @@ function peptide_starter_the_custom_logo() {
* @return string Hero title text. * @return string Hero title text.
*/ */
function peptide_starter_get_hero_title() { function peptide_starter_get_hero_title() {
return get_theme_mod( 'hero_title', esc_html__( 'Is this peptide right for you?', 'peptide-starter' ) ); return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
} }
/** /**
@@ -164,7 +164,7 @@ function peptide_starter_get_hero_title() {
* @return string Hero subtitle text. * @return string Hero subtitle text.
*/ */
function peptide_starter_get_hero_subtitle() { function peptide_starter_get_hero_subtitle() {
return get_theme_mod( 'hero_subtitle', esc_html__( 'What the research really says', 'peptide-starter' ) ); return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
} }
/** /**

View File

@@ -1,137 +0,0 @@
<?php
/**
* Verdict helper functions for homepage and template parts.
*
* What: Provides data arrays for the 5-state verdict taxonomy used in the
* homepage explainer strip and featured verdict cards.
* Who triggers: Required by functions.php on every page load.
* Depends on: Nothing — pure data functions, no WP dependencies.
*
* @see inc/verdict-meta.php — verdict CPT meta registration
* @see template-parts/verdict/ — badge, card, evidence-row components
* @see front-page.php — homepage verdict sections
*
* @package peptide-starter
*/
/**
* Returns the verdict taxonomy data array for the homepage explainer strip.
*
* Canonical order matches CMO brief §5.1 and the 5-state enum in verdict-meta.php.
*
* @return array[] Each element: state (string), glyph (string), label (string), description (string), abbrs (array, optional).
*/
function peptide_starter_get_verdict_taxonomy() {
return array(
array(
'state' => 'established',
'glyph' => '✓',
'label' => 'Established',
'description' => 'Strong RCT evidence; widely prescribed or validated.',
// abbrs: terms in description that should render as inline tap-targets.
'abbrs' => array(
'RCT' => 'Randomized Controlled Trial — the gold standard for clinical evidence. Participants are randomly assigned to treatment or control groups to measure real effect.',
),
),
array(
'state' => 'promising',
'glyph' => '◐',
'label' => 'Promising',
'description' => 'Meaningful human data accumulating; not yet RCT-strong.',
),
array(
'state' => 'investigational',
'glyph' => '?',
'label' => 'Investigational',
'description' => 'Mechanism plausible; human trials sparse.',
),
array(
'state' => 'insufficient',
'glyph' => '⊘',
'label' => 'Insufficient Evidence',
'description' => 'Not enough data to say anything responsible.',
),
array(
'state' => 'cautionary',
'glyph' => '⚠',
'label' => 'Cautionary',
'description' => 'Active safety signals or misuse patterns.',
),
);
}
/**
* Returns the verdict state config map (glyph + label per state slug).
*
* Used by featured verdict cards on the homepage and any template that needs
* to render a verdict badge from a state string without loading the full card.
*
* @return array Keyed by state slug. Each value: array{ glyph: string, label: string }.
*/
function peptide_starter_get_verdict_config() {
return array(
'established' => array(
'glyph' => '✓',
'label' => 'Established',
),
'promising' => array(
'glyph' => '◐',
'label' => 'Promising',
),
'investigational' => array(
'glyph' => '?',
'label' => 'Investigational',
),
'insufficient' => array(
'glyph' => '⊘',
'label' => 'Insufficient Evidence',
),
'cautionary' => array(
'glyph' => '⚠',
'label' => 'Cautionary',
),
);
}
/**
* Renders a description string with optional inline abbreviation tap-targets.
*
* What: Builds safe HTML for a verdict card description, replacing known
* abbreviation terms with <abbr> elements that trigger a tooltip.
* Who calls it: front-page.php verdict explainer loop.
* Depends on: Nothing — pure string transformation.
*
* The output is intentionally built from escaped components (not from raw $text)
* so we never expose unescaped user or data-layer content. Each substitution is:
* esc_html( $text ) → str_replace( literal_term, safe_abbr_html )
* This is safe because the abbr tag, class, and data-def are all hardcoded or
* escaped here, and the term key is a literal constant from our own data array.
*
* @param string $text Plain-text description string. Will be HTML-escaped.
* @param array $abbrs Associative array of term => definition. Keys are literal
* strings (e.g. 'RCT'). Values are plain-text definitions.
* @return string Safe HTML string suitable for echo without further escaping.
*
* @see inc/verdict-helpers.php — $abbrs data lives in peptide_starter_get_verdict_taxonomy()
* @see assets/js/abbr-tooltip.js — JS that handles tap/click to show the popover
* @see style.css — .pr-abbr and .pr-abbr-popover styles
*/
function peptide_starter_render_desc( string $text, array $abbrs ): string {
$html = esc_html( $text );
foreach ( $abbrs as $term => $definition ) {
$safe_term = esc_html( $term );
$safe_def = esc_attr( $definition );
$abbr_html = sprintf(
'<abbr class="pr-abbr" data-def="%s" tabindex="0" role="button" aria-label="%s: %s">%s</abbr>',
$safe_def,
$safe_term,
$safe_def,
$safe_term
);
// Replace only the first occurrence — abbreviations should not stack.
$html = preg_replace( '/' . preg_quote( $safe_term, '/' ) . '/', $abbr_html, $html, 1 );
}
return $html;
}

View File

@@ -2,15 +2,11 @@
/** /**
* Verdict System Post Meta Registration & Meta Box * Verdict System Post Meta Registration & Meta Box
* *
* Registers post meta for peptide CPT: * Registers post meta for pr_peptide CPT:
* - verdict_state (required): one of 5 states * - verdict_state (required): one of 5 states
* - verdict_text (required for launch): 3–5 sentence opinionated verdict * - signal_row_1, signal_row_2, signal_row_3 (optional): evidence signal labels
* - signal_row_1/2/3 (optional): evidence signal label text
* - signal_row_1/2/3_glyph (optional): per-row glyph; enum ✓|!|?|⊘|⚠
* - has_partner_link (bool, default false): controls inline disclosure render
* *
* Who calls it: loaded by functions.php via require_once. * Adds a meta box to the pr_peptide edit screen for managing these fields.
* Depends on: nothing — pure WP register_post_meta.
* *
* @package peptide-starter * @package peptide-starter
*/ */
@@ -20,20 +16,20 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
/** /**
* Register verdict post meta for peptide CPT. * Register verdict post meta for pr_peptide CPT.
* *
* @return void * @return void
*/ */
function peptide_starter_register_verdict_meta() { function peptide_starter_register_verdict_meta() {
$allowed_states = array( 'established', 'promising', 'investigational', 'insufficient', 'cautionary' ); $allowed_states = array( 'established', 'promising', 'investigational', 'insufficient', 'cautionary' );
$allowed_glyphs = array( '✓', '!', '?', '⊘', '⚠' );
// Register verdict_state meta. // Register verdict_state meta.
register_post_meta( register_post_meta(
'peptide', 'pr_peptide',
'verdict_state', 'verdict_state',
array( array(
'type' => 'string', 'type' => 'string',
'description' => 'Verdict classification state',
'single' => true, 'single' => true,
'show_in_rest' => true, 'show_in_rest' => true,
'auth_callback' => '__return_true', 'auth_callback' => '__return_true',
@@ -43,68 +39,26 @@ function peptide_starter_register_verdict_meta() {
) )
); );
// Register verdict_text meta — 3–5 sentence opinionated verdict paragraph. // Register signal row meta (up to 3).
register_post_meta(
'peptide',
'verdict_text',
array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'auth_callback' => '__return_true',
'sanitize_callback' => 'sanitize_textarea_field',
)
);
// Register signal row text meta (up to 3 rows).
for ( $i = 1; $i <= 3; $i++ ) { for ( $i = 1; $i <= 3; $i++ ) {
register_post_meta( register_post_meta(
'peptide', 'pr_peptide',
'signal_row_' . $i, 'signal_row_' . $i,
array( array(
'type' => 'string', 'type' => 'string',
'description' => 'Evidence signal row ' . $i,
'single' => true, 'single' => true,
'show_in_rest' => true, 'show_in_rest' => true,
'auth_callback' => '__return_true', 'auth_callback' => '__return_true',
'sanitize_callback' => 'sanitize_text_field', 'sanitize_callback' => 'sanitize_text_field',
) )
); );
// Register per-row glyph meta.
// Validated as one of the 5-state glyph set to keep visual vocabulary consistent.
register_post_meta(
'peptide',
'signal_row_' . $i . '_glyph',
array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'auth_callback' => '__return_true',
'sanitize_callback' => function ( $value ) use ( $allowed_glyphs ) {
return ( '' === $value || in_array( $value, $allowed_glyphs, true ) ) ? $value : '';
},
)
);
} }
// Register has_partner_link — gates inline affiliate disclosure rendering.
// Default false: disclosure is suppressed unless a partner link is explicitly present.
register_post_meta(
'peptide',
'has_partner_link',
array(
'type' => 'boolean',
'single' => true,
'show_in_rest' => true,
'auth_callback' => '__return_true',
'default' => false,
)
);
} }
add_action( 'init', 'peptide_starter_register_verdict_meta' ); add_action( 'init', 'peptide_starter_register_verdict_meta' );
/** /**
* Add verdict meta box to peptide CPT edit screen. * Add verdict meta box to pr_peptide edit screen.
* *
* @return void * @return void
*/ */
@@ -113,7 +67,7 @@ function peptide_starter_add_verdict_meta_box() {
'peptide_starter_verdict', 'peptide_starter_verdict',
esc_html__( 'Verdict System', 'peptide-starter' ), esc_html__( 'Verdict System', 'peptide-starter' ),
'peptide_starter_render_verdict_meta_box', 'peptide_starter_render_verdict_meta_box',
'peptide', 'pr_peptide',
'normal', 'normal',
'high' 'high'
); );
@@ -130,7 +84,9 @@ function peptide_starter_render_verdict_meta_box( $post ) {
wp_nonce_field( 'peptide_starter_verdict_nonce', 'peptide_starter_verdict_nonce' ); wp_nonce_field( 'peptide_starter_verdict_nonce', 'peptide_starter_verdict_nonce' );
$verdict_state = get_post_meta( $post->ID, 'verdict_state', true ); $verdict_state = get_post_meta( $post->ID, 'verdict_state', true );
$verdict_text = get_post_meta( $post->ID, 'verdict_text', true ); $signal_row_1 = get_post_meta( $post->ID, 'signal_row_1', true );
$signal_row_2 = get_post_meta( $post->ID, 'signal_row_2', true );
$signal_row_3 = get_post_meta( $post->ID, 'signal_row_3', true );
$allowed_states = array( $allowed_states = array(
'established' => esc_html__( 'Established', 'peptide-starter' ), 'established' => esc_html__( 'Established', 'peptide-starter' ),
'promising' => esc_html__( 'Promising', 'peptide-starter' ), 'promising' => esc_html__( 'Promising', 'peptide-starter' ),
@@ -138,15 +94,12 @@ function peptide_starter_render_verdict_meta_box( $post ) {
'insufficient' => esc_html__( 'Insufficient Evidence', 'peptide-starter' ), 'insufficient' => esc_html__( 'Insufficient Evidence', 'peptide-starter' ),
'cautionary' => esc_html__( 'Cautionary', 'peptide-starter' ), 'cautionary' => esc_html__( 'Cautionary', 'peptide-starter' ),
); );
$allowed_glyphs = array( '✓', '!', '?', '⊘', '⚠' );
$has_partner = (bool) get_post_meta( $post->ID, 'has_partner_link', true );
?> ?>
<div style="padding:12px 0;"> <div style="padding: 12px 0;">
<label for="verdict_state" style="display: block; margin-bottom: 8px;">
<label for="verdict_state" style="display:block;margin-bottom:6px;">
<strong><?php esc_html_e( 'Verdict State', 'peptide-starter' ); ?></strong> <strong><?php esc_html_e( 'Verdict State', 'peptide-starter' ); ?></strong>
</label> </label>
<select id="verdict_state" name="verdict_state" style="width:100%;padding:8px;font-size:14px;margin-bottom:16px;"> <select id="verdict_state" name="verdict_state" style="width: 100%; padding: 8px; font-size: 14px;">
<option value=""><?php esc_html_e( '— Select —', 'peptide-starter' ); ?></option> <option value=""><?php esc_html_e( '— Select —', 'peptide-starter' ); ?></option>
<?php foreach ( $allowed_states as $key => $label ) : ?> <?php foreach ( $allowed_states as $key => $label ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $verdict_state, $key ); ?>> <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $verdict_state, $key ); ?>>
@@ -155,70 +108,22 @@ function peptide_starter_render_verdict_meta_box( $post ) {
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<label for="verdict_text" style="display:block;margin-bottom:6px;"> <div style="margin-top: 20px;">
<strong><?php esc_html_e( 'Our Verdict (3–5 sentences)', 'peptide-starter' ); ?></strong> <label style="display: block; margin-bottom: 4px;">
<span style="font-weight:normal;font-size:12px;color:#666;display:block;margin-top:2px;"> <strong><?php esc_html_e( 'Signal Row 1', 'peptide-starter' ); ?></strong>
<?php esc_html_e( 'The opinionated verdict paragraph shown on the card hero. Be specific — this is the brand\'s editorial spine.', 'peptide-starter' ); ?>
</span>
</label> </label>
<textarea id="verdict_text" name="verdict_text" rows="5" <input type="text" name="signal_row_1" value="<?php echo esc_attr( $signal_row_1 ); ?>" placeholder="<?php esc_attr_e( 'e.g., Strong animal evidence', 'peptide-starter' ); ?>" style="width: 100%; padding: 8px; font-size: 14px; margin-bottom: 12px;">
style="width:100%;padding:8px;font-size:14px;margin-bottom:16px;"
placeholder="<?php esc_attr_e( 'e.g., Semaglutide works. The data is among the strongest of any peptide therapy...', 'peptide-starter' ); ?>"><?php echo esc_textarea( $verdict_text ); ?></textarea>
<hr style="border:none;border-top:1px solid #ddd;margin:8px 0 16px;"> <label style="display: block; margin-bottom: 4px;">
<p style="font-weight:bold;margin-bottom:8px;"><?php esc_html_e( 'Evidence Signal Rows', 'peptide-starter' ); ?></p> <strong><?php esc_html_e( 'Signal Row 2', 'peptide-starter' ); ?></strong>
<?php
for ( $i = 1; $i <= 3; $i++ ) :
$row_text = get_post_meta( $post->ID, 'signal_row_' . $i, true );
$row_glyph = get_post_meta( $post->ID, 'signal_row_' . $i . '_glyph', true );
?>
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:12px;">
<div style="flex:0 0 80px;">
<label for="signal_row_<?php echo esc_attr( $i ); ?>_glyph"
style="display:block;margin-bottom:4px;font-size:12px;color:#444;">
<?php esc_html_e( 'Glyph', 'peptide-starter' ); ?>
</label> </label>
<select id="signal_row_<?php echo esc_attr( $i ); ?>_glyph" <input type="text" name="signal_row_2" value="<?php echo esc_attr( $signal_row_2 ); ?>" placeholder="<?php esc_attr_e( 'e.g., Limited human data', 'peptide-starter' ); ?>" style="width: 100%; padding: 8px; font-size: 14px; margin-bottom: 12px;">
name="signal_row_<?php echo esc_attr( $i ); ?>_glyph"
style="width:100%;padding:6px;font-size:16px;"> <label style="display: block; margin-bottom: 4px;">
<option value="" <?php selected( $row_glyph, '' ); ?>>—</option> <strong><?php esc_html_e( 'Signal Row 3', 'peptide-starter' ); ?></strong>
<?php foreach ( $allowed_glyphs as $g ) : ?> </label>
<option value="<?php echo esc_attr( $g ); ?>" <?php selected( $row_glyph, $g ); ?>> <input type="text" name="signal_row_3" value="<?php echo esc_attr( $signal_row_3 ); ?>" placeholder="<?php esc_attr_e( 'e.g., Regulatory status varies', 'peptide-starter' ); ?>" style="width: 100%; padding: 8px; font-size: 14px;">
<?php echo esc_html( $g ); ?>
</option>
<?php endforeach; ?>
</select>
</div> </div>
<div style="flex:1;">
<label for="signal_row_<?php echo esc_attr( $i ); ?>"
style="display:block;margin-bottom:4px;font-size:12px;color:#444;">
<?php
/* translators: %d is the row number */
echo esc_html( sprintf( __( 'Row %d label', 'peptide-starter' ), $i ) );
?>
</label>
<input type="text" id="signal_row_<?php echo esc_attr( $i ); ?>"
name="signal_row_<?php echo esc_attr( $i ); ?>"
value="<?php echo esc_attr( $row_text ); ?>"
style="width:100%;padding:8px;font-size:14px;"
placeholder="<?php esc_attr_e( 'Evidence signal label', 'peptide-starter' ); ?>">
</div>
</div>
<?php endfor; ?>
<hr style="border:none;border-top:1px solid #ddd;margin:8px 0 16px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" name="has_partner_link" value="1"
<?php checked( $has_partner, true ); ?> style="width:16px;height:16px;">
<span>
<strong><?php esc_html_e( 'Has active partner link', 'peptide-starter' ); ?></strong>
<span style="display:block;font-size:12px;color:#666;margin-top:2px;">
<?php esc_html_e( 'Check this when the monograph contains an affiliate partner link. Controls the inline disclosure strip.', 'peptide-starter' ); ?>
</span>
</span>
</label>
</div> </div>
<?php <?php
} }
@@ -232,23 +137,21 @@ function peptide_starter_render_verdict_meta_box( $post ) {
*/ */
function peptide_starter_save_verdict_meta( $post_id, $post ) { function peptide_starter_save_verdict_meta( $post_id, $post ) {
// Verify nonce. // Verify nonce.
if ( ! isset( $_POST['peptide_starter_verdict_nonce'] ) if ( ! isset( $_POST['peptide_starter_verdict_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['peptide_starter_verdict_nonce'] ), 'peptide_starter_verdict_nonce' ) ) {
|| ! wp_verify_nonce( sanitize_key( $_POST['peptide_starter_verdict_nonce'] ), 'peptide_starter_verdict_nonce' )
) {
return; return;
} }
// Only run for peptide CPT. // Only run for pr_peptide CPT.
if ( 'peptide' !== $post->post_type ) { if ( 'pr_peptide' !== $post->post_type ) {
return; return;
} }
// Allow only editors and admins.
if ( ! current_user_can( 'edit_post', $post_id ) ) { if ( ! current_user_can( 'edit_post', $post_id ) ) {
return; return;
} }
$allowed_states = array( 'established', 'promising', 'investigational', 'insufficient', 'cautionary' ); $allowed_states = array( 'established', 'promising', 'investigational', 'insufficient', 'cautionary' );
$allowed_glyphs = array( '✓', '!', '?', '⊘', '⚠' );
// Save verdict_state. // Save verdict_state.
if ( isset( $_POST['verdict_state'] ) ) { if ( isset( $_POST['verdict_state'] ) ) {
@@ -260,17 +163,7 @@ function peptide_starter_save_verdict_meta( $post_id, $post ) {
} }
} }
// Save verdict_text. // Save signal rows.
if ( isset( $_POST['verdict_text'] ) ) {
$text = sanitize_textarea_field( wp_unslash( $_POST['verdict_text'] ) );
if ( ! empty( $text ) ) {
update_post_meta( $post_id, 'verdict_text', $text );
} else {
delete_post_meta( $post_id, 'verdict_text' );
}
}
// Save signal rows and their per-row glyphs.
for ( $i = 1; $i <= 3; $i++ ) { for ( $i = 1; $i <= 3; $i++ ) {
$field_name = 'signal_row_' . $i; $field_name = 'signal_row_' . $i;
if ( isset( $_POST[ $field_name ] ) ) { if ( isset( $_POST[ $field_name ] ) ) {
@@ -281,20 +174,6 @@ function peptide_starter_save_verdict_meta( $post_id, $post ) {
delete_post_meta( $post_id, $field_name ); delete_post_meta( $post_id, $field_name );
} }
} }
$glyph_field = 'signal_row_' . $i . '_glyph';
if ( isset( $_POST[ $glyph_field ] ) ) {
$glyph = wp_unslash( $_POST[ $glyph_field ] );
if ( in_array( $glyph, $allowed_glyphs, true ) ) {
update_post_meta( $post_id, $glyph_field, $glyph );
} else {
delete_post_meta( $post_id, $glyph_field );
} }
}
}
// Save has_partner_link (checkbox — present = true, absent = false).
$has_partner = isset( $_POST['has_partner_link'] ) && '1' === $_POST['has_partner_link'];
update_post_meta( $post_id, 'has_partner_link', $has_partner ? '1' : '0' );
} }
add_action( 'save_post', 'peptide_starter_save_verdict_meta', 10, 2 ); add_action( 'save_post', 'peptide_starter_save_verdict_meta', 10, 2 );

View File

@@ -42,5 +42,4 @@ get_header();
</div> </div>
</main> </main>
<?php <?php get_footer();
get_footer();

View File

@@ -33,8 +33,8 @@ get_header();
<section class="ps-tool-page"> <section class="ps-tool-page">
<div class="ps-container"> <div class="ps-container">
<?php <?php
if ( shortcode_exists( 'prc_calculator' ) ) { if ( shortcode_exists( 'peptide_tools_calculator' ) ) {
echo do_shortcode( '[prc_calculator]' ); echo do_shortcode( '[peptide_tools_calculator]' );
} else { } else {
?> ?>
<div class="ps-tool-fallback"> <div class="ps-tool-fallback">

View File

@@ -41,5 +41,4 @@ get_header();
</div> </div>
</main> </main>
<?php <?php get_footer();
get_footer();

View File

@@ -1,46 +0,0 @@
<?php
/**
* Template Name: Our Methodology
*
* Two-column layout with sticky sidebar ToC for the editorial methodology page.
* Mirrors page-how-we-review.php layout; CSS classes use page-our-methodology prefix.
*
* @package peptide-starter
*/
get_header();
?>
<main id="main" class="site-main">
<div class="page-our-methodology">
<div class="page-our-methodology__content">
<?php
while ( have_posts() ) {
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
</header>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php
}
?>
</div>
<?php if ( ! empty( get_the_content() ) ) : ?>
<aside class="page-our-methodology__sidebar">
<nav class="page-toc" aria-label="<?php esc_attr_e( 'Page contents', 'peptide-starter' ); ?>">
<h3><?php esc_html_e( 'Contents', 'peptide-starter' ); ?></h3>
<!-- JavaScript-generated ToC will be inserted here -->
</nav>
</aside>
<?php endif; ?>
</div>
</main>
<?php
get_footer();

View File

@@ -1,44 +0,0 @@
<?php
/**
* Template Name: Subscribe
*
* Single-column, signup-form-centric layout for the /subscribe/ landing page.
* Lighter than the methodology / how-we-review templates — no sticky two-column ToC.
* This is a conversion page, not a reference page.
*
* Who calls it: WordPress page loader when page_template meta = page-subscribe.php
* Dependencies: Beehiiv embed script (loaded inline below); attribution.js loaded
* site-wide via peptide_starter_enqueue_beehiiv_attribution() in functions.php
*
* @see footer.php — newsletter CTA in footer column 1
* @see single-peptide.php — compact inline form on monograph pages
* @package peptide-starter
*/
get_header();
?>
<main id="main" class="site-main">
<div class="page-subscribe">
<div class="page-subscribe__content">
<?php
while ( have_posts() ) {
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
</header>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php
}
?>
</div>
</div>
</main>
<?php
get_footer();

View File

@@ -139,25 +139,6 @@ get_header();
?> ?>
</article> </article>
<!-- Newsletter Signup — compact inline form below monograph content -->
<div class="peptide-newsletter-cta" style="margin-top: var(--spacing-3xl); padding: var(--spacing-2xl); background: var(--color-surface-subtle, #f8f8f6); border-radius: 8px; border: 1px solid var(--color-border-default);">
<p style="margin: 0 0 var(--spacing-md); font-size: 0.95rem; color: var(--color-text-secondary);">
<?php esc_html_e( 'Want updates on monographs like this? One email a week, no spam.', 'peptide-starter' ); ?>
</p>
<div class="beehiiv-embed-wrap" style="max-width: 100%; overflow: hidden;">
<script async src="https://subscribe-forms.beehiiv.com/embed.js"></script>
<iframe
src="https://subscribe-forms.beehiiv.com/6edcd482-cbea-4767-8777-97ac17d6d609"
class="beehiiv-embed"
data-test-id="beehiiv-embed"
frameborder="0"
scrolling="no"
style="width: 100%; max-width: 100%; height: 180px; margin: 0; border-radius: 4px; background-color: transparent; box-shadow: 0 0 #0000;"
title="<?php esc_attr_e( 'Subscribe to Repo Weekly', 'peptide-starter' ); ?>"
> </iframe>
</div>
</div>
<!-- Navigation --> <!-- Navigation -->
<nav class="post-navigation" style="margin-top: var(--spacing-3xl); padding-top: var(--spacing-2xl); border-top: 1px solid var(--color-border-default);"> <nav class="post-navigation" style="margin-top: var(--spacing-3xl); padding-top: var(--spacing-2xl); border-top: 1px solid var(--color-border-default);">
<div style="display: flex; justify-content: space-between; gap: var(--spacing-lg);"> <div style="display: flex; justify-content: space-between; gap: var(--spacing-lg);">

300
style.css
View File

@@ -4,7 +4,7 @@ Theme URI: https://github.com/peptiderepo/peptide-starter
Description: Premium, accessible WordPress theme for scientific peptide reference database Description: Premium, accessible WordPress theme for scientific peptide reference database
Author: Peptide Repo Author: Peptide Repo
Author URI: https://peptiderepo.com Author URI: https://peptiderepo.com
Version: 2.2.2 Version: 2.0.0-alpha.1
License: GPL v2 or later License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: peptide-starter Text Domain: peptide-starter
@@ -1674,13 +1674,6 @@ body .pn-article-title {
margin-bottom: var(--spacing-sm); margin-bottom: var(--spacing-sm);
} }
/* Collapse empty Elementor/page-builder containers that add phantom space */
.page-content .elementor-section:empty,
.page-content .elementor-widget-wrap:empty,
.page-content .elementor-element:empty,
.page-content div:empty:not([data-peptide-news-feed]) {
display: none;
}
/* ---- BACK LINK (breadcrumb-style) ---- */ /* ---- BACK LINK (breadcrumb-style) ---- */
@@ -3171,294 +3164,3 @@ html[data-theme="dark"] .ps-verify-required {
font-size: 0.85rem; font-size: 0.85rem;
opacity: 0.7; opacity: 0.7;
} }
/* ============================================================================
HOMEPAGE — VERDICT EXPLAINER SECTION
hero-verdict-explainer: "Every peptide gets a verdict. Here's what they mean."
5-state explainer grid above the featured verdict cards.
============================================================================ */
.hero-verdict-explainer {
padding: var(--spacing-3xl) 0; /* horizontal padding handled by ps-container inside */
background-color: var(--color-bg-secondary);
}
.hero-verdict-explainer__heading {
max-width: var(--container-lg);
margin: 0 auto var(--spacing-xl);
font-size: var(--text-h3);
font-weight: var(--text-h3-weight);
line-height: var(--text-h3-lh);
letter-spacing: var(--text-h3-ls);
color: var(--color-text-primary);
text-align: center;
}
.hero-verdict-explainer__grid {
max-width: var(--container-lg);
margin: 0 auto;
display: grid;
grid-template-columns: 1fr; /* single column on narrow phones; 2-col added at 520px below */
gap: var(--spacing-md);
}
.hero-verdict-explainer__item {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
min-width: 0; /* prevent grid item from blowing out column */
}
/* Cap pill width to column so Insufficient Evidence pill never overflows */
.hero-verdict-explainer__item .pr-verdict {
max-width: 100%;
}
.hero-verdict-explainer__desc {
font-size: var(--text-body-md);
color: var(--color-text-secondary);
line-height: 1.5;
}
.hero-verdict-explainer__cta-line {
max-width: var(--container-lg);
margin: var(--spacing-xl) auto 0;
text-align: center;
}
.hero-verdict-explainer__cta-line a {
color: var(--color-brand-primary);
font-size: var(--text-body-lg);
font-weight: 500;
text-decoration: underline;
text-underline-offset: 3px;
}
.hero-verdict-explainer__cta-line a:hover {
color: var(--color-link-hover);
}
/* ============================================================================
HOMEPAGE — FEATURED VERDICT CARDS SECTION
hero-featured-verdicts: 3 pinned monographs showing the verdict system in action.
Classes mirror BEM pattern used in front-page.php.
============================================================================ */
.hero-featured-verdicts {
padding: var(--spacing-3xl) var(--spacing-lg);
background-color: var(--color-bg-primary);
}
.hero-featured-verdicts__title {
font-size: var(--text-h2);
font-weight: var(--text-h2-weight);
line-height: var(--text-h2-lh);
letter-spacing: var(--text-h2-ls);
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.hero-featured-verdicts__subtitle {
font-size: var(--text-body-lg);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-2xl);
}
.hero-featured-verdicts__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
/* Individual verdict card */
.hero-verdict-card {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-xl);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-left: 3px solid var(--color-border-default);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.hero-verdict-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
/* Badge sits at top of card — already styled by .pr-verdict in brand.css */
.hero-verdict-card__badge {
align-self: flex-start;
}
/* Tint left border to match verdict state — uses CSS :has() (supported in all modern browsers) */
.hero-verdict-card:has(.pr-verdict--established) {
border-left-color: var(--verdict-established-bg, #059669);
}
.hero-verdict-card:has(.pr-verdict--promising) {
border-left-color: var(--verdict-promising-bg, #10b981);
}
.hero-verdict-card:has(.pr-verdict--investigational) {
border-left-color: var(--verdict-investigational-bg, #f59e0b);
}
.hero-verdict-card:has(.pr-verdict--insufficient) {
border-left-color: var(--verdict-insufficient-bg, #6b7280);
}
.hero-verdict-card:has(.pr-verdict--cautionary) {
border-left-color: var(--verdict-cautionary-bg, #dc2626);
}
.hero-verdict-card__title {
font-size: var(--text-h3);
font-weight: var(--text-h3-weight);
line-height: var(--text-h3-lh);
letter-spacing: var(--text-h3-ls);
margin: 0;
}
.hero-verdict-card__title a {
color: var(--color-brand-primary);
text-decoration: none;
}
.hero-verdict-card__title a:hover {
text-decoration: underline;
text-underline-offset: 3px;
color: var(--color-link-hover);
}
.hero-verdict-card__summary {
font-size: var(--text-body-lg);
color: var(--color-text-primary);
line-height: 1.65;
}
/* Signal rows list */
.hero-verdict-card__signals {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-md);
background-color: var(--color-bg-secondary);
border-radius: var(--radius-md);
list-style: none;
}
.hero-verdict-card__signal-item {
display: flex;
align-items: baseline;
gap: var(--spacing-sm);
font-size: var(--text-body-md);
color: var(--color-text-secondary);
line-height: 1.5;
}
.hero-verdict-card__signal-glyph {
flex-shrink: 0;
font-size: 0.85em;
}
/* "Read the full evidence →" link */
.hero-verdict-card__link {
align-self: flex-start;
color: var(--color-brand-primary);
font-size: var(--text-body-lg);
font-weight: 500;
text-decoration: none;
margin-top: auto;
}
.hero-verdict-card__link:hover {
text-decoration: underline;
text-underline-offset: 3px;
color: var(--color-link-hover);
}
/* "Browse all peptides" footer row */
.hero-featured-verdicts__footer {
text-align: center;
}
/* ---- RESPONSIVE — Featured Verdict Cards ---- */
@media (min-width: 520px) {
.hero-verdict-explainer__grid {
grid-template-columns: repeat(2, 1fr); /* 520px+ gives columns wide enough for all pills */
}
}
@media (min-width: 640px) {
.hero-verdict-explainer__grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 900px) {
.hero-featured-verdicts__grid {
grid-template-columns: repeat(3, 1fr);
}
.hero-verdict-explainer__grid {
grid-template-columns: repeat(5, 1fr);
}
}
/* ==========================================================================
Abbreviation tooltip (.pr-abbr / .pr-abbr-popover)
Inline tap-targets for glossary terms in the verdict legend strip.
@see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr
@see assets/js/abbr-tooltip.js — JS popover logic
========================================================================== */
.pr-abbr {
border-bottom: 1px dashed currentColor;
cursor: help;
text-decoration: none;
white-space: nowrap;
/* Slightly muted so the dashed line reads as "interactive" not "broken link". */
opacity: 0.85;
transition: opacity 0.15s ease;
}
.pr-abbr:hover,
.pr-abbr:focus,
.pr-abbr--active {
opacity: 1;
outline: none;
}
.pr-abbr-popover {
position: absolute;
z-index: 9999;
max-width: min( 280px, calc( 100vw - 32px ) );
padding: 0.6rem 0.85rem;
border-radius: 6px;
font-size: 0.8125rem;
line-height: 1.5;
/* Use theme surface colours so dark-mode inherits correctly. */
background-color: var(--color-surface-elevated, #1e1e2e);
color: var(--color-text-primary, #e2e2f0);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
/* Subtle fade-in. */
animation: pr-popover-in 0.12s ease forwards;
}
@keyframes pr-popover-in {
from {
opacity: 0;
transform: translateY( -4px );
}
to {
opacity: 1;
transform: translateY( 0 );
}
}

View File

@@ -2,10 +2,8 @@
/** /**
* Template Part: Research Module Cards * Template Part: Research Module Cards
* *
* Renders a 5-card grid of research modules (tools/sections) for the front page. * Renders a 6-card grid of research modules (tools/sections) for the front page.
* Each card checks if the target plugin is active; inactive cards are dimmed. * Each card checks if the target plugin is active; inactive cards are dimmed.
* Cards with 'coming_soon' => true display a "Coming soon" badge overlaid in
* the top-right corner — layered on top of the --inactive visual treatment.
* *
* @see front-page.php — includes this template part * @see front-page.php — includes this template part
* @see style.css — .ps-module-* classes * @see style.css — .ps-module-* classes
@@ -23,48 +21,48 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
// Module card definitions with shortcode checks for graceful degradation. // Module card definitions with shortcode checks for graceful degradation.
// 'coming_soon' => true adds a "Coming soon" badge without removing existing
// --inactive styling — the badge layers on top of the dim/no-hover treatment.
$modules = array( $modules = array(
array( array(
'title' => __( 'Peptides', 'peptide-starter' ), 'title' => __( 'Peptides', 'peptide-starter' ),
'description' => __( 'Read the verdicts. 20 monographs across 5 evidence states, growing weekly.', 'peptide-starter' ), 'description' => __( 'Browse the complete reagent directory with protocols and research parameters.', 'peptide-starter' ),
'url' => '/peptides', 'url' => '/peptides',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"/><path d="M5 12C5 8.13 8.13 5 12 5s7 3.13 7 7-3.13 7-7 7-7-3.13-7-7z"/><circle cx="12" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="16" r="1.5" fill="currentColor" stroke="none"/></svg>', 'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"/><path d="M5 12C5 8.13 8.13 5 12 5s7 3.13 7 7-3.13 7-7 7-7-3.13-7-7z"/><circle cx="12" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="16" r="1.5" fill="currentColor" stroke="none"/></svg>',
'available' => shortcode_exists( 'peptide_directory' ) || post_type_exists( 'peptide' ), 'available' => shortcode_exists( 'peptide_directory' ) || post_type_exists( 'peptide' ),
'coming_soon' => false,
), ),
array( array(
'title' => __( 'Calculator', 'peptide-starter' ), 'title' => __( 'Calculator', 'peptide-starter' ),
'description' => __( 'Reconstitute a peptide accurately. Enter the vial, the bacteriostatic water, the target dose; get the units.', 'peptide-starter' ), 'description' => __( 'Precision reconstitution calculator for accurate laboratory preparations.', 'peptide-starter' ),
'url' => '/calculator', 'url' => '/calculator',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>', 'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>',
'available' => shortcode_exists( 'prc_calculator' ), 'available' => shortcode_exists( 'peptide_tools_calculator' ),
'coming_soon' => false,
), ),
array( array(
'title' => __( 'Protocol Builder', 'peptide-starter' ), 'title' => __( 'Protocol Builder', 'peptide-starter' ),
'description' => __( 'Build a protocol you can hand to anyone. Doses, schedule, notes, reviewable history.', 'peptide-starter' ), 'description' => __( 'Interactive multi-step wizard for designing research protocols.', 'peptide-starter' ),
'url' => '/protocol-builder', 'url' => '/protocol-builder',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>', 'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>',
'available' => shortcode_exists( 'peptide_tools_protocol_builder' ), 'available' => shortcode_exists( 'peptide_tools_protocol_builder' ),
'coming_soon' => true,
), ),
array( array(
'title' => __( 'Tracker', 'peptide-starter' ), 'title' => __( 'Tracker', 'peptide-starter' ),
'description' => __( 'Log doses and timing. For people running their own protocols, and people supervising someone else\'s.', 'peptide-starter' ), 'description' => __( 'Log doses, manage active compounds, and forecast vial depletion timelines.', 'peptide-starter' ),
'url' => '/tracker', 'url' => '/tracker',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>', 'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>',
'available' => shortcode_exists( 'peptide_tracker' ), 'available' => shortcode_exists( 'peptide_tracker' ),
'coming_soon' => true, ),
array(
'title' => __( 'Science Feed', 'peptide-starter' ),
'description' => __( 'Latest research publications, clinical studies, and laboratory insights.', 'peptide-starter' ),
'url' => '/news',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 002-2V4a2 2 0 00-2-2H8a2 2 0 00-2 2v16a2 2 0 01-2 2zm0 0a2 2 0 01-2-2v-9c0-1.1.9-2 2-2h2"/><line x1="10" y1="6" x2="18" y2="6"/><line x1="10" y1="10" x2="18" y2="10"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
'available' => shortcode_exists( 'peptide_news' ),
), ),
array( array(
'title' => __( 'Subject Log', 'peptide-starter' ), 'title' => __( 'Subject Log', 'peptide-starter' ),
'description' => __( 'Track outcomes over time. Subjective markers, lab results, protocol changes — your own data, kept private.', 'peptide-starter' ), 'description' => __( 'Longitudinal tracking for biomarker data and protocol history.', 'peptide-starter' ),
'url' => '/subject-log', 'url' => '/subject-log',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', 'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
'available' => shortcode_exists( 'peptide_tracker_subject_log' ), 'available' => shortcode_exists( 'peptide_tracker_subject_log' ),
'coming_soon' => true,
), ),
); );
?> ?>
@@ -72,9 +70,9 @@ $modules = array(
<section class="ps-modules-section"> <section class="ps-modules-section">
<div class="ps-container-wide"> <div class="ps-container-wide">
<div class="ps-modules-header"> <div class="ps-modules-header">
<h2 class="ps-modules-title"><?php esc_html_e( 'Tools', 'peptide-starter' ); ?></h2> <h2 class="ps-modules-title"><?php esc_html_e( 'Research Modules', 'peptide-starter' ); ?></h2>
<p class="ps-modules-subtitle"> <p class="ps-modules-subtitle">
<?php esc_html_e( 'Free to use. Built for people running their own protocols, and people deciding whether they should.', 'peptide-starter' ); ?> <?php esc_html_e( 'Tools and resources for laboratory research and data management.', 'peptide-starter' ); ?>
</p> </p>
</div> </div>
@@ -82,9 +80,6 @@ $modules = array(
<?php foreach ( $modules as $module ) : ?> <?php foreach ( $modules as $module ) : ?>
<a href="<?php echo esc_url( home_url( $module['url'] ) ); ?>" <a href="<?php echo esc_url( home_url( $module['url'] ) ); ?>"
class="ps-module-card <?php echo $module['available'] ? '' : 'ps-module-card--inactive'; ?>"> class="ps-module-card <?php echo $module['available'] ? '' : 'ps-module-card--inactive'; ?>">
<?php if ( ! empty( $module['coming_soon'] ) ) : ?>
<span class="ps-module-card__badge"><?php esc_html_e( 'Coming soon', 'peptide-starter' ); ?></span>
<?php endif; ?>
<div class="ps-module-card__icon"> <div class="ps-module-card__icon">
<?php <?php
// SVG icons are defined in this template — safe to output directly. // SVG icons are defined in this template — safe to output directly.
@@ -101,4 +96,3 @@ $modules = array(
</div> </div>
</div> </div>
</section> </section>

View File

@@ -17,26 +17,11 @@ $show_label = isset( $args['show_label'] ) ? (bool) $args['show_label'] : tru
// State configuration. // State configuration.
$states = array( $states = array(
'established' => array( 'established' => array( 'glyph' => '✓', 'label' => 'Established' ),
'glyph' => '✓', 'promising' => array( 'glyph' => '◐', 'label' => 'Promising' ),
'label' => 'Established', 'investigational' => array( 'glyph' => '?', 'label' => 'Investigational' ),
), 'insufficient' => array( 'glyph' => '⊘', 'label' => 'Insufficient Evidence' ),
'promising' => array( 'cautionary' => array( 'glyph' => '⚠', 'label' => 'Cautionary' ),
'glyph' => '◐',
'label' => 'Promising',
),
'investigational' => array(
'glyph' => '?',
'label' => 'Investigational',
),
'insufficient' => array(
'glyph' => '⊘',
'label' => 'Insufficient Evidence',
),
'cautionary' => array(
'glyph' => '⚠',
'label' => 'Cautionary',
),
); );
// Early exit if invalid state. // Early exit if invalid state.

View File

@@ -2,17 +2,10 @@
/** /**
* Verdict Card Component (Monograph Hero) * Verdict Card Component (Monograph Hero)
* *
* Displays the full verdict card hero for a peptide post. * Displays a full verdict card for a pr_peptide post.
* Reads verdict_state, verdict_text, signal_row_1/2/3, and * Reads verdict_state and signal_row_1/2/3 from post meta.
* signal_row_1/2/3_glyph from post meta. * Accepts $args['post_id'].
* *
* Who calls it: single-peptide.php for posts with a verdict_state.
* Depends on: inc/verdict-meta.php (meta registration),
* template-parts/verdict/badge.php,
* template-parts/verdict/evidence-row.php.
*
* @see inc/verdict-meta.php
* @see single-peptide.php
* @package peptide-starter * @package peptide-starter
*/ */
@@ -22,32 +15,27 @@ if ( ! defined( 'ABSPATH' ) ) {
$post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : get_the_ID(); $post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : get_the_ID();
// Get verdict state; bail early if unset. // Get verdict state from post meta.
$verdict_state = get_post_meta( $post_id, 'verdict_state', true ); $verdict_state = get_post_meta( $post_id, 'verdict_state', true );
// Early exit if no verdict state is set.
if ( empty( $verdict_state ) ) { if ( empty( $verdict_state ) ) {
return; return;
} }
// Get opinionated verdict text; fall back to placeholder if not yet authored. // Collect signal rows.
$verdict_text = get_post_meta( $post_id, 'verdict_text', true );
if ( empty( $verdict_text ) ) {
$verdict_text = __( 'This peptide merits careful scientific review. Please consult with a healthcare provider before use.', 'peptide-starter' );
}
// Collect signal rows with their per-row glyphs.
// Glyph comes from the signal_row_N_glyph meta field; if unset, no glyph is
// rendered. This is intentional — a missing glyph is better than a wrong one.
$signals = array(); $signals = array();
for ( $i = 1; $i <= 3; $i++ ) { for ( $i = 1; $i <= 3; $i++ ) {
$signal_text = get_post_meta( $post_id, 'signal_row_' . $i, true ); $signal_text = get_post_meta( $post_id, 'signal_row_' . $i, true );
$signal_glyph = get_post_meta( $post_id, 'signal_row_' . $i . '_glyph', true );
if ( ! empty( $signal_text ) ) { if ( ! empty( $signal_text ) ) {
$signals[] = array( $signals[] = array(
'glyph' => $signal_glyph, // May be empty string; evidence-row handles that. 'glyph' => $i === 1 ? '✓' : ( $i === 2 ? '!' : '?' ),
'label' => $signal_text, 'label' => $signal_text,
); );
} }
} }
$post = get_post( $post_id );
?> ?>
<div class="pr-verdict-card"> <div class="pr-verdict-card">
<div class="pr-verdict-card__header"> <div class="pr-verdict-card__header">
@@ -63,13 +51,16 @@ for ( $i = 1; $i <= 3; $i++ ) {
); );
?> ?>
</div> </div>
<!-- last_reviewed_date timestamp deferred to v1.1 — WP post date != editorial review date --> <time class="pr-verdict-card__updated" datetime="<?php echo esc_attr( get_the_date( 'c', $post_id ) ); ?>">
<?php echo esc_html( 'Updated ' . get_the_date( 'M d, Y', $post_id ) ); ?>
</time>
</div> </div>
<h1 class="pr-verdict-card__title"><?php echo esc_html( get_the_title( $post_id ) ); ?></h1> <h1 class="pr-verdict-card__title"><?php echo esc_html( get_the_title( $post_id ) ); ?></h1>
<p class="pr-verdict-card__summary"> <p class="pr-verdict-card__summary">
<?php <?php
// Get post excerpt or first 100 chars of content.
$summary = get_the_excerpt( $post_id ); $summary = get_the_excerpt( $post_id );
if ( empty( $summary ) ) { if ( empty( $summary ) ) {
$content = get_the_content( null, false, $post_id ); $content = get_the_content( null, false, $post_id );
@@ -91,7 +82,10 @@ for ( $i = 1; $i <= 3; $i++ ) {
<div class="pr-verdict-card__verdict-label"><?php esc_html_e( 'Our verdict:', 'peptide-starter' ); ?></div> <div class="pr-verdict-card__verdict-label"><?php esc_html_e( 'Our verdict:', 'peptide-starter' ); ?></div>
<div class="pr-verdict-card__verdict-text"> <div class="pr-verdict-card__verdict-text">
<?php echo wp_kses_post( nl2br( $verdict_text ) ); ?> <?php
// Placeholder verdict text; can be extended with a custom meta field.
echo esc_html__( 'This peptide merits careful scientific review. Please consult with a healthcare provider before use.', 'peptide-starter' );
?>
</div> </div>
<div class="pr-verdict-card__cta-group"> <div class="pr-verdict-card__cta-group">