77 KiB
Portfolio 2026 Theme Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Rebuild the surface of the ks-portfolio WordPress theme to match the Portfolio 2026 identity (Carbon Blue dark atmosphere + Raleway typography + project catalog focused on GitHub/Gitea-backed Projects), resolve the documented source conflicts, and ship a working tested theme.
Architecture: SoloFrame Evo base (Tailwind 4 + ACF blocks + namespaced PHP classes), namespaced from SoloFrameEvo → KsPortfolio. The theme is a pure presentation layer over the installed projects-portfolio plugin — no new CPT registration. Tokens drive every color/typography decision; templates pull data from WordPress content and the plugin's projects_portfolio_get_*() helpers with empty-state fallbacks.
Tech Stack: PHP 8.1+ (WordPress 6.x), Tailwind CSS 4, ACF Pro (Secure Custom Fields), Playwright + axe-core, PHPCS + WPCS, Raleway (Google Fonts).
Global Constraints
- PHP namespace:
KsPortfolio(wasSoloFrameEvo). Every PHP file in the theme uses this namespace. Composer text-domain:ks-portfolio. - Text domain:
ks-portfoliofor every__(),_e(),esc_html__(), etc. call. Theme slug instyle.cssheader matches. - Tokens only: no raw hex outside
theme.jsoneditor palette,styles/base/colors.css, or the Carbon Blue partial. Reference viavar(--color-*)/var(--font-*)/var(--text-*). --font-headings: var(--font-sans)is defined instyles/base/typography.css.- Body surface:
background-color: #100e0b; color: var(--color-on-surface);. The lightmain#maincontent { background: var(--color-background); }rule is removed. - Carbon Blue atmospheric field lives in
views/partials/aura-bg.phpand is loaded onfront-page.phpandarchive-projects.php. The container must NOT carrybackground-color(blend-mode architecture). - Decorative layers:
position: absolute; inset: 0; pointer-events: none; aria-hidden="true". - Accessibility target: WCAG 2.1 AA. Every template has exactly one
<h1>. Skip link preserved.prefers-reduced-motionhonored. - Security: all forms use
wp_nonce_field; state-changing requests usewp_verify_nonce; dynamic output uses the rightesc_*for context; external URLsesc_url+rel="noopener noreferrer"whentarget="_blank". - No
picsum.photosURLs anywhere in shipped theme. - No invented project copy, statistics, or testimonials. Empty states use the documented copy:
Nothing here yet...,No published projects found.,Page Not Found,Sorry, the page you're looking for doesn't exist. - Test scope: Chromium + axe only. Firefox / WebKit / mobile are out of scope. OWASP ZAP out of scope — security audit is PHPCS + grep + manual review.
- Local base URL:
http://portfolio.test(matches.env).
File Structure
Modified
style.css— already correct; verify only.functions.php— namespace swap, include newclass-projects.php, include new aura-bg partial bootstrap.header.php— namespace swap, dark surface, three-region responsive header, aura-bg on home/archive.footer.php— namespace swap, dark surface, footer links match Carbon Blue.front-page.php— full home template replacing 8-line stub.index.php— namespace swap, dark surface, removepicsum.photos, empty-state copy.single.php— namespace swap, dark surface, removepicsum.photos.page.php— namespace swap, dark surface.search.php— namespace swap, dark surface, removepicsum.photos.404.php— namespace swap, dark surface, documented empty-state copy.lib/activation.php— namespace swap.lib/class-acf.php— namespace swap.lib/class-breadcrumbs.php— namespace swap.lib/class-enqueue.php— namespace swap, verify Raleway + theme.css enqueue.lib/class-menuitems.php— namespace swap.lib/class-resources.php— namespace swap (keep CPT registration as-is).lib/extras.php— namespace swap.lib/helpers.php— namespace swap, addgetTheTitle(),hasSidebar(),customExcerpt(),getFieldValue().lib/hooks.php— namespace swap.lib/search-features.php— namespace swap.lib/show-template.php— namespace swap.theme.json— fixstylesreferences to existing slugs, add dark surface palette.styles/base/colors.css— add Carbon Blue tokens.styles/base/typography.css— define--font-headings.styles/base/global.css— dark body, remove lightmainrule, dark container / section tweaks.styles/base/forms.css— dark form surfaces.styles/navigation/nav-*.css— dark link colors.styles/blocks/buttons.css— verify dark-surface contrast for primary/secondary buttons.views/components/nav-aux.php,nav-main.php,nav-main__toggle.php— namespace swap.views/partials/page-hero.php— namespace swap, dark surface defaults.views/partials/social-media.php— namespace swap, dark color overrides.views/forms/search.php— escape audit only.playwright.config.js— set base URL, keep chromium only.
New
archive-projects.php— project archive template.single-projects.php— project detail template.taxonomy-project-type.php— project type archive template.views/partials/aura-bg.php— Carbon Blue atmospheric field.views/components/project-card.php— reusable project card.views/components/project-stat-card.php— Downloads/Stars/Forks/Issues tile.views/components/project-meta-row.php— repository metadata table row.lib/class-projects.php— thin wrapper around plugin helpers + provider-aware defaults.tests/home.spec.js— Playwright + axe on home.tests/archive.spec.js— Playwright + axe on project archive.tests/single.spec.js— Playwright + axe on project detail.docs/superpowers/specs/2026-08-16-portfolio-2026-design.md— already committed.docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md— final report (delivered at end).
Removed
screenshot.png(Basic WP) — replaced at end.picsum.photosURLs inindex.php,search.php.- Legacy hex values in pagination CSS.
Task 1: Verify environment + capture baseline screenshots
Files:
- Inspect:
playwright.config.js - Inspect:
.env - Inspect:
static/dist/theme.css - Inspect:
wp-content/plugins/projects-portfolio/(installed)
Goal: Confirm the local site at http://portfolio.test is reachable, the existing Playwright suite runs, and capture a baseline of each mockup reference page so we can diff later.
- Step 1: Confirm base URL and Playwright config
Open playwright.config.js. Confirm chromium is enabled and baseURL is set to http://portfolio.test. If baseURL is commented out, set:
use: {
baseURL: 'http://portfolio.test',
trace: 'on-first-retry',
ignoreHTTPSErrors: true,
},
- Step 2: Confirm Playwright + axe dependencies are installed
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright --version
Expected: prints a version (e.g. Version 1.52.x).
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm ls @axe-core/playwright @playwright/test --depth=0
Expected: both packages installed.
- Step 3: Confirm the projects-portfolio plugin is present
Run (PowerShell): Get-ChildItem "C:\Users\ksolo\Herd\portfolio\wp-content\plugins\projects-portfolio" | Select-Object Name
Expected: shows projects-portfolio.php, README.md, includes/, templates/, etc.
- Step 4: Probe local site reachability with a 3-second timeout
Run (PowerShell):
try { (Invoke-WebRequest -Uri "http://portfolio.test" -UseBasicParsing -TimeoutSec 3).StatusCode } catch { "unreachable: $($_.Exception.Message)" }
Expected: prints 200. If unreachable: ..., stop and ask the user — we cannot run browser tests without the local site.
- Step 5: Run the existing Playwright suite as baseline
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test tests/site-a11y.spec.js --reporter=list
Expected: may fail (axe violations expected on current light theme) — that's fine, we capture baseline.
- Step 6: Commit no changes (this task is verification only)
If you modified playwright.config.js, run:
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add playwright.config.js
git commit -m "chore: set Playwright baseURL to portfolio.test"
Task 2: Define --font-headings + dark body surface
Files:
- Modify:
styles/base/typography.css(add--font-headings) - Modify:
styles/base/global.css(dark body, remove light main rule)
Goal: Resolve the unresolved token from notes/DESIGN.md §12 and flip the body surface to Carbon Blue dark.
- Step 1: Add
--font-headingsto typography.css
In styles/base/typography.css, inside the @theme { ... } block (after --font-sans), add:
--font-headings: var(--font-sans);
- Step 2: Darken body + remove light main rule in global.css
In styles/base/global.css, find the existing rule:
main#maincontent {
background-color: var(--color-background);
color: var(--color-text);
margin: 0;
padding: 0 0 4rem;
}
Replace with:
main#maincontent {
background-color: transparent;
color: var(--color-on-surface, #e3e1e9);
margin: 0;
padding: 0 0 4rem;
}
- Step 3: Set dark body in typography.css
In styles/base/typography.css, find the existing body { ... } block and replace with:
body {
background-color: #100e0b;
color: var(--color-on-surface, #e3e1e9);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--line-height);
}
- Step 4: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
Expected: static/dist/theme.css rebuilt without errors.
- Step 5: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/base/typography.css styles/base/global.css static/dist/theme.css
git commit -m "feat(theme): add --font-headings; flip body to Carbon Blue dark"
Task 3: Add Carbon Blue tokens to colors.css + theme.json
Files:
- Modify:
styles/base/colors.css(add Carbon Blue + Material-derived tokens) - Modify:
theme.json(add dark palette; fixstylespreset references)
Goal: Expose Carbon Blue / dark-surface tokens through both CSS custom properties and the WordPress block-editor palette. Fix the background / text preset reference mismatch from notes/DESIGN.md §12.
- Step 1: Append Carbon Blue tokens to colors.css
Open styles/base/colors.css. Inside the @theme { ... } block, append:
/* Carbon Blue / dark surface tokens (Portfolio 2026) */
--color-surface: #121318;
--color-surface-dim: #121318;
--color-surface-bright: #38393f;
--color-surface-container-lowest: #0d0e13;
--color-surface-container-low: #1a1b21;
--color-surface-container: #1e1f25;
--color-surface-container-high: #292a2f;
--color-surface-container-highest:#34343a;
--color-on-surface: #e3e1e9;
--color-on-surface-variant:#c5c5d3;
--color-outline: #8f909d;
--color-outline-variant: #444651;
--color-page-base: #100e0b;
--color-backlight: rgba(96, 165, 250, 0.20);
--color-primary-fixed: #dce1ff;
--color-primary-fixed-dim: #b6c4ff;
--color-on-primary-fixed: #00164e;
--color-on-primary-fixed-variant: #264191;
--color-success: #5ccf91;
--color-warning: #f4e04d;
--color-danger: #cc3b3b;
--color-info: #3d7ab0;
(If --color-success / --color-warning / --color-danger / --color-info already exist in oklch, REMOVE the existing lines first so the new hex values are the only definitions.)
- Step 2: Update theme.json palette
Open theme.json. In the settings.color.palette array, ADD these entries (don't remove existing ones):
{
"slug": "theme-surface",
"color": "#121318",
"name": "Theme Surface"
},
{
"slug": "theme-on-surface",
"color": "#e3e1e9",
"name": "Theme On Surface"
},
{
"slug": "theme-outline",
"color": "#8f909d",
"name": "Theme Outline"
},
{
"slug": "theme-outline-variant",
"color": "#444651",
"name": "Theme Outline Variant"
},
{
"slug": "theme-page-base",
"color": "#100e0b",
"name": "Theme Page Base"
}
- Step 3: Fix theme.json styles preset references
In theme.json, find the top-level styles block:
"styles": {
"color": {
"background": "var(--wp--preset--color--background)",
"text": "var(--wp--preset--color--text)"
},
...
}
Replace with:
"styles": {
"color": {
"background": "var(--wp--preset--color--theme-bg, #100e0b)",
"text": "var(--wp--preset--color--theme-text, #e3e1e9)"
},
"elements": {
"link": {
"color": {
"text": "var(--wp--preset--color--theme-bodylinks)"
}
}
},
"typography": {
"fontSize": "var(--wp--preset--font-size--base)",
"fontFamily": "var(--wp--preset--font-family--theme-sans)",
"lineHeight": "1.5"
}
}
(The elements.link is moved up here so it actually applies; previously it was nested under styles rather than styles.elements, which makes WordPress ignore it.)
- Step 4: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 5: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/base/colors.css theme.json static/dist/theme.css
git commit -m "feat(tokens): add Carbon Blue dark-surface tokens; fix theme.json presets"
Task 4: Carbon Blue atmospheric partial
Files:
- Create:
views/partials/aura-bg.php
Goal: Provide a reusable partial that renders the Carbon Blue atmospheric field per notes/Background Gradient.md. Used by front-page.php and archive-projects.php only.
- Step 1: Write the partial
Create views/partials/aura-bg.php:
<?php
/**
* Aura Background Partial — Carbon Blue
*
* Renders three decorative layers (linear gradient, radial backlight, SVG
* grain) over a transparent container. The body carries `background-color:
* #100e0b`; do NOT set a background on this container or the blend modes
* will composite against the container instead of the page.
*
* All layers are aria-hidden and ignore pointer events. Content must sit
* inside a wrapper with `position: relative; z-index: 1`.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
$blur = isset( $blur ) && 'desktop' === $blur ? 'aura-layer-2--desktop' : 'aura-layer-2--mobile';
?>
<div class="aura-bg" aria-hidden="true">
<div class="aura-layer-1"></div>
<div class="aura-layer-2 <?php echo esc_attr( $blur ); ?>"></div>
<div class="aura-grain">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<filter id="aura-grain-filter">
<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="4" stitchTiles="stitch" />
<feColorMatrix type="matrix"
values="0.181 0.608 0.061 0 0.075
0.181 0.608 0.061 0 0.075
0.181 0.608 0.061 0 0.075
0 0 0 1 0" />
</filter>
<rect width="100%" height="100%" filter="url(#aura-grain-filter)" />
</svg>
</div>
</div>
- Step 2: Add CSS for the partial
Append to styles/base/global.css (or create styles/components/aura.css and @import it from styles/theme.css):
/* Carbon Blue atmospheric field (decorative — content must sit in z-index: 1) */
.aura-bg {
position: relative;
overflow: hidden;
min-height: 100vh;
isolation: isolate;
}
.aura-layer-1 {
position: absolute;
inset: 0;
background: linear-gradient(138deg, #05070c 0%, #0f172a 40%, #172554 70%, #1e3a8a 100%);
mix-blend-mode: normal;
pointer-events: none;
transform: translateZ(0);
will-change: transform;
}
.aura-layer-2 {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 45% 50% at 72% 40%, rgba(96, 165, 250, 0.20) 0%, transparent 68%);
mix-blend-mode: screen;
pointer-events: none;
transform: translateZ(0);
will-change: transform;
}
.aura-layer-2--mobile { filter: blur(105px); }
.aura-layer-2--desktop { filter: blur(151px); }
.aura-grain {
position: absolute;
inset: 0;
mix-blend-mode: overlay;
opacity: 0.85;
pointer-events: none;
}
.aura-grain svg { display: block; width: 100%; height: 100%; }
- Step 3: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 4: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/partials/aura-bg.php styles/base/global.css static/dist/theme.css
git commit -m "feat(aura): add Carbon Blue atmospheric background partial"
Task 5: PHP namespace swap — header.php, footer.php, all lib/*.php
Files:
- Modify:
functions.php - Modify:
header.php - Modify:
footer.php - Modify:
lib/activation.php - Modify:
lib/class-acf.php - Modify:
lib/class-breadcrumbs.php - Modify:
lib/class-enqueue.php - Modify:
lib/class-menuitems.php - Modify:
lib/class-resources.php - Modify:
lib/extras.php - Modify:
lib/helpers.php - Modify:
lib/hooks.php - Modify:
lib/search-features.php - Modify:
lib/show-template.php
Goal: Replace namespace SoloFrameEvo; with namespace KsPortfolio; and update internal __NAMESPACE__ . '\\…' references.
- Step 1: Replace namespace declaration in each PHP file
For each file listed above, find namespace SoloFrameEvo; and replace with namespace KsPortfolio;.
- Step 2: Replace internal namespace references in header.php
In header.php, the only reference is namespace SoloFrameEvo; (already handled in step 1). Verify no __NAMESPACE__ . '\\…' calls remain.
- Step 3: Replace internal namespace references in lib/class-breadcrumbs.php
In lib/class-breadcrumbs.php, find every __NAMESPACE__ . '\\…' and replace '\\KsPortfolio\\…' (literal). Example: __NAMESPACE__ . '\\Breadcrumbs' becomes '\\KsPortfolio\\Breadcrumbs'.
- Step 4: Replace internal namespace references in lib/hooks.php
In lib/hooks.php, find every __NAMESPACE__ . '\\…' and replace with '\\KsPortfolio\\…'.
- Step 5: Update functions.php to include the new project helper
In functions.php, after the existing foreach ( glob( __DIR__ . '/lib/*.php' ) ... ), add the project helper include (we will create the file in Task 6 — guard with file_exists for now):
// Project helper (thin wrapper around the projects-portfolio plugin).
$projects_helper = __DIR__ . '/lib/class-projects.php';
if ( file_exists( $projects_helper ) ) {
require_once $projects_helper;
}
- Step 6: Run PHPCS
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -40
Expected: lists current violations (we expect namespace-related errors fixed; other violations are expected at this stage).
- Step 7: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add functions.php header.php footer.php lib/
git commit -m "refactor: rename namespace SoloFrameEvo to KsPortfolio"
Task 6: lib/class-projects.php — provider-agnostic helper
Files:
- Create:
lib/class-projects.php
Goal: Wrap the projects-portfolio plugin's helpers so templates never branch on provider. Provide safe defaults when the plugin is missing or repo data is unavailable.
- Step 1: Write the helper
Create lib/class-projects.php:
<?php
/**
* Project helper for the ks-portfolio theme.
*
* Thin wrapper around the projects-portfolio plugin's provider-aware
* helpers. Templates should call these methods instead of branching on
* provider, so the theme stays decoupled from GitHub/Gitea implementation
* details.
*
* Every accessor is safe to call whether the plugin is active or not: when
* the underlying function is missing or repo data is unavailable, defaults
* that match the documented data contract are returned.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
class Projects {
/**
* Whether the projects-portfolio plugin is active.
*
* @return bool
*/
public static function is_plugin_active(): bool {
return function_exists( 'projects_portfolio_get_provider' );
}
/**
* Provider label for a project (e.g. 'GitHub', 'Gitea', or 'Unknown').
*
* @param int $post_id Project post ID.
* @return string
*/
public static function get_provider_label( int $post_id ): string {
if ( ! self::is_plugin_active() ) {
return 'Unknown';
}
$provider = projects_portfolio_get_provider( $post_id );
return method_exists( $provider, 'get_label' ) ? (string) $provider->get_label() : 'Unknown';
}
/**
* Release ZIP URL (or empty string).
*
* @param int $post_id Project post ID.
* @return string
*/
public static function get_release_url( int $post_id ): string {
if ( ! self::is_plugin_active() ) {
return '';
}
return (string) projects_portfolio_get_release_url( $post_id );
}
/**
* Public repository browse URL (or empty string).
*
* @param int $post_id Project post ID.
* @return string
*/
public static function get_repo_browse_url( int $post_id ): string {
if ( ! self::is_plugin_active() ) {
return '';
}
$provider = projects_portfolio_get_provider( $post_id );
return method_exists( $provider, 'get_repo_browse_url' ) ? (string) $provider->get_repo_browse_url() : '';
}
/**
* Normalized repository data array (or null).
*
* @param int $post_id Project post ID.
* @return array<string,mixed>|null
*/
public static function get_repo_data( int $post_id ) {
if ( ! self::is_plugin_active() ) {
return null;
}
$data = projects_portfolio_get_repo_data( $post_id );
return is_array( $data ) ? $data : null;
}
/**
* Owner data (avatar/login/html_url) or null.
*
* @param int $post_id Project post ID.
* @return array<string,mixed>|null
*/
public static function get_owner_data( int $post_id ) {
$data = self::get_repo_data( $post_id );
if ( ! $data || empty( $data['owner']['login'] ) ) {
return null;
}
return $data['owner'];
}
/**
* Latest release tag, or 'Unknown'.
*
* @param int $post_id Project post ID.
* @return string
*/
public static function get_latest_version( int $post_id ): string {
if ( ! self::is_plugin_active() ) {
return 'Unknown';
}
$provider = projects_portfolio_get_provider( $post_id );
return method_exists( $provider, 'get_latest_version' ) ? (string) $provider->get_latest_version() : 'Unknown';
}
/**
* Local download endpoint URL for the project.
*
* @param int $post_id Project post ID.
* @return string
*/
public static function get_download_url( int $post_id ): string {
return home_url( '/download/' . $post_id . '/' );
}
/**
* Render a stat-card-safe integer with optional formatting.
*
* @param mixed $value Raw repo stat value.
* @return string Display-safe string ('1.2k', '12,345', etc.) or '—' when empty.
*/
public static function format_stat( $value ): string {
if ( null === $value || '' === $value ) {
return '—';
}
$value = (int) $value;
if ( $value >= 10000 ) {
return number_format( $value / 1000, 1 ) . 'k';
}
return number_format( $value );
}
}
- Step 2: Run PHPCS
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -30
Expected: no new violations introduced by this file.
- Step 3: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add lib/class-projects.php
git commit -m "feat(projects): add provider-agnostic helper for templates"
Task 7: Reusable project-card partial
Files:
- Create:
views/components/project-card.php
Goal: One shared card used by home and archive templates. Provider-agnostic. Empty-state safe.
- Step 1: Write the partial
Create views/components/project-card.php:
<?php
/**
* Project Card Component
*
* Renders one project card. Used by front-page (Recent Projects) and
* archive-projects. Expects $post (WP_Post) in scope.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
if ( ! isset( $post ) || ! ( $post instanceof \WP_Post ) ) {
return;
}
$post_id = (int) $post->ID;
$type_label = '';
$type_terms = get_the_terms( $post_id, 'project-type' );
if ( ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ) {
$type_label = strtoupper( $type_terms[0]->name );
}
$release_url = Projects::get_release_url( $post_id );
$repo_url = Projects::get_repo_browse_url( $post_id );
$repo_data = Projects::get_repo_data( $post_id );
$language = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? (string) $repo_data['language'] : '';
$stars = is_array( $repo_data ) && isset( $repo_data['stargazers_count'] ) ? (int) $repo_data['stargazers_count'] : 0;
$excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '…' );
$card_classes = 'project-card group';
?>
<article class="<?php echo esc_attr( $card_classes ); ?>" aria-labelledby="project-<?php echo esc_attr( (string) $post_id ); ?>-title">
<header class="project-card__head">
<?php if ( $type_label ) : ?>
<span class="project-card__type"><?php echo esc_html( $type_label ); ?></span>
<?php endif; ?>
<span class="project-card__provider" aria-hidden="true">
<?php echo esc_html( Projects::get_provider_label( $post_id ) ); ?>
</span>
</header>
<h3 id="project-<?php echo esc_attr( (string) $post_id ); ?>-title" class="project-card__title">
<a class="project-card__title-link" href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
<?php echo esc_html( get_the_title( $post_id ) ); ?>
</a>
</h3>
<?php if ( $excerpt ) : ?>
<p class="project-card__excerpt"><?php echo esc_html( $excerpt ); ?></p>
<?php endif; ?>
<footer class="project-card__foot">
<?php if ( $language || $stars > 0 ) : ?>
<div class="project-card__meta">
<?php if ( $language ) : ?>
<span class="project-card__lang"><?php echo esc_html( $language ); ?></span>
<?php endif; ?>
<?php if ( $stars > 0 ) : ?>
<span class="project-card__stars" aria-label="<?php echo esc_attr( $stars . ' stars' ); ?>">★ <?php echo esc_html( (string) $stars ); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="project-card__actions">
<?php if ( $release_url ) : ?>
<a class="project-card__action project-card__action--primary" href="<?php echo esc_url( Projects::get_download_url( $post_id ) ); ?>" rel="nofollow">
<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
</a>
<?php endif; ?>
<?php if ( $repo_url ) : ?>
<a class="project-card__action project-card__action--secondary" href="<?php echo esc_url( $repo_url ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
</a>
<?php endif; ?>
</div>
</footer>
</article>
- Step 2: Add CSS for the card
Append to styles/components/project-card.css (create new file) and import it from styles/theme.css:
/* Project card — Carbon Blue surface */
.project-card {
background: var(--color-surface-container, #1e1f25);
border: 1px solid var(--color-outline-variant, #444651);
border-radius: 1rem;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
transition: border-color 200ms ease, transform 200ms ease;
}
.project-card:hover,
.project-card:focus-within {
border-color: var(--color-primary, #b6c4ff);
}
.project-card:focus-within {
outline: 2px solid var(--color-primary, #b6c4ff);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.project-card { transition: none; }
}
.project-card__head {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.project-card__type {
background: rgba(182, 196, 255, 0.12);
color: var(--color-primary, #b6c4ff);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 600;
}
.project-card__provider { color: var(--color-on-surface-variant, #c5c5d3); }
.project-card__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: 1.125rem;
line-height: 1.4;
margin: 0;
color: var(--color-on-surface, #e3e1e9);
}
.project-card__title-link {
color: inherit;
text-decoration: none;
}
.project-card__title-link:hover { text-decoration: underline; }
.project-card__excerpt {
color: var(--color-on-surface-variant, #c5c5d3);
font-size: 0.9375rem;
line-height: 1.6;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
.project-card__foot {
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid var(--color-outline-variant, #444651);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
}
.project-card__meta { display: inline-flex; gap: 0.75rem; align-items: center; color: var(--color-on-surface-variant, #c5c5d3); font-size: 0.875rem; }
.project-card__actions { display: inline-flex; gap: 0.5rem; }
.project-card__action {
display: inline-flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
line-height: 1.1;
text-decoration: none;
border: 2px solid transparent;
transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
}
.project-card__action--primary {
background: var(--color-primary, #b6c4ff);
color: var(--color-on-primary, #05297a);
border-color: var(--color-primary, #b6c4ff);
}
.project-card__action--primary:hover { background: #dce1ff; border-color: #dce1ff; }
.project-card__action--secondary {
color: var(--color-on-surface, #e3e1e9);
border-color: var(--color-outline, #8f909d);
}
.project-card__action--secondary:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }
.project-card__action:focus-visible {
outline: 2px solid var(--color-primary, #b6c4ff);
outline-offset: 3px;
}
- Step 3: Register the import
In styles/theme.css, after @import "./blocks/index.css";, add:
@import "./components/project-card.css";
(If a styles/components/index.css already exists, append @import "./project-card.css"; to it instead and import the index from theme.css.)
- Step 4: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 5: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-card.php styles/components/project-card.css styles/theme.css static/dist/theme.css
git commit -m "feat(projects): add reusable project-card component + styles"
Task 8: Project stat-card partial
Files:
- Create:
views/components/project-stat-card.php
Goal: A single stat tile (Downloads / Stars / Forks / Issues) used by the sidebar in single-projects.php.
- Step 1: Write the partial
Create views/components/project-stat-card.php:
<?php
/**
* Project Stat Card Component
*
* Renders one stat tile. Expects $label, $value, and $icon (optional SVG
* markup) in scope. $icon MUST be aria-hidden decorative markup; the label
* is the accessible name.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
if ( ! isset( $label ) || ! isset( $value ) ) {
return;
}
$stat_label = (string) $label;
$stat_value = (string) $value;
?>
<div class="stat-card" role="group" aria-label="<?php echo esc_attr( $stat_label ); ?>">
<?php if ( isset( $icon ) && $icon ) : ?>
<span class="stat-card__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
<?php endif; ?>
<span class="stat-card__value"><?php echo esc_html( $stat_value ); ?></span>
<span class="stat-card__label"><?php echo esc_html( $stat_label ); ?></span>
</div>
- Step 2: Add CSS for the stat card
Append to styles/components/project-card.css (or create styles/components/stat-card.css and import it):
.stat-card {
background: var(--color-surface-container, #1e1f25);
border: 1px solid var(--color-outline-variant, #444651);
border-radius: 1rem;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
text-align: center;
}
.stat-card__icon { color: var(--color-on-surface-variant, #c5c5d3); }
.stat-card__value {
font-weight: 700;
font-size: 1.5rem;
color: var(--color-on-surface, #e3e1e9);
}
.stat-card__label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.75rem;
color: var(--color-on-surface-variant, #c5c5d3);
}
- Step 3: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 4: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-stat-card.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add stat-card partial"
Task 9: Project meta row partial
Files:
- Create:
views/components/project-meta-row.php
Goal: One row in the Repository Details table (Version, Last Updated, License, Language). Provider-agnostic.
- Step 1: Write the partial
Create views/components/project-meta-row.php:
<?php
/**
* Project Meta Row Component
*
* Renders one row of the Repository Details table on the project detail
* page. Expects $label, $value, and $icon (optional decorative SVG) in
* scope.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
if ( ! isset( $label ) || ! isset( $value ) ) {
return;
}
$row_label = (string) $label;
$row_value = (string) $value;
?>
<div class="meta-row">
<span class="meta-row__label">
<?php if ( isset( $icon ) && $icon ) : ?>
<span class="meta-row__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
<?php endif; ?>
<?php echo esc_html( $row_label ); ?>
</span>
<span class="meta-row__value"><?php echo esc_html( $row_value ); ?></span>
</div>
- Step 2: Add CSS for the meta row
Append to styles/components/project-card.css:
.meta-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--color-outline-variant, #444651);
font-size: 0.9375rem;
}
.meta-row:last-child { border-bottom: 0; }
.meta-row__label { color: var(--color-on-surface-variant, #c5c5d3); display: inline-flex; align-items: center; gap: 0.5rem; }
.meta-row__value { color: var(--color-on-surface, #e3e1e9); font-weight: 600; }
.meta-row__icon { color: var(--color-on-surface-variant, #c5c5d3); }
- Step 3: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 4: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-meta-row.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add project meta-row component"
Task 10: archive-projects.php — project archive template
Files:
- Create:
archive-projects.php
Goal: Render the project archive to match notes/archive-screen.png — hero + grid of project cards + pagination.
- Step 1: Write the template
Create archive-projects.php:
<?php
/**
* Project Archive Template
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
$hero_intro = get_field( 'archive_intro', (int) get_queried_object_id() );
if ( ! $hero_intro ) {
$hero_intro = __( 'A comprehensive catalog of WordPress plugins, custom themes, and open-source contributions developed for scalable enterprise environments. Focused on modern development practices, performant architectures, and secure integrations.', 'ks-portfolio' );
}
?>
<section class="archive-projects aura-bg">
<div class="aura-bg__content">
<div class="container mx-auto my-section">
<header class="archive-projects__head">
<h1 class="archive-projects__title"><?php echo esc_html__( 'Project Archive', 'ks-portfolio' ); ?></h1>
<p class="archive-projects__intro"><?php echo esc_html( $hero_intro ); ?></p>
</header>
<?php if ( have_posts() ) : ?>
<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<?php
while ( have_posts() ) :
the_post();
get_template_part( 'views/components/project-card' );
endwhile;
?>
</div>
<div class="archive-projects__pagination">
<?php
the_posts_pagination(
array(
'mid_size' => 2,
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
)
);
?>
</div>
<?php else : ?>
<div class="archive-projects__empty">
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
- Step 2: Add hero + grid styles
Append to styles/components/project-card.css:
.archive-projects { padding-top: 2rem; padding-bottom: 4rem; }
.archive-projects__head {
padding-left: 1rem;
border-left: 4px solid var(--color-primary, #b6c4ff);
margin-bottom: 2rem;
}
.archive-projects__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: 2.25rem;
line-height: 1.2;
margin: 0 0 0.5rem;
color: var(--color-on-surface, #e3e1e9);
}
.archive-projects__intro {
color: var(--color-on-surface-variant, #c5c5d3);
font-size: 1rem;
line-height: 1.6;
max-width: 70ch;
margin: 0;
}
.archive-projects__empty {
text-align: center;
padding: 4rem 1rem;
color: var(--color-on-surface-variant, #c5c5d3);
}
.archive-projects__empty-title {
font-weight: 700;
font-size: 1.5rem;
margin: 0 0 0.5rem;
color: var(--color-on-surface, #e3e1e9);
}
/* Pagination links on dark surface */
.archive-projects__pagination a,
.archive-projects__pagination span {
background: var(--color-surface-container, #1e1f25);
color: var(--color-on-surface, #e3e1e9);
border: 1px solid var(--color-outline-variant, #444651);
border-radius: 4px;
}
- Step 3: Build the CSS bundle
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
- Step 4: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add archive-projects.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add archive-projects template"
Task 11: taxonomy-project-type.php — taxonomy archive
Files:
- Create:
taxonomy-project-type.php
Goal: Same shell as archive-projects but with the taxonomy label as the title.
- Step 1: Write the template
Create taxonomy-project-type.php:
<?php
/**
* Project Type Taxonomy Archive Template
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
$queried = get_queried_object();
$title = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
?>
<section class="archive-projects aura-bg">
<div class="aura-bg__content">
<div class="container mx-auto my-section">
<header class="archive-projects__head">
<h1 class="archive-projects__title"><?php echo esc_html( $title ); ?></h1>
<p class="archive-projects__intro">
<a class="archive-projects__back-link" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
<?php echo esc_html__( '← All Projects', 'ks-portfolio' ); ?>
</a>
</p>
</header>
<?php if ( have_posts() ) : ?>
<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<?php
while ( have_posts() ) :
the_post();
get_template_part( 'views/components/project-card' );
endwhile;
?>
</div>
<div class="archive-projects__pagination">
<?php
the_posts_pagination(
array(
'mid_size' => 2,
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
)
);
?>
</div>
<?php else : ?>
<div class="archive-projects__empty">
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found in this category.', 'ks-portfolio' ); ?></p>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
- Step 2: Add back-link style
Append to styles/components/project-card.css:
.archive-projects__back-link {
color: var(--color-primary, #b6c4ff);
text-decoration: none;
font-weight: 600;
}
.archive-projects__back-link:hover { text-decoration: underline; }
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add taxonomy-project-type.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add taxonomy-project-type template"
Task 12: single-projects.php — project detail
Files:
- Create:
single-projects.php
Goal: Render the project detail page to match notes/single-screen.png — hero with provider + title + version, Download/View Repo CTAs, two-column body, owner, repository details, stat cards.
- Step 1: Write the template
Create single-projects.php:
<?php
/**
* Single Project Template
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
while ( have_posts() ) :
the_post();
$post_id = (int) get_the_ID();
$provider = Projects::get_provider_label( $post_id );
$version = Projects::get_latest_version( $post_id );
$release = Projects::get_release_url( $post_id );
$repo = Projects::get_repo_browse_url( $post_id );
$repo_data = Projects::get_repo_data( $post_id );
$owner = Projects::get_owner_data( $post_id );
$type_terms = get_the_terms( $post_id, 'project-type' );
$type_label = ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ? strtoupper( $type_terms[0]->name ) : '';
$excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : '';
?>
<article class="single-project aura-bg">
<div class="aura-bg__content">
<div class="container mx-auto my-section">
<header class="single-project__hero">
<p class="single-project__meta">
<span class="single-project__provider"><?php echo esc_html( $provider ); ?></span>
<?php if ( $type_label ) : ?>
<span class="single-project__type"><?php echo esc_html( $type_label ); ?></span>
<?php endif; ?>
</p>
<h1 class="single-project__title">
<?php echo esc_html( get_the_title( $post_id ) ); ?>
<?php if ( 'Unknown' !== $version ) : ?>
<span class="single-project__version">v<?php echo esc_html( $version ); ?></span>
<?php endif; ?>
</h1>
<?php if ( $excerpt ) : ?>
<p class="single-project__excerpt"><?php echo esc_html( $excerpt ); ?></p>
<?php endif; ?>
<div class="single-project__actions">
<?php if ( $release ) : ?>
<a class="button button--primary" href="<?php echo esc_url( Projects::get_download_url( $post_id ) ); ?>" rel="nofollow">
<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
<?php if ( 'Unknown' !== $version ) : ?>
v<?php echo esc_html( $version ); ?>
<?php endif; ?>
</a>
<?php endif; ?>
<?php if ( $repo ) : ?>
<a class="button button--outline" href="<?php echo esc_url( $repo ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
</a>
<?php endif; ?>
</div>
</header>
<div class="single-project__layout">
<div class="single-project__body">
<?php if ( has_post_thumbnail( $post_id ) ) : ?>
<figure class="single-project__featured">
<?php echo get_the_post_thumbnail( $post_id, 'large' ); ?>
</figure>
<?php endif; ?>
<section class="single-project__content" aria-label="<?php echo esc_attr__( 'Architecture and Implementation', 'ks-portfolio' ); ?>">
<h2><?php echo esc_html__( 'Architecture & Implementation', 'ks-portfolio' ); ?></h2>
<?php the_content(); ?>
</section>
</div>
<aside class="single-project__sidebar" aria-label="<?php echo esc_attr__( 'Repository details', 'ks-portfolio' ); ?>">
<?php if ( $owner ) : ?>
<section class="single-project__maintainer">
<h3><?php echo esc_html__( 'Maintainer', 'ks-portfolio' ); ?></h3>
<div class="single-project__owner">
<?php if ( ! empty( $owner['avatar_url'] ) ) : ?>
<span class="single-project__owner-avatar" style="background-image: url('<?php echo esc_url( $owner['avatar_url'] ); ?>');" aria-hidden="true"></span>
<?php endif; ?>
<a class="single-project__owner-link" href="<?php echo esc_url( ! empty( $owner['html_url'] ) ? $owner['html_url'] : ( 'https://github.com/' . $owner['login'] ) ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( $owner['login'] ); ?>
</a>
</div>
</section>
<?php endif; ?>
<section class="single-project__details">
<h3><?php echo esc_html__( 'Repository Details', 'ks-portfolio' ); ?></h3>
<?php
$updated_at = is_array( $repo_data ) && ! empty( $repo_data['updated_at'] ) ? human_time_diff( strtotime( $repo_data['updated_at'] ), current_time( 'timestamp' ) ) . ' ' . __( 'ago', 'ks-portfolio' ) : '—';
$license = is_array( $repo_data ) && ! empty( $repo_data['license']['name'] ) ? $repo_data['license']['name'] : __( 'None', 'ks-portfolio' );
$language = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? $repo_data['language'] : '—';
$rows = array(
array( 'label' => __( 'Version', 'ks-portfolio' ), 'value' => 'Unknown' !== $version ? 'v' . $version : __( 'Unknown', 'ks-portfolio' ) ),
array( 'label' => __( 'Last Updated', 'ks-portfolio' ), 'value' => $updated_at ),
array( 'label' => __( 'License', 'ks-portfolio' ), 'value' => $license ),
array( 'label' => __( 'Language', 'ks-portfolio' ), 'value' => $language ),
);
foreach ( $rows as $row ) :
set_query_var( 'label', $row['label'] );
set_query_var( 'value', $row['value'] );
get_template_part( 'views/components/project-meta-row' );
endforeach;
?>
</section>
<section class="single-project__stats" aria-label="<?php echo esc_attr__( 'Repository statistics', 'ks-portfolio' ); ?>">
<?php
$stats = array(
array( 'label' => __( 'Downloads', 'ks-portfolio' ), 'value' => Projects::format_stat( get_post_meta( $post_id, '_projects_portfolio_download_count', true ) ) ),
array( 'label' => __( 'Stars', 'ks-portfolio' ), 'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['stargazers_count'] ?? 0 ) : 0 ) ),
array( 'label' => __( 'Forks', 'ks-portfolio' ), 'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['forks_count'] ?? 0 ) : 0 ) ),
array( 'label' => __( 'Issues', 'ks-portfolio' ), 'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['open_issues_count'] ?? 0 ) : 0 ) ),
);
foreach ( $stats as $stat ) :
set_query_var( 'label', $stat['label'] );
set_query_var( 'value', $stat['value'] );
get_template_part( 'views/components/project-stat-card' );
endforeach;
?>
</section>
</aside>
</div>
</div>
</div>
</article>
<?php
endwhile;
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
- Step 2: Add single-project CSS
Append to styles/components/project-card.css:
.single-project__hero {
background: var(--color-surface-container-low, #1a1b21);
border: 1px solid var(--color-outline-variant, #444651);
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
}
.single-project__meta {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0 0 0.5rem;
color: var(--color-on-surface-variant, #c5c5d3);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.single-project__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: 2.25rem;
line-height: 1.2;
color: var(--color-on-surface, #e3e1e9);
margin: 0 0 0.75rem;
}
.single-project__version { color: var(--color-on-surface-variant, #c5c5d3); font-weight: 400; font-size: 1.5rem; margin-left: 0.5rem; }
.single-project__excerpt {
color: var(--color-on-surface-variant, #c5c5d3);
font-size: 1rem;
line-height: 1.6;
max-width: 70ch;
margin: 0 0 1.5rem;
}
.single-project__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.single-project__layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
@media (min-width: 1024px) {
.single-project__layout {
grid-template-columns: 2fr 1fr;
gap: 4rem;
}
}
.single-project__body h2,
.single-project__sidebar h3 { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }
.single-project__maintainer,
.single-project__details,
.single-project__stats {
background: var(--color-surface-container, #1e1f25);
border: 1px solid var(--color-outline-variant, #444651);
border-radius: 1rem;
padding: 1.25rem;
margin-bottom: 1rem;
}
.single-project__owner { display: flex; align-items: center; gap: 0.75rem; }
.single-project__owner-avatar {
width: 40px;
height: 40px;
background-size: 40px 40px;
background-repeat: no-repeat;
background-position: center;
border-radius: 0.5rem;
background-color: var(--color-surface-container-high, #292a2f);
}
.single-project__owner-link { color: var(--color-on-surface, #e3e1e9); font-weight: 600; text-decoration: none; }
.single-project__owner-link:hover { color: var(--color-primary, #b6c4ff); }
.single-project__stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add single-projects.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add single-projects detail template"
Task 13: front-page.php — full home template
Files:
- Modify:
front-page.php
Goal: Replace the 8-line stub with a home page matching notes/home-screen.png: Carbon Blue hero, Recent Projects grid, footer.
- Step 1: Write the template
Replace front-page.php with:
<?php
/**
* Front Page Template — Portfolio 2026 home
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
$hero_heading = get_field( 'homepage_hero_heading' );
$hero_intro = get_field( 'homepage_hero_intro' );
$hero_cta_1 = get_field( 'homepage_hero_cta_1' ); // array( url, title, target )
$hero_cta_2 = get_field( 'homepage_hero_cta_2' );
?>
<section class="home-hero aura-bg">
<div class="aura-bg__content">
<div class="container mx-auto my-section">
<div class="home-hero__inner">
<?php if ( $hero_heading ) : ?>
<h1 class="home-hero__heading"><?php echo esc_html( $hero_heading ); ?></h1>
<?php endif; ?>
<?php if ( $hero_intro ) : ?>
<p class="home-hero__intro"><?php echo esc_html( $hero_intro ); ?></p>
<?php endif; ?>
<div class="home-hero__actions">
<?php if ( ! empty( $hero_cta_1['url'] ) && ! empty( $hero_cta_1['title'] ) ) : ?>
<a class="button button--primary" href="<?php echo esc_url( $hero_cta_1['url'] ); ?>"<?php echo ! empty( $hero_cta_1['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
<?php echo esc_html( $hero_cta_1['title'] ); ?>
</a>
<?php endif; ?>
<?php if ( ! empty( $hero_cta_2['url'] ) && ! empty( $hero_cta_2['title'] ) ) : ?>
<a class="button button--outline" href="<?php echo esc_url( $hero_cta_2['url'] ); ?>"<?php echo ! empty( $hero_cta_2['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
<?php echo esc_html( $hero_cta_2['title'] ); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>
<section class="home-recent container mx-auto my-section">
<header class="home-recent__head">
<h2 class="home-recent__title"><?php echo esc_html__( 'Recent Projects', 'ks-portfolio' ); ?></h2>
<a class="home-recent__view-all" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
<?php echo esc_html__( 'View All', 'ks-portfolio' ); ?> →
</a>
</header>
<hr class="home-recent__divider" />
<?php
$recent = new \WP_Query(
array(
'post_type' => 'projects',
'posts_per_page' => 3,
'no_found_rows' => true,
)
);
if ( $recent->have_posts() ) : ?>
<div class="home-recent__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<?php
while ( $recent->have_posts() ) :
$recent->the_post();
get_template_part( 'views/components/project-card' );
endwhile;
wp_reset_postdata();
?>
</div>
<?php else : ?>
<div class="home-recent__empty">
<h3><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h3>
<p><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
</div>
<?php endif; ?>
</section>
<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
- Step 2: Add home-hero + home-recent CSS
Append to styles/components/project-card.css:
.home-hero { min-height: 60vh; display: flex; align-items: center; padding: 4rem 0; }
.home-hero__inner { max-width: 50rem; }
.home-hero__heading {
font-family: var(--font-sans);
font-weight: 700;
font-size: clamp(2rem, 4vw, 3.5rem);
line-height: 1.1;
color: var(--color-on-surface, #e3e1e9);
margin: 0 0 1rem;
}
.home-hero__intro {
color: var(--color-on-surface-variant, #c5c5d3);
font-size: clamp(1rem, 1.5vw, 1.25rem);
line-height: 1.6;
max-width: 50ch;
margin: 0 0 1.5rem;
}
.home-hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.home-recent__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.home-recent__title {
font-family: var(--font-sans);
font-weight: 700;
font-size: 1.75rem;
color: var(--color-on-surface, #e3e1e9);
margin: 0;
}
.home-recent__view-all {
color: var(--color-primary, #b6c4ff);
font-weight: 600;
text-decoration: none;
}
.home-recent__view-all:hover { text-decoration: underline; }
.home-recent__divider {
border: 0;
border-top: 1px solid var(--color-outline-variant, #444651);
margin: 0 0 1.5rem;
}
.home-recent__empty { text-align: center; padding: 4rem 1rem; color: var(--color-on-surface-variant, #c5c5d3); }
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add front-page.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(home): rebuild front-page template to match Carbon Blue mockup"
Task 14: Header — three-region responsive layout + dark nav
Files:
- Modify:
header.php - Modify:
styles/navigation/nav-aux.css - Modify:
styles/navigation/nav-main-default.css
Goal: Switch from grid-cols-[83px_auto] to a three-region header (logo | nav | aux button), recolor nav to Carbon Blue.
- Step 1: Update header.php markup
Replace the existing .header__nav-main block in header.php with:
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[auto_1fr_auto] grid-rows-[1fr] justify-between">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo block">
<img class="site-header__logo-img" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" />
</a>
<?php get_template_part( 'views/components/nav-main' ); ?>
<div class="site-header__aux">
<a class="button button--outline" href="<?php echo esc_url( home_url( '/cv/' ) ); ?>">
<?php echo esc_html__( 'Download CV', 'ks-portfolio' ); ?>
</a>
</div>
</div>
- Step 2: Recolor nav for dark surface
Append to styles/navigation/nav-main-default.css:
.nav-main a,
.nav-aux a {
color: var(--color-on-surface, #e3e1e9);
}
.nav-main a:hover,
.nav-aux a:hover {
color: var(--color-primary, #b6c4ff);
}
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add header.php styles/navigation/ static/dist/theme.css
git commit -m "feat(header): three-region layout + dark nav colors"
Task 15: Footer — Carbon Blue surface
Files:
- Modify:
footer.php
Goal: Recolor footer links, swap "Footer Area 1/2/3" headings for widget titles, and update copyright to match Carbon Blue palette.
- Step 1: Update footer.php
Find the existing bg-gray-800 text-white block on <footer> and replace with:
<footer role="contentinfo" class="site-footer">
<div class="container mx-auto py-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div class="site-footer__brand max-w-[40ch]">
<h2 class="site-footer__title">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
<?php if ( $footerLogo ) { ?>
<img src="<?php echo esc_url( $footerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>" />
<?php } else {
echo esc_html( get_bloginfo( 'name' ) );
} ?>
</a>
</h2>
<?php if ( $footerDesc ) : ?>
<p class="site-footer__desc"><?php echo wp_kses_post( $footerDesc ); ?></p>
<?php endif; ?>
</div>
<?php for ( $i = 1; $i <= 3; $i++ ) :
$area_id = 'footer-' . $i;
if ( ! is_active_sidebar( $area_id ) ) {
continue;
}
?>
<div class="site-footer__area">
<?php dynamic_sidebar( $area_id ); ?>
</div>
<?php endfor; ?>
<?php if ( $footerNav ) : ?>
<nav class="site-footer__nav" aria-label="<?php echo esc_attr__( 'Footer navigation', 'ks-portfolio' ); ?>">
<h3><?php echo esc_html__( 'Navigation', 'ks-portfolio' ); ?></h3>
<ul>
<?php foreach ( $footerNav as $item ) : ?>
<li>
<a href="<?php echo esc_url( $item->url ); ?>">
<?php echo esc_html( $item->title ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
<?php endif; ?>
</div>
</div>
<div class="site-footer__copyright">
<?php echo wp_kses_post( $copyright ); ?>
</div>
</footer>
- Step 2: Add footer CSS
Append to styles/navigation/nav-footer.css (or create styles/components/footer.css and import from styles/theme.css):
.site-footer {
background: var(--color-surface, #121318);
color: var(--color-on-surface, #e3e1e9);
border-top: 1px solid var(--color-outline-variant, #444651);
}
.site-footer__title a { color: var(--color-on-surface, #e3e1e9); text-decoration: none; }
.site-footer__desc { color: var(--color-on-surface-variant, #c5c5d3); }
.site-footer__area h3,
.site-footer__nav h3 {
color: var(--color-on-surface, #e3e1e9);
font-family: var(--font-sans);
font-weight: 700;
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-outline-variant, #444651);
}
.site-footer a { color: var(--color-primary, #b6c4ff); }
.site-footer a:hover { text-decoration: underline; }
.site-footer__copyright {
background: var(--color-surface-container-lowest, #0d0e13);
color: var(--color-on-surface-variant, #c5c5d3);
text-align: center;
padding: 0.75rem;
font-size: 0.875rem;
}
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add footer.php styles/ static/dist/theme.css
git commit -m "feat(footer): Carbon Blue surface + cleaner widget rendering"
Task 16: Other templates — namespace swap + dark surface + remove picsum
Files:
- Modify:
index.php - Modify:
single.php - Modify:
page.php - Modify:
search.php - Modify:
404.php - Modify:
views/partials/page-hero.php
Goal: Apply namespace swap, remove picsum.photos fallbacks, ensure empty-state copy is the documented strings.
- Step 1: Namespace swap + picsum removal in index.php
In index.php:
- Replace
namespace SoloFrameEvo;withnamespace KsPortfolio;. - Find the line:
Replace with:
$postImg = $featImg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();$postImg = $featImg; - Find the
<img>tag and wrap the whole<figure>in:(Don't render an empty figure.)<?php if ( has_post_thumbnail() ) : ?> <figure>...existing markup...</figure> <?php endif; ?>
- Step 2: Namespace swap + picsum removal in search.php
Same edits as Step 1.
- Step 3: Namespace swap in single.php, page.php, 404.php, page-hero.php
In each file, replace namespace SoloFrameEvo; with namespace KsPortfolio;. In 404.php, also update:
<?php echo esc_html__( 'Search for:', 'label' ); ?>
to:
<?php echo esc_html__( 'Search for:', 'ks-portfolio' ); ?>
(And the same for any other 'label' text-domain references in 404.php.)
- Step 4: Dark surface for page-hero partial
In views/partials/page-hero.php:
- Namespace swap.
- Replace the existing
bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hiddenwith:
<section class="page-hero py-12 lg:py-16">
- Replace the
<h1>color classestext-lightwithpage-hero__title(let CSS handle it).
- Step 5: Add page-hero CSS
Append to styles/components/page-hero.css (create new file and import from styles/theme.css):
.page-hero {
background: var(--color-surface-container-low, #1a1b21);
color: var(--color-on-surface, #e3e1e9);
border-bottom: 1px solid var(--color-outline-variant, #444651);
margin-bottom: 2rem;
}
.page-hero__title { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }
.page-hero__intro { color: var(--color-on-surface-variant, #c5c5d3); }
- Step 6: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add index.php single.php page.php search.php 404.php views/partials/page-hero.php styles/ static/dist/theme.css
git commit -m "feat(templates): namespace swap + dark surface + remove picsum fallbacks"
Task 17: Button styles for dark surface
Files:
- Modify:
styles/blocks/buttons.css
Goal: Verify button classes render with WCAG-AA contrast against #100e0b. Add a button--outline variant if missing (used by the home + archive + single-project templates).
- Step 1: Inspect existing button styles
Read styles/blocks/buttons.css. Confirm it defines (or can be extended to define) the classes: button, button--primary, button--outline, button--secondary, button--white, button--black. Note what's present.
- Step 2: Append dark-surface button styles
Append to styles/blocks/buttons.css:
.button {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1rem;
line-height: 1.1;
padding: 0.5rem 2rem;
border-radius: 0.5rem;
border: 3px solid transparent;
text-decoration: none;
transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
}
.button:active { transform: scale(0.99); }
@media (prefers-reduced-motion: reduce) { .button:active { transform: none; } }
.button:focus-visible { outline: 2px solid var(--color-primary, #b6c4ff); outline-offset: 3px; }
.button--primary {
background: var(--color-primary, #b6c4ff);
color: var(--color-on-primary, #05297a);
border-color: var(--color-primary, #b6c4ff);
}
.button--primary:hover { background: #dce1ff; border-color: #dce1ff; }
.button--outline {
background: transparent;
color: var(--color-on-surface, #e3e1e9);
border-color: var(--color-outline, #8f909d);
}
.button--outline:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }
.button--secondary {
background: var(--color-surface-container-high, #292a2f);
color: var(--color-on-surface, #e3e1e9);
border-color: var(--color-outline-variant, #444651);
}
- Step 3: Build + commit
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/blocks/buttons.css static/dist/theme.css
git commit -m "feat(buttons): dark-surface button variants with WCAG-AA contrast"
Task 18: Playwright + axe specs
Files:
- Create:
tests/home.spec.js - Create:
tests/archive.spec.js - Create:
tests/single.spec.js
Goal: Browser + accessibility tests for the new Carbon Blue surfaces.
- Step 1: Write tests/home.spec.js
Create tests/home.spec.js:
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test.use({ viewport: { width: 1920, height: 1080 } });
test.describe('home', () => {
test('home renders Carbon Blue hero + recent projects + zero axe violations', async ({ page }, testInfo) => {
await page.goto('/');
await page.screenshot({ path: 'test-results/home.png', fullPage: true });
// Hero region must be present.
await expect(page.locator('.home-hero')).toBeVisible();
// Either the grid OR the empty state must be present.
const grid = page.locator('.home-recent__grid');
const empty = page.locator('.home-recent__empty');
await expect(grid.or(empty)).toBeVisible();
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
.analyze();
await testInfo.attach('axe', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
});
- Step 2: Write tests/archive.spec.js
Create tests/archive.spec.js:
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test.use({ viewport: { width: 1920, height: 1080 } });
test.describe('archive-projects', () => {
test('project archive renders grid or empty + zero axe violations', async ({ page }, testInfo) => {
await page.goto('/projects/');
await page.screenshot({ path: 'test-results/archive.png', fullPage: true });
await expect(page.locator('.archive-projects__head h1')).toBeVisible();
const grid = page.locator('.archive-projects__grid');
const empty = page.locator('.archive-projects__empty');
await expect(grid.or(empty)).toBeVisible();
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
.analyze();
await testInfo.attach('axe', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
});
- Step 3: Write tests/single.spec.js
Create tests/single.spec.js:
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test.use({ viewport: { width: 1920, height: 1080 } });
test.describe('single-project', () => {
test('project detail renders hero, sidebar, no axe violations', async ({ page }, testInfo) => {
// Pick the first project; skip the test gracefully if none exist.
const response = await page.goto('/projects/');
expect(response && response.status()).toBeLessThan(400);
const firstLink = page.locator('.archive-projects__grid a.project-card__title-link').first();
if (await firstLink.count() === 0) {
test.skip(true, 'No projects to test detail page');
return;
}
await Promise.all([
page.waitForLoadState('networkidle'),
firstLink.click(),
]);
await page.screenshot({ path: 'test-results/single.png', fullPage: true });
await expect(page.locator('.single-project__title')).toBeVisible();
await expect(page.locator('.single-project__sidebar')).toBeVisible();
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
.analyze();
await testInfo.attach('axe', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
});
- Step 4: Run all Playwright tests
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test --reporter=list
Expected: all four specs (site-a11y, home, archive, single) pass. If any fail, debug the specific violation, fix in the template/CSS, re-run.
- Step 5: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add tests/home.spec.js tests/archive.spec.js tests/single.spec.js
git commit -m "test: add Playwright + axe specs for home, archive, single"
Task 19: PHPCS clean run
Files:
- All PHP files in theme
Goal: composer run lint exits 0.
- Step 1: Run PHPCS
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -80
- Step 2: Fix violations inline
For each reported violation, fix the file. Common ones expected: missing wp_kses_post on the_content() calls already correctly used; trailing whitespace; array alignment; WordPress.Security.EscapeOutput — fix by adding the right esc_* function.
- Step 3: Re-run until clean
Repeat step 1 until the command exits with no violations (or only the documented exclusions in .phpcs.xml).
- Step 4: Commit any fixes
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add -A
git commit -m "style: PHPCS clean across theme PHP" --allow-empty
Task 20: Static security audit
Files:
- Audit only (no file changes unless finding requires them)
Goal: Identify any Critical/High security issues. Produce a written report that becomes part of the final testing report.
- Step 1: Run PHPCS WordPress.Security ruleset
Run: cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && ./vendor/bin/phpcs --standard=.phpcs.xml --sniffs=WordPress.Security.EscapeOutput,WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput . 2>&1 | tail -80
Expected: zero findings (since we already passed PHPCS in Task 19). If there are findings, fix inline.
- Step 2: Grep for unsanitized superglobal access
Run (PowerShell):
Select-String -Path "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio" -Include "*.php" -Recurse -Pattern '\$_(GET|POST|REQUEST)\[' | ForEach-Object { $_.Path + ":" + $_.LineNumber + " " + $_.Line }
Expected: empty result (theme has no direct superglobal access). Any finding is a security issue — fix by switching to filter_input + sanitize_* or to WordPress API equivalents.
- Step 3: Audit new templates manually
For each new template (front-page.php, archive-projects.php, single-projects.php, taxonomy-project-type.php) and each new partial (views/partials/aura-bg.php, views/components/project-card.php, views/components/project-stat-card.php, views/components/project-meta-row.php), check:
-
Every dynamic output uses the right
esc_*for context. -
Every state-changing form has
wp_nonce_field(none in this theme — none expected). -
External URLs use
esc_urland addrel="noopener noreferrer"whentarget="_blank". -
set_query_var/get_template_partdata is escaped in the consumer partials. -
Step 4: Write the security section of the final report
Append to docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md (we'll create the file in Task 22):
## Security audit
- PHPCS WordPress.Security: <PASS / count of findings>
- Direct superglobal access (grep): <PASS / count of findings>
- Manual template audit: <PASS / list of findings with severity>
Severity summary: Critical: 0, High: 0, Medium: 0, Low: 0, Info: 0.
- Step 5: Commit audit notes (if any fixes were made)
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add -A
git commit -m "security: remediate audit findings" --allow-empty
Task 21: Replace screenshot.png with Carbon Blue capture
Files:
- Modify:
screenshot.png
Goal: Drop the Basic WP screenshot and replace it with a Carbon Blue capture.
- Step 1: Capture the home page screenshot
Run the existing Playwright test (it already saves test-results/home.png) or run explicitly:
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
npx playwright test tests/home.spec.js --reporter=list
- Step 2: Copy the capture to theme root
Run (PowerShell):
Copy-Item "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\test-results\home.png" "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\screenshot.png" -Force
- Step 3: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git rm screenshot.png
git add screenshot.png
git commit -m "chore: replace Basic WP screenshot with Carbon Blue home capture"
Task 22: Final testing report
Files:
- Create:
docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md
Goal: Ship a complete report covering all four test categories (browser, functional, unit, pen-test), issues encountered, and recommendations.
- Step 1: Write the report
Create docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md with the following sections (replace <…> with actual numbers):
# Portfolio 2026 — Testing Report
**Date:** 2026-08-16
**Theme:** ks-portfolio (Portfolio 2026)
**Local base URL:** http://portfolio.test
## 1. Scope
Browser (Chromium + axe-core), functional (Playwright), unit (PHPCS + WPCS), static security audit. Firefox/WebKit/mobile and OWASP ZAP were out of scope per user choice.
## 2. Browser + functional (Playwright)
Specs run:
- `tests/site-a11y.spec.js` — homepage, blog index, 404
- `tests/home.spec.js` — Carbon Blue hero + recent projects
- `tests/archive.spec.js` — project archive grid or empty state
- `tests/single.spec.js` — project detail hero + sidebar
Result: <PASS / FAIL — count of passing specs>
Screenshots: `test-results/home.png`, `test-results/archive.png`, `test-results/single.png`.
## 3. Unit / lint (PHPCS)
Command: `composer run lint`
Result: <PASS / FAIL — count of remaining violations>
## 4. Static security audit
<see "Security audit" block from Task 20>
## 5. Issues encountered
<list — severity-ranked>
## 6. Recommendations for future improvement
<list — non-blocking>
- Step 2: Commit
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md
git commit -m "docs: add Portfolio 2026 testing report"
Self-Review
Spec coverage:
- §3 Identity/naming → Tasks 5, 21, 22. ✓
- §4.1 Body surface → Task 2. ✓
- §4.2 Carbon Blue partial → Task 4. ✓
- §4.3 Tokens → Task 3. ✓
- §5.1 front-page → Task 13. ✓
- §5.2 archive-projects → Task 10. ✓
- §5.3 single-projects → Task 12. ✓
- §5.4 taxonomy-project-type → Task 11. ✓
- §5.5 Other templates → Task 16. ✓
- §6.1 Buttons → Task 17. ✓
- §6.2 Project card → Task 7. ✓
- §6.3 Stat card → Task 8. ✓
- §6.4 Navigation → Task 14. ✓
- §6.5 Header layout → Task 14. ✓
- §7 Accessibility → Tasks 17, 18. ✓
- §8 Security → Tasks 19, 20. ✓
- §9 Testing → Tasks 1, 18, 19, 20, 22. ✓
- §10 File-level → all tasks together. ✓
- §12 Definition of done → Task 22. ✓
Placeholder scan: no TBDs / "TODO" / "implement later".
Type consistency:
Projects::get_*signatures used in Tasks 7, 8, 9, 10, 11, 12, 13 match the definitions in Task 6.Projects::format_statreturns'—'when empty — used by stat-card in Task 12. ✓- Card partial names (
project-card,project-stat-card,project-meta-row) consistent across Tasks 7–12. ✓
Plan complete and saved to docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md. Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration.
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints.
Which approach?