Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 a navigation ref auto-resolver: when a `core/navigation` block has no `ref` attribute, it is resolved at render time to the `wp_navigation` post matching a documented slug convention (`header-navigation` by default, filterable via `ls_plugin_nav_ref_slug`), so themes never need a hardcoded, environment-specific post ID. The lookup allows multilingual plugins (WPML, Polylang) to filter it and resolve the translated nav post for the current language.
- 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.
Expand Down
33 changes: 33 additions & 0 deletions build/blocks/breadcrumbs/block.json
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions build/blocks/breadcrumbs/index.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-i18n'), 'version' => '6f5af63b3f3bd6404efb');
1 change: 1 addition & 0 deletions build/blocks/breadcrumbs/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions build/blocks/breadcrumbs/render.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php
/**
* Breadcrumbs block render template.
*
* Builds a breadcrumb trail entirely from core WordPress data (pages, posts,
* categories/tags/taxonomies, search, and 404) — no plugin dependency.
*
* @package LS_Plugin
*/

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

$ls_breadcrumbs_wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => 'ls-crumbs',
'aria-label' => __( 'Breadcrumb', 'ls-plugin' ),
)
);

$ls_breadcrumbs_before = '<nav ' . $ls_breadcrumbs_wrapper_attributes . '>';
$ls_breadcrumbs_after = '</nav>';

$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 ) {
LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_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_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];

LS_Plugin_Breadcrumbs::add_taxonomy_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_primary_term );

$ls_breadcrumbs_trail[] = array(
'label' => $ls_breadcrumbs_primary_term->name,
'url' => LS_Plugin_Breadcrumbs::get_term_url( $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_Plugin_Breadcrumbs::add_post_ancestors( $ls_breadcrumbs_trail, $ls_breadcrumbs_post );

$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(
'<span aria-current="page">%s</span>',
esc_html( $ls_breadcrumbs_crumb['label'] )
);
continue;
}

printf(
'<a href="%1$s">%2$s</a><i>/</i>',
esc_url( $ls_breadcrumbs_crumb['url'] ),
esc_html( $ls_breadcrumbs_crumb['label'] )
);
}

echo $ls_breadcrumbs_after; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1 change: 1 addition & 0 deletions build/blocks/breadcrumbs/style-index-rtl.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/blocks/breadcrumbs/style-index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions inc/class-breadcrumbs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php
/**
* Breadcrumbs Block
*
* Registers the Breadcrumbs dynamic block.
*
* @package LS_Plugin
*/

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class LS_Plugin_Breadcrumbs
*
* Handles registration for the Breadcrumbs block.
*/
class LS_Plugin_Breadcrumbs {

/**
* Constructor.
*/
public function __construct() {
add_action( 'init', array( $this, 'register_block' ) );
}

/**
* Register the breadcrumbs block from the built block assets.
*/
public function register_block() {
$block_path = LS_PLUGIN_PLUGIN_DIR . 'build/blocks/breadcrumbs';

if ( file_exists( $block_path . '/block.json' ) ) {
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;
}
}
3 changes: 3 additions & 0 deletions ls-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function ls_plugin_init() {
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-taxonomy-filter.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-style-switcher.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-carousel.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-breadcrumbs.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json-validator.php';
require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-permalinks.php';
Expand All @@ -69,6 +70,8 @@ function ls_plugin_init() {

new LS_Plugin_Carousel();

new LS_Plugin_Breadcrumbs();

// Configure SCF to use plugin-managed Local JSON paths.
new LS_Plugin_SCF_JSON();

Expand Down
33 changes: 33 additions & 0 deletions src/blocks/breadcrumbs/block.json
Original file line number Diff line number Diff line change
@@ -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"
}
27 changes: 27 additions & 0 deletions src/blocks/breadcrumbs/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';

/**
* Edit component for the breadcrumbs block.
*
* This is a dynamic block — the real breadcrumb trail is generated by
* render.php at render time. The editor only needs a static placeholder
* so the block doesn't appear blank or error while inserted.
*
* @param {Object} props Block props.
* @return {Element} Element to render.
*/
export default function Edit() {
const blockProps = useBlockProps( {
className: 'ls-crumbs-placeholder',
} );

return (
<nav { ...blockProps } aria-label={ __( 'Breadcrumb', 'ls-plugin' ) }>
<span>{ __( 'Breadcrumbs (dynamic)', 'ls-plugin' ) }</span>
</nav>
);
}
20 changes: 20 additions & 0 deletions src/blocks/breadcrumbs/index.js
Original file line number Diff line number Diff line change
@@ -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,
} );
Loading