SEO and structured data
The theme layers a lot on top of Yoast SEO: two custom schema graph pieces
(Person for team members, LegalService for services), inline JSON-LD for video
and podcast articles, an independent FAQPage emitter, breadcrumb rewriting,
canonical fixes, and an HTML sitemap shortcode.
Search visibility is this site’s main acquisition channel, so changes here have commercial consequences and should be verified with Google’s Rich Results Test.
Why it exists
Section titled “Why it exists”Yoast handles titles, meta and the base schema graph. Everything here exists
because a law firm needs entity markup Yoast does not produce out of the box:
individual solicitors as Person, practice areas as LegalService with an
aggregate rating, and content-type markup for the Knowledge Hub’s video and
podcast formats.
Yoast’s own breadcrumbs are removed from the schema graph and replaced with a
hand-built trail so the Knowledge Hub sits correctly in the hierarchy — insights
live at the site root but must read as children of /knowledge-hub/.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/bpcollins/inc/seo.php:46 |
PersonSchema — Yoast graph piece for team singles |
wp-content/themes/bpcollins/inc/seo.php:110 |
ServiceSchema — LegalService graph piece with aggregateRating |
wp-content/themes/bpcollins/inc/seo.php:9-38 |
Removes Yoast breadcrumbs from the schema graph and the WebPage piece |
wp-content/themes/bpcollins/inc/template-functions.php:246 |
yoast_seo_breadcrumb_append_link() — rewrites the visible breadcrumb trail |
wp-content/themes/bpcollins/functions.php:1163 |
Canonical for paginated Knowledge Hub pages |
wp-content/themes/bpcollins/functions.php:1174 |
“ - Page N“ title suffix for paginated hub pages |
wp-content/themes/bpcollins/functions.php:1338 |
force_canonical_with_trailing_slash() |
wp-content/themes/bpcollins/functions.php:295 |
wp_head_pagination_mod() — rel=prev/rel=next link tags |
wp-content/themes/bpcollins/header.php:~140-235 |
Inline VideoObject and PodcastEpisode JSON-LD for those categories |
wp-content/themes/bpcollins/inc/faq-schema.php |
FAQPage JSON-LD (see the FAQs page) |
wp-content/themes/bpcollins/inc/sitemap-shortcode.php |
[simple-sitemap] HTML sitemap |
How it works
Section titled “How it works”Yoast graph pieces. Both custom pieces extend
Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece and are appended via the
wpseo_schema_graph_pieces filter at priority 11. is_needed() gates each on
is_singular('team') / is_singular('service').
PersonSchemaemits name, permalink,thumbnail_image,roleasjobTitle, the excerpt,worksForhardcoded as “B P Collins LLP”, andsameAsset to the LinkedIn URL fromteam_social_links.ServiceSchemaemitsLegalServicewith the office postal address hardcoded (inc/seo.php:200-207), a fixedsameAslist (Trustpilot, Companies House, Facebook, LinkedIn, Twitter, Instagram, YouTube), andaggregateRatingcalculated from up to 12reviewposts — see the Reviews and ratings page.
Breadcrumbs. Yoast’s breadcrumb graph piece is filtered out of the schema
entirely, and the breadcrumb property is unset from the WebPage piece
(inc/seo.php:20-38). The visible breadcrumb is then rewritten by
yoast_seo_breadcrumb_append_link(), which splices a synthetic ancestor into the
trail per context: /knowledge-hub/ (plus the primary category) for categories and
posts, /news/ for news, /careers/ for career, /people/ for team,
/services/ for service, /sectors/ for sector. It then array_shift()s the
first link — dropping the Home crumb — before returning.
Canonicals and pagination. Paginated Knowledge Hub pages get a canonical of
their own URL rather than page 1 (functions.php:1163), and a title suffixed
“ - Page N“ built from the stored _yoast_wpseo_title meta. All canonicals are
forced to a trailing slash (functions.php:1338). wp_head_pagination_mod()
writes rel="prev"/rel="next" tags by string-slicing the request URI at the
literal page.
Content-type JSON-LD. header.php inspects the post’s category and, for
videos and podcasts, echoes hand-built VideoObject / PodcastEpisode
JSON-LD inside <head>, using esc_js() on the values and pulling video_duration
/ podcast_duration ACF fields. The organisation logo comes from the
logo_dark global option.
HTML sitemap. The [simple-sitemap] shortcode
(inc/sitemap-shortcode.php) enumerates all public post types, forces page and
post to the front, always excludes attachment and block-guide, honours the
exclude_post_types_from_html_sitemap global option (whose choices are populated
dynamically by inc/acf.php:52), and renders hierarchical types with nested
<ul>s.
Search-engine hygiene elsewhere: attachment and block-guide are removed
from site search (inc/template-functions.php:600), the WP generator tag and
wlwmanifest link are removed (inc/security-functions.php:119-122), and Yoast’s
metabox is pushed to low priority (inc/seo.php:3-5).
flowchart TD Y[Yoast SEO] --> G[schema graph] G --> R[breadcrumb piece removed] A[PersonSchema: is_singular team] --> G B[ServiceSchema: is_singular service] --> G B --> C[aggregateRating from review posts] D[header.php: category videos/podcasts] --> E[inline VideoObject / PodcastEpisode] F[FAQ renderers] --> H[wp_footer FAQPage JSON-LD] I[wpseo_breadcrumb_links filter] --> J[visible trail with synthetic ancestor, Home dropped]Configuration
Section titled “Configuration”- Yoast SEO is a required plugin (
inc/required-plugins/required-plugins.php), as is Redirection (used for URL moves). - Global Options ACF fields used here:
logo_dark,exclude_post_types_from_html_sitemap, plus the review options consumed byServiceSchema. - Per-post:
_yoast_wpseo_title,_yoast_wpseo_metadesc(Yoast),video_duration,podcast_duration,role,thumbnail_image,team_social_links. - Category slugs
videosandpodcastsare matched by name inheader.php— they are effectively configuration held in content. robots.txtis a static file at the repository root.- A Facebook domain verification meta tag is hardcoded in
header.php:6.
Invariants and gotchas
Section titled “Invariants and gotchas”yoast_seo_development_modeis forced true (inc/seo.php:40), on production as well. It changes how Yoast behaves internally (no remote calls, different caching). Anyone debugging Yoast behaviour needs to know this before assuming a bug.- Breadcrumb splicing uses
array_splice()with negative lengths (inc/template-functions.php:254,266,273…) and thenarray_shift()s the result. It works for the current menu depth but is extremely position-sensitive — adding a crumb elsewhere can silently delete others. Always check the rendered trail on all six templates after touching it. - Schema breadcrumbs are gone entirely. Only the visible trail exists, so
Google gets no
BreadcrumbList. That appears deliberate but is worth stating. PersonSchemaandServiceSchemause"Description"and"AreaServed"with capital letters (inc/seo.php:89,196,199). Schema.org properties are case-sensitive and the correct spellings aredescription/areaServed, so those values are currently ignored by consumers.ServiceSchemadivides by$reviewCountbefore checking it is non-zero (inc/seo.php:191), and$reviewsis undefined when no review source is configured — PHP warnings on such service pages.partials/additional-schema.phpis dead code. Its only call site is commented out atfooter.php:764. It contains an older, overlappingPersonandServicemarkup — including a trailing comma inside a JSON object (additional-schema.php:36) that would emit invalid JSON-LD. Do not re-enable it without fixing that and de-duplicating againstinc/seo.php.- Video/podcast JSON-LD is built by string interpolation in
header.php, notwp_json_encode.esc_js()is not a JSON escaper — a title containing certain characters can break the block. VideoObjectincludes the full post content astranscript, inflating every such page’s HTML.wp_head_pagination_mod()slices the URL at the substringpage(functions.php:302), so any URL containing “page” earlier in its path produces a mangled prev/next link.- Trailing-slash canonicals are forced unconditionally, including for URLs with query strings or file extensions.
- Two
Personimplementations existed historically (inc/seo.phpand the dead partial). Only the Yoast graph piece is live.
Changing it safely
Section titled “Changing it safely”- New schema type → add an
Abstract_Schema_Piecesubclass ininc/seo.phpplus awpseo_schema_graph_piecesfilter, following the two existing classes. Prefer this over inline JSON-LD in a template: graph pieces are validated and merged by Yoast. - If you must emit JSON-LD directly, use
wp_json_encode()on a PHP array — asinc/faq-schema.php:130does — never string interpolation. - Fixing the
Description/AreaServedcasing is a genuine improvement, but it changes emitted markup on every team and service page. Do it deliberately and re-test in the Rich Results Test. - Adding a breadcrumb context → add a new
is_singular()branch and verify the splice offsets on every existing template, not just the new one. - Deliberately not abstracted: breadcrumb handling is a chain of context checks rather than a map, because each context splices at a different position.
- Verify: Rich Results Test on a team page, a service page, a video article, a podcast article, a page with FAQs, and page 2 of the Knowledge Hub. Check the visible breadcrumb on all of them too.
None. No automated tests exist in this repository. Use Google’s Rich Results Test and Search Console’s URL inspection after any schema change.