From e20f03209c1cd5ade9c337c7738c662b7572baeb Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Tue, 14 Jul 2026 10:30:07 +0200 Subject: [PATCH 1/2] feat(breadcrumbs): add native breadcrumbs dynamic block - Register ls-plugin/breadcrumbs, a dynamic block rendering nav.ls-crumbs - Build the trail entirely from core WordPress data, no plugin dependency - Walk nested ancestors for pages, posts, and categories/tags/taxonomies - Handle front page, blog home, search, and 404 cases - Add editor placeholder preview and front-end styling --- CHANGELOG.md | 1 + build/blocks/breadcrumbs/block.json | 33 ++++ build/blocks/breadcrumbs/index.asset.php | 1 + build/blocks/breadcrumbs/index.js | 1 + build/blocks/breadcrumbs/render.php | 180 +++++++++++++++++++ build/blocks/breadcrumbs/style-index-rtl.css | 1 + build/blocks/breadcrumbs/style-index.css | 1 + inc/class-breadcrumbs.php | 39 ++++ ls-plugin.php | 3 + src/blocks/breadcrumbs/block.json | 33 ++++ src/blocks/breadcrumbs/edit.js | 27 +++ src/blocks/breadcrumbs/index.js | 20 +++ src/blocks/breadcrumbs/render.php | 180 +++++++++++++++++++ src/blocks/breadcrumbs/save.js | 10 ++ src/blocks/breadcrumbs/style.scss | 42 +++++ 15 files changed, 572 insertions(+) create mode 100644 build/blocks/breadcrumbs/block.json create mode 100644 build/blocks/breadcrumbs/index.asset.php create mode 100644 build/blocks/breadcrumbs/index.js create mode 100644 build/blocks/breadcrumbs/render.php create mode 100644 build/blocks/breadcrumbs/style-index-rtl.css create mode 100644 build/blocks/breadcrumbs/style-index.css create mode 100644 inc/class-breadcrumbs.php create mode 100644 src/blocks/breadcrumbs/block.json create mode 100644 src/blocks/breadcrumbs/edit.js create mode 100644 src/blocks/breadcrumbs/index.js create mode 100644 src/blocks/breadcrumbs/render.php create mode 100644 src/blocks/breadcrumbs/save.js create mode 100644 src/blocks/breadcrumbs/style.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index b01a05d..4b0d83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added a new Breadcrumbs dynamic block (`ls-plugin/breadcrumbs`) that builds its trail entirely from core WordPress data (front page, blog, category/tag/taxonomy archives with nested term ancestors, single posts, pages, search, and 404), with no dependency on any third-party plugin. Includes an editor placeholder preview and front-end styling for the `nav.ls-crumbs` trail. - Added default term seeding for Portfolio taxonomies (Industries, Software, Project types, and Services) so a fresh plugin install reproduces the approved term set instead of requiring manual term creation. - Added a Style Switcher block with selectable theme style variations and configurable icon display behaviour. - Added a Button Icon selector panel for core Button blocks, including left/right positioning and up/down icon options. diff --git a/build/blocks/breadcrumbs/block.json b/build/blocks/breadcrumbs/block.json new file mode 100644 index 0000000..c52f745 --- /dev/null +++ b/build/blocks/breadcrumbs/block.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "ls-plugin/breadcrumbs", + "version": "1.0.0", + "title": "Breadcrumbs", + "category": "theme", + "description": "Displays a breadcrumb navigation trail for the current page, built entirely from core WordPress data.", + "keywords": [ + "breadcrumb", + "breadcrumbs", + "navigation" + ], + "textdomain": "ls-plugin", + "editorScript": "file:./index.js", + "style": "file:./style-index.css", + "supports": { + "html": false, + "align": [ + "wide", + "full" + ], + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + } + }, + "render": "file:./render.php" +} \ No newline at end of file diff --git a/build/blocks/breadcrumbs/index.asset.php b/build/blocks/breadcrumbs/index.asset.php new file mode 100644 index 0000000..705af23 --- /dev/null +++ b/build/blocks/breadcrumbs/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-i18n'), 'version' => '6f5af63b3f3bd6404efb'); diff --git a/build/blocks/breadcrumbs/index.js b/build/blocks/breadcrumbs/index.js new file mode 100644 index 0000000..16fb1e0 --- /dev/null +++ b/build/blocks/breadcrumbs/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var r,n={157(){const r=window.wp.blocks,n=window.wp.i18n,e=window.wp.blockEditor,i=window.ReactJSXRuntime,l=JSON.parse('{"UU":"ls-plugin/breadcrumbs"}');(0,r.registerBlockType)(l.UU,{icon:"admin-links",edit:function(){const r=(0,e.useBlockProps)({className:"ls-crumbs-placeholder"});return(0,i.jsx)("nav",{...r,"aria-label":(0,n.__)("Breadcrumb","ls-plugin"),children:(0,i.jsx)("span",{children:(0,n.__)("Breadcrumbs (dynamic)","ls-plugin")})})},save:function(){return null}})}},e={};function i(r){var l=e[r];if(void 0!==l)return l.exports;var o=e[r]={exports:{}};return n[r](o,o.exports,i),o.exports}i.m=n,r=[],i.O=(n,e,l,o)=>{if(!e){var s=1/0;for(u=0;u=o)&&Object.keys(i.O).every(r=>i.O[r](e[t]))?e.splice(t--,1):(a=!1,o0&&r[u-1][2]>o;u--)r[u]=r[u-1];r[u]=[e,l,o]},i.o=(r,n)=>Object.prototype.hasOwnProperty.call(r,n),(()=>{var r={863:0,23:0};i.O.j=n=>0===r[n];var n=(n,e)=>{var l,o,[s,a,t]=e,c=0;if(s.some(n=>0!==r[n])){for(l in a)i.o(a,l)&&(i.m[l]=a[l]);if(t)var u=t(i)}for(n&&n(e);ci(157));l=i.O(l)})(); \ No newline at end of file diff --git a/build/blocks/breadcrumbs/render.php b/build/blocks/breadcrumbs/render.php new file mode 100644 index 0000000..41b44be --- /dev/null +++ b/build/blocks/breadcrumbs/render.php @@ -0,0 +1,180 @@ + 'ls-crumbs', + 'aria-label' => __( 'Breadcrumb', 'ls-plugin' ), + ) +); + +$ls_breadcrumbs_before = ''; + +$ls_breadcrumbs_trail = array(); + +if ( is_front_page() ) { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Home', 'ls-plugin' ), + 'url' => null, + ); +} else { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Home', 'ls-plugin' ), + 'url' => home_url( '/' ), + ); + + if ( is_home() ) { + $ls_breadcrumbs_page_for_posts = (int) get_option( 'page_for_posts' ); + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_page_for_posts + ? get_the_title( $ls_breadcrumbs_page_for_posts ) + : __( 'Blog', 'ls-plugin' ), + 'url' => null, + ); + } elseif ( is_category() || is_tag() || is_tax() ) { + $ls_breadcrumbs_term = get_queried_object(); + + if ( $ls_breadcrumbs_term instanceof WP_Term ) { + if ( is_taxonomy_hierarchical( $ls_breadcrumbs_term->taxonomy ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( + get_ancestors( $ls_breadcrumbs_term->term_id, $ls_breadcrumbs_term->taxonomy, 'taxonomy' ) + ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, $ls_breadcrumbs_term->taxonomy ); + + if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_ancestor_term->name, + 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), + ); + } + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_term->name, + 'url' => null, + ); + } + } elseif ( is_single() ) { + $ls_breadcrumbs_post = get_queried_object(); + + if ( $ls_breadcrumbs_post instanceof WP_Post ) { + $ls_breadcrumbs_post_type = get_post_type( $ls_breadcrumbs_post ); + + if ( is_post_type_hierarchical( $ls_breadcrumbs_post_type ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), + 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), + ); + } + } elseif ( 'post' === $ls_breadcrumbs_post_type ) { + $ls_breadcrumbs_categories = get_the_category( $ls_breadcrumbs_post->ID ); + + if ( ! empty( $ls_breadcrumbs_categories ) ) { + $ls_breadcrumbs_primary_term = $ls_breadcrumbs_categories[0]; + + if ( is_taxonomy_hierarchical( 'category' ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( + get_ancestors( $ls_breadcrumbs_primary_term->term_id, 'category', 'taxonomy' ) + ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, 'category' ); + + if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_ancestor_term->name, + 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), + ); + } + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_primary_term->name, + 'url' => get_term_link( $ls_breadcrumbs_primary_term ), + ); + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_post ), + 'url' => null, + ); + } + } elseif ( is_page() ) { + $ls_breadcrumbs_post = get_queried_object(); + + if ( $ls_breadcrumbs_post instanceof WP_Post ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), + 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), + ); + } + + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_post ), + 'url' => null, + ); + } + } elseif ( is_search() ) { + $ls_breadcrumbs_trail[] = array( + /* translators: %s: search query */ + 'label' => sprintf( __( 'Search results for: %s', 'ls-plugin' ), get_search_query() ), + 'url' => null, + ); + } elseif ( is_404() ) { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Page not found', 'ls-plugin' ), + 'url' => null, + ); + } else { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_archive_title(), + 'url' => null, + ); + } +} + +echo $ls_breadcrumbs_before; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + +$ls_breadcrumbs_last_index = count( $ls_breadcrumbs_trail ) - 1; + +foreach ( $ls_breadcrumbs_trail as $ls_breadcrumbs_index => $ls_breadcrumbs_crumb ) { + if ( $ls_breadcrumbs_index === $ls_breadcrumbs_last_index || empty( $ls_breadcrumbs_crumb['url'] ) ) { + printf( + '%s', + esc_html( $ls_breadcrumbs_crumb['label'] ) + ); + continue; + } + + printf( + '%2$s/', + esc_url( $ls_breadcrumbs_crumb['url'] ), + esc_html( $ls_breadcrumbs_crumb['label'] ) + ); +} + +echo $ls_breadcrumbs_after; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/build/blocks/breadcrumbs/style-index-rtl.css b/build/blocks/breadcrumbs/style-index-rtl.css new file mode 100644 index 0000000..056ef16 --- /dev/null +++ b/build/blocks/breadcrumbs/style-index-rtl.css @@ -0,0 +1 @@ +.ls-crumbs{align-items:center;color:var(--wp--custom--color--text--muted,currentColor);display:flex;flex-wrap:wrap;font-size:.875rem;gap:.35em;line-height:1.4}.ls-crumbs a{color:inherit;text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s ease}.ls-crumbs a:focus,.ls-crumbs a:hover{text-decoration-color:currentColor}.ls-crumbs i{font-style:normal;opacity:.5}.ls-crumbs span[aria-current=page]{color:var(--wp--custom--color--text--default,currentColor);font-weight:500}.ls-crumbs-placeholder{border:1px dashed;font-style:italic;opacity:.6;padding:.5em .75em} diff --git a/build/blocks/breadcrumbs/style-index.css b/build/blocks/breadcrumbs/style-index.css new file mode 100644 index 0000000..056ef16 --- /dev/null +++ b/build/blocks/breadcrumbs/style-index.css @@ -0,0 +1 @@ +.ls-crumbs{align-items:center;color:var(--wp--custom--color--text--muted,currentColor);display:flex;flex-wrap:wrap;font-size:.875rem;gap:.35em;line-height:1.4}.ls-crumbs a{color:inherit;text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s ease}.ls-crumbs a:focus,.ls-crumbs a:hover{text-decoration-color:currentColor}.ls-crumbs i{font-style:normal;opacity:.5}.ls-crumbs span[aria-current=page]{color:var(--wp--custom--color--text--default,currentColor);font-weight:500}.ls-crumbs-placeholder{border:1px dashed;font-style:italic;opacity:.6;padding:.5em .75em} diff --git a/inc/class-breadcrumbs.php b/inc/class-breadcrumbs.php new file mode 100644 index 0000000..257aedd --- /dev/null +++ b/inc/class-breadcrumbs.php @@ -0,0 +1,39 @@ + + { __( 'Breadcrumbs (dynamic)', 'ls-plugin' ) } + + ); +} diff --git a/src/blocks/breadcrumbs/index.js b/src/blocks/breadcrumbs/index.js new file mode 100644 index 0000000..5b4d8d7 --- /dev/null +++ b/src/blocks/breadcrumbs/index.js @@ -0,0 +1,20 @@ +/** + * Registers a new block provided a unique name and an object defining its behavior. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import Edit from './edit.js'; +import save from './save.js'; +import metadata from './block.json'; +import './style.scss'; + +registerBlockType( metadata.name, { + icon: 'admin-links', + edit: Edit, + save, +} ); diff --git a/src/blocks/breadcrumbs/render.php b/src/blocks/breadcrumbs/render.php new file mode 100644 index 0000000..41b44be --- /dev/null +++ b/src/blocks/breadcrumbs/render.php @@ -0,0 +1,180 @@ + 'ls-crumbs', + 'aria-label' => __( 'Breadcrumb', 'ls-plugin' ), + ) +); + +$ls_breadcrumbs_before = ''; + +$ls_breadcrumbs_trail = array(); + +if ( is_front_page() ) { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Home', 'ls-plugin' ), + 'url' => null, + ); +} else { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Home', 'ls-plugin' ), + 'url' => home_url( '/' ), + ); + + if ( is_home() ) { + $ls_breadcrumbs_page_for_posts = (int) get_option( 'page_for_posts' ); + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_page_for_posts + ? get_the_title( $ls_breadcrumbs_page_for_posts ) + : __( 'Blog', 'ls-plugin' ), + 'url' => null, + ); + } elseif ( is_category() || is_tag() || is_tax() ) { + $ls_breadcrumbs_term = get_queried_object(); + + if ( $ls_breadcrumbs_term instanceof WP_Term ) { + if ( is_taxonomy_hierarchical( $ls_breadcrumbs_term->taxonomy ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( + get_ancestors( $ls_breadcrumbs_term->term_id, $ls_breadcrumbs_term->taxonomy, 'taxonomy' ) + ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, $ls_breadcrumbs_term->taxonomy ); + + if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_ancestor_term->name, + 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), + ); + } + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_term->name, + 'url' => null, + ); + } + } elseif ( is_single() ) { + $ls_breadcrumbs_post = get_queried_object(); + + if ( $ls_breadcrumbs_post instanceof WP_Post ) { + $ls_breadcrumbs_post_type = get_post_type( $ls_breadcrumbs_post ); + + if ( is_post_type_hierarchical( $ls_breadcrumbs_post_type ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), + 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), + ); + } + } elseif ( 'post' === $ls_breadcrumbs_post_type ) { + $ls_breadcrumbs_categories = get_the_category( $ls_breadcrumbs_post->ID ); + + if ( ! empty( $ls_breadcrumbs_categories ) ) { + $ls_breadcrumbs_primary_term = $ls_breadcrumbs_categories[0]; + + if ( is_taxonomy_hierarchical( 'category' ) ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( + get_ancestors( $ls_breadcrumbs_primary_term->term_id, 'category', 'taxonomy' ) + ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, 'category' ); + + if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_ancestor_term->name, + 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), + ); + } + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => $ls_breadcrumbs_primary_term->name, + 'url' => get_term_link( $ls_breadcrumbs_primary_term ), + ); + } + } + + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_post ), + 'url' => null, + ); + } + } elseif ( is_page() ) { + $ls_breadcrumbs_post = get_queried_object(); + + if ( $ls_breadcrumbs_post instanceof WP_Post ) { + $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); + + foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), + 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), + ); + } + + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_title( $ls_breadcrumbs_post ), + 'url' => null, + ); + } + } elseif ( is_search() ) { + $ls_breadcrumbs_trail[] = array( + /* translators: %s: search query */ + 'label' => sprintf( __( 'Search results for: %s', 'ls-plugin' ), get_search_query() ), + 'url' => null, + ); + } elseif ( is_404() ) { + $ls_breadcrumbs_trail[] = array( + 'label' => __( 'Page not found', 'ls-plugin' ), + 'url' => null, + ); + } else { + $ls_breadcrumbs_trail[] = array( + 'label' => get_the_archive_title(), + 'url' => null, + ); + } +} + +echo $ls_breadcrumbs_before; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + +$ls_breadcrumbs_last_index = count( $ls_breadcrumbs_trail ) - 1; + +foreach ( $ls_breadcrumbs_trail as $ls_breadcrumbs_index => $ls_breadcrumbs_crumb ) { + if ( $ls_breadcrumbs_index === $ls_breadcrumbs_last_index || empty( $ls_breadcrumbs_crumb['url'] ) ) { + printf( + '%s', + esc_html( $ls_breadcrumbs_crumb['label'] ) + ); + continue; + } + + printf( + '%2$s/', + esc_url( $ls_breadcrumbs_crumb['url'] ), + esc_html( $ls_breadcrumbs_crumb['label'] ) + ); +} + +echo $ls_breadcrumbs_after; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/src/blocks/breadcrumbs/save.js b/src/blocks/breadcrumbs/save.js new file mode 100644 index 0000000..d98dfc0 --- /dev/null +++ b/src/blocks/breadcrumbs/save.js @@ -0,0 +1,10 @@ +/** + * Save component for the breadcrumbs block. + * + * Dynamic block — output is generated by render.php. + * + * @return {null} Nothing to save. + */ +export default function save() { + return null; +} diff --git a/src/blocks/breadcrumbs/style.scss b/src/blocks/breadcrumbs/style.scss new file mode 100644 index 0000000..130ca50 --- /dev/null +++ b/src/blocks/breadcrumbs/style.scss @@ -0,0 +1,42 @@ +/** + * Breadcrumbs Block Styles + */ + +.ls-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35em; + font-size: 0.875rem; + line-height: 1.4; + color: var(--wp--custom--color--text--muted, currentColor); + + a { + color: inherit; + text-decoration: underline; + text-decoration-color: transparent; + transition: text-decoration-color 0.15s ease; + + &:hover, + &:focus { + text-decoration-color: currentColor; + } + } + + i { + font-style: normal; + opacity: 0.5; + } + + span[aria-current="page"] { + color: var(--wp--custom--color--text--default, currentColor); + font-weight: 500; + } +} + +.ls-crumbs-placeholder { + padding: 0.5em 0.75em; + border: 1px dashed currentColor; + opacity: 0.6; + font-style: italic; +} From eae90cad4f1387ee08541d14d9d027b4de388a00 Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Tue, 14 Jul 2026 10:58:46 +0200 Subject: [PATCH 2/2] fix(breadcrumbs): guard get_term_link() against WP_Error, dedupe ancestor logic - Extract taxonomy/post ancestor-walking into static helpers on LS_Plugin_Breadcrumbs (add_taxonomy_ancestors, add_post_ancestors, get_term_url), removing 3x duplicated logic across render.php branches - Fix a real PHP 8 TypeError risk: get_term_link() can return WP_Error, which was being passed straight into esc_url() unguarded - Addresses Gemini Code Assist and CodeRabbit review feedback on PR #16 --- build/blocks/breadcrumbs/render.php | 54 +++----------------------- inc/class-breadcrumbs.php | 60 +++++++++++++++++++++++++++++ src/blocks/breadcrumbs/render.php | 54 +++----------------------- 3 files changed, 70 insertions(+), 98 deletions(-) diff --git a/build/blocks/breadcrumbs/render.php b/build/blocks/breadcrumbs/render.php index 41b44be..4d05975 100644 --- a/build/blocks/breadcrumbs/render.php +++ b/build/blocks/breadcrumbs/render.php @@ -48,22 +48,7 @@ $ls_breadcrumbs_term = get_queried_object(); if ( $ls_breadcrumbs_term instanceof WP_Term ) { - if ( is_taxonomy_hierarchical( $ls_breadcrumbs_term->taxonomy ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( - get_ancestors( $ls_breadcrumbs_term->term_id, $ls_breadcrumbs_term->taxonomy, 'taxonomy' ) - ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, $ls_breadcrumbs_term->taxonomy ); - - if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { - $ls_breadcrumbs_trail[] = array( - 'label' => $ls_breadcrumbs_ancestor_term->name, - 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), - ); - } - } - } + LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_term ); $ls_breadcrumbs_trail[] = array( 'label' => $ls_breadcrumbs_term->name, @@ -77,40 +62,18 @@ $ls_breadcrumbs_post_type = get_post_type( $ls_breadcrumbs_post ); if ( is_post_type_hierarchical( $ls_breadcrumbs_post_type ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_trail[] = array( - 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), - 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), - ); - } + LS_Plugin_Breadcrumbs::add_post_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_post ); } elseif ( 'post' === $ls_breadcrumbs_post_type ) { $ls_breadcrumbs_categories = get_the_category( $ls_breadcrumbs_post->ID ); if ( ! empty( $ls_breadcrumbs_categories ) ) { $ls_breadcrumbs_primary_term = $ls_breadcrumbs_categories[0]; - if ( is_taxonomy_hierarchical( 'category' ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( - get_ancestors( $ls_breadcrumbs_primary_term->term_id, 'category', 'taxonomy' ) - ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, 'category' ); - - if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { - $ls_breadcrumbs_trail[] = array( - 'label' => $ls_breadcrumbs_ancestor_term->name, - 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), - ); - } - } - } + LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_primary_term ); $ls_breadcrumbs_trail[] = array( 'label' => $ls_breadcrumbs_primary_term->name, - 'url' => get_term_link( $ls_breadcrumbs_primary_term ), + 'url' => LS_Plugin_Breadcrumbs::get_term_url( $ls_breadcrumbs_primary_term ), ); } } @@ -124,14 +87,7 @@ $ls_breadcrumbs_post = get_queried_object(); if ( $ls_breadcrumbs_post instanceof WP_Post ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_trail[] = array( - 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), - 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), - ); - } + LS_Plugin_Breadcrumbs::add_post_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_post ); $ls_breadcrumbs_trail[] = array( 'label' => get_the_title( $ls_breadcrumbs_post ), diff --git a/inc/class-breadcrumbs.php b/inc/class-breadcrumbs.php index 257aedd..94000db 100644 --- a/inc/class-breadcrumbs.php +++ b/inc/class-breadcrumbs.php @@ -36,4 +36,64 @@ public function register_block() { register_block_type( $block_path ); } } + + /** + * Appends a hierarchical taxonomy term's ancestor chain to a breadcrumb trail. + * + * Does not append the term itself — only its ancestors, root-first. + * + * @param array $trail Breadcrumb trail, passed by reference. + * @param WP_Term $term Term whose ancestors should be added. + * @return void + */ + public static function add_taxonomy_ancestors( array &$trail, $term ) { + if ( ! is_taxonomy_hierarchical( $term->taxonomy ) ) { + return; + } + + $ancestor_ids = array_reverse( get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) ); + + foreach ( $ancestor_ids as $ancestor_id ) { + $ancestor_term = get_term( $ancestor_id, $term->taxonomy ); + + if ( $ancestor_term instanceof WP_Term ) { + $trail[] = array( + 'label' => $ancestor_term->name, + 'url' => self::get_term_url( $ancestor_term ), + ); + } + } + } + + /** + * Appends a post's ancestor chain to a breadcrumb trail. + * + * Does not append the post itself — only its ancestors, root-first. + * + * @param array $trail Breadcrumb trail, passed by reference. + * @param WP_Post $post Post whose ancestors should be added. + * @return void + */ + public static function add_post_ancestors( array &$trail, $post ) { + $ancestor_ids = array_reverse( get_post_ancestors( $post ) ); + + foreach ( $ancestor_ids as $ancestor_id ) { + $trail[] = array( + 'label' => get_the_title( $ancestor_id ), + 'url' => get_permalink( $ancestor_id ), + ); + } + } + + /** + * Safely resolves a term's permalink, guarding against WP_Error. + * + * @param WP_Term $term Term to link to. + * @return string|null Term permalink, or null on failure. + */ + public static function get_term_url( $term ) { + $link = get_term_link( $term ); + + return is_wp_error( $link ) ? null : $link; + } } diff --git a/src/blocks/breadcrumbs/render.php b/src/blocks/breadcrumbs/render.php index 41b44be..4d05975 100644 --- a/src/blocks/breadcrumbs/render.php +++ b/src/blocks/breadcrumbs/render.php @@ -48,22 +48,7 @@ $ls_breadcrumbs_term = get_queried_object(); if ( $ls_breadcrumbs_term instanceof WP_Term ) { - if ( is_taxonomy_hierarchical( $ls_breadcrumbs_term->taxonomy ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( - get_ancestors( $ls_breadcrumbs_term->term_id, $ls_breadcrumbs_term->taxonomy, 'taxonomy' ) - ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, $ls_breadcrumbs_term->taxonomy ); - - if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { - $ls_breadcrumbs_trail[] = array( - 'label' => $ls_breadcrumbs_ancestor_term->name, - 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), - ); - } - } - } + LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_term ); $ls_breadcrumbs_trail[] = array( 'label' => $ls_breadcrumbs_term->name, @@ -77,40 +62,18 @@ $ls_breadcrumbs_post_type = get_post_type( $ls_breadcrumbs_post ); if ( is_post_type_hierarchical( $ls_breadcrumbs_post_type ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_trail[] = array( - 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), - 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), - ); - } + LS_Plugin_Breadcrumbs::add_post_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_post ); } elseif ( 'post' === $ls_breadcrumbs_post_type ) { $ls_breadcrumbs_categories = get_the_category( $ls_breadcrumbs_post->ID ); if ( ! empty( $ls_breadcrumbs_categories ) ) { $ls_breadcrumbs_primary_term = $ls_breadcrumbs_categories[0]; - if ( is_taxonomy_hierarchical( 'category' ) ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( - get_ancestors( $ls_breadcrumbs_primary_term->term_id, 'category', 'taxonomy' ) - ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_ancestor_term = get_term( $ls_breadcrumbs_ancestor_id, 'category' ); - - if ( $ls_breadcrumbs_ancestor_term instanceof WP_Term ) { - $ls_breadcrumbs_trail[] = array( - 'label' => $ls_breadcrumbs_ancestor_term->name, - 'url' => get_term_link( $ls_breadcrumbs_ancestor_term ), - ); - } - } - } + LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_primary_term ); $ls_breadcrumbs_trail[] = array( 'label' => $ls_breadcrumbs_primary_term->name, - 'url' => get_term_link( $ls_breadcrumbs_primary_term ), + 'url' => LS_Plugin_Breadcrumbs::get_term_url( $ls_breadcrumbs_primary_term ), ); } } @@ -124,14 +87,7 @@ $ls_breadcrumbs_post = get_queried_object(); if ( $ls_breadcrumbs_post instanceof WP_Post ) { - $ls_breadcrumbs_ancestor_ids = array_reverse( get_post_ancestors( $ls_breadcrumbs_post ) ); - - foreach ( $ls_breadcrumbs_ancestor_ids as $ls_breadcrumbs_ancestor_id ) { - $ls_breadcrumbs_trail[] = array( - 'label' => get_the_title( $ls_breadcrumbs_ancestor_id ), - 'url' => get_permalink( $ls_breadcrumbs_ancestor_id ), - ); - } + LS_Plugin_Breadcrumbs::add_post_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_post ); $ls_breadcrumbs_trail[] = array( 'label' => get_the_title( $ls_breadcrumbs_post ),