feature: Implement project-type taxonomy archive template and enhance breadcrumb navigation
Sync TODOs with Issues / sync_todos (push) Successful in 8s

This commit is contained in:
Keith Solomon
2026-08-28 20:06:54 -05:00
parent e31ed29962
commit 935dd74194
6 changed files with 178 additions and 105 deletions
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* Project Type Taxonomy Archive Template (plugin override)
*
* The projects-portfolio plugin's template loader looks for this filename
* (archive-project-type.php) via locate_template(); when present it is used
* in place of the plugin's bundled template. We mirror the pattern used by
* the post-type archive and the Recent Projects block: a flex-row head
* with the term title on the left and an "All Projects" link on the right,
* a divider, then the project card grid.
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
$queried = get_queried_object();
$term_name = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
?>
<section class="tax-project container mx-auto my-section">
<?php if ( have_posts() ) : ?>
<div class="tax-project__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="tax-project__pagination">
<?php
the_posts_pagination(
array(
'mid_size' => 2,
'prev_text' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
)
);
?>
</div>
<?php else : ?>
<div class="tax-project__empty">
<h2 class="tax-project__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', 'ks-portfolio' ); ?></h2>
<p class="tax-project__empty-desc"><?php echo esc_html__( 'No published projects found in this category.', 'ks-portfolio' ); ?></p>
</div>
<?php endif; ?>
</section>
<?php
get_footer();
+23 -1
View File
@@ -46,7 +46,11 @@ class Breadcrumbs {
// Static Page - add the parent pages if any, and the page title
$breadcrumbs = array_merge( $breadcrumbs, self::getStaticPageBreadcrumbs( $post ) );
} elseif ( is_category() || is_tag() || is_tax() ) {
// Taxonomy Archive - add the taxonomy term name
// Taxonomy Archive - if this is a project-type term, surface the
// Projects archive as a clickable parent in the trail.
if ( is_tax( 'project-type' ) ) {
$breadcrumbs[] = self::getProjectsArchiveLinkBreadcrumb();
}
$breadcrumbs[] = self::getTaxonomyArchiveBreadcrumb();
} elseif ( is_post_type_archive() ) {
// Post Type Archive - add the post type name
@@ -205,6 +209,24 @@ class Breadcrumbs {
return $postType ? array( 'label' => $postType->labels->name ) : array();
}
/** Generates a clickable Projects archive breadcrumb.
*
* Used in the taxonomy archive trail so users have a working "back to
* all projects" link in the breadcrumb. Distinct from
* getPostTypeArchiveBreadcrumb(), which omits the URL because on the
* post-type archive itself that crumb is the active leaf and shouldn't
* link to itself.
*
* @return array Breadcrumb data with both label and url.
*/
private static function getProjectsArchiveLinkBreadcrumb() {
$url = get_post_type_archive_link( 'projects' );
return array(
'url' => $url ? $url : '',
'label' => __( 'Projects', 'ks-portfolio' ),
);
}
/** Generates breadcrumbs for date-based archives.
*
* This method is responsible for creating breadcrumb navigation
+42
View File
@@ -154,6 +154,8 @@ function getTheTitle() {
if ( is_home() || is_single() ) {
$title = get_the_title( get_option( 'page_for_posts', true ) );
} elseif ( is_archive() ) {
// get_the_archive_title() is filtered below to drop the
// "Project Type:" / "Category:" prefix — see stripArchiveTitlePrefix().
$title = get_the_archive_title();
} elseif ( is_search() ) {
$title = sprintf(
@@ -170,6 +172,26 @@ function getTheTitle() {
return $title;
}
/** Strip the "Project Type:" / "Category:" / "Tag:" prefix from archive titles.
*
* WordPress's get_the_archive_title() returns plain text with the taxonomy
* or post-type label concatenated before a colon (e.g. "Project Type: Plugins",
* "Category: WordPress", "Tag: php"). The site's hero partial uses the raw
* archive title as its <h1>, and templates render their own context-appropriate
* headers, so the prefix is redundant and visually noisy.
*
* @param string $title The full archive title, including prefix.
* @return string The title with the prefix removed.
*/
function stripArchiveTitlePrefix( $title ) {
// Drop the "<Label>: " prefix WordPress prepends. The label may be wrapped
// in a <span> on some archive types (date archives) — strip that too.
$title = (string) $title;
$title = preg_replace( '/^\s*(?:<[^>]+>\s*)*[^:<]+:\s*/u', '', $title );
return $title;
}
add_filter( 'get_the_archive_title', __NAMESPACE__ . '\\stripArchiveTitlePrefix' );
/** Wraps iframes and embed elements in a div with a specific class.
*
* This function searches for iframe and embed elements within the provided
@@ -198,6 +220,26 @@ function divWrapper( $content ) {
add_filter( 'the_content', __NAMESPACE__ . '\\divWrapper' );
/** Strip the "<Label>: " prefix from the document title.
*
* The SEO Framework wipes out all `pre_get_document_title` filters at
* `template_redirect` priority 20 and composes the title itself. To strip
* the archive prefix consistently across <title>, og:title, and twitter:title
* (which all derive from TSF's generated archive title list), hook the
* archive-title-items filter and return the unprefixed title.
*
* @param array $items [title, prefix, title_without_prefix]
* @return array The same items with the prefix removed from the title.
*/
function stripArchiveTitleItemsPrefix( $items ) {
if ( ! is_array( $items ) || empty( $items[2] ) ) {
return $items;
}
$items[0] = $items[2];
return $items;
}
add_filter( 'the_seo_framework_generated_archive_title_items', __NAMESPACE__ . '\\stripArchiveTitleItemsPrefix' );
/** Selectively add sidebar to page.
*
* This function adds a custom field group to the WordPress editor for
+27 -5
View File
@@ -207,12 +207,34 @@
border-radius: 4px;
}
.archive-projects__back-link {
color: var(--color-primary, #b6c4ff);
text-decoration: none;
font-weight: 600;
/* Project-type taxonomy archive — the page hero renders the heading,
* breadcrumbs handle navigation. */
.tax-project { padding-top: 2rem; padding-bottom: 4rem; }
.tax-project__pagination {
margin-top: 2rem;
}
.tax-project__empty {
text-align: center;
padding: 4rem 1rem;
color: var(--color-on-surface-variant, #c5c5d3);
}
.tax-project__empty-title {
font-weight: 700;
font-size: 1.5rem;
margin: 0 0 0.5rem;
color: var(--color-on-surface, #e3e1e9);
}
.tax-project__pagination a,
.tax-project__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;
}
.archive-projects__back-link:hover { text-decoration: underline; }
.single-project__hero {
background: var(--color-surface-container-low, #1a1b21);
+9 -18
View File
@@ -29,49 +29,40 @@
@media screen and (min-width: 62.5rem) {
.menu-vdi {
.menu-vdi__toggle {
@apply flex items-center gap-2;
}
.menu-vdi__toggle { @apply flex items-center gap-2; }
.menu-vdi__item--parent {
@apply relative;
}
.menu-vdi__item--parent { @apply relative; }
.menu-vdi__submenu {
@apply bg-white shadow-lg left-4 w-64 flex-col;
top: calc(100% + 1rem);
>li {
@apply w-full;
}
>li { @apply w-full; }
.menu-vdi__item {
/* text */
@apply font-bold text-18px text-black hover:text-light no-underline leading-snug;
@apply font-bold text-18px text-black! hover:text-light! no-underline leading-snug;
/* spacing & display */
@apply block w-full;
/* interaction */
@apply focus-visible:bg-secondary-200 hover:bg-secondary-200;
a {
@apply block w-full;
}
a { @apply block w-full; }
}
a.menu-vdi__item,
.menu-vdi__item a {
@apply p-4;
}
.menu-vdi__item a { @apply p-4; }
}
}
}
.nav-main a,
.nav-aux a {
color: var(--color-on-surface, #e3e1e9);
color: #000;
text-decoration: none;
}
.nav-main a:hover,
.nav-aux a:hover {
color: var(--color-primary, #b6c4ff);
color: var(--color-primary);
}
-57
View File
@@ -1,57 +0,0 @@
<?php
/**
* Project Type Taxonomy Archive Template
*
* @package KsPortfolio
*/
namespace KsPortfolio;
get_header();
$queried = get_queried_object();
$term_name = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
?>
<section class="archive-projects">
<div class="container mx-auto my-section">
<header class="archive-projects__head">
<h1 class="archive-projects__title"><?php echo esc_html( $term_name ); ?></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__( '&larr; 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' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
)
);
?>
</div>
<?php else : ?>
<div class="archive-projects__empty">
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', '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>
</section>
<?php
get_footer();