Skip to content

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.

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/.

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 ServiceSchemaLegalService 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

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').

  • PersonSchema emits name, permalink, thumbnail_image, role as jobTitle, the excerpt, worksFor hardcoded as “B P Collins LLP”, and sameAs set to the LinkedIn URL from team_social_links.
  • ServiceSchema emits LegalService with the office postal address hardcoded (inc/seo.php:200-207), a fixed sameAs list (Trustpilot, Companies House, Facebook, LinkedIn, Twitter, Instagram, YouTube), and aggregateRating calculated from up to 12 review posts — 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]
  • 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 by ServiceSchema.
  • Per-post: _yoast_wpseo_title, _yoast_wpseo_metadesc (Yoast), video_duration, podcast_duration, role, thumbnail_image, team_social_links.
  • Category slugs videos and podcasts are matched by name in header.php — they are effectively configuration held in content.
  • robots.txt is a static file at the repository root.
  • A Facebook domain verification meta tag is hardcoded in header.php:6.
  • yoast_seo_development_mode is 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 then array_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.
  • PersonSchema and ServiceSchema use "Description" and "AreaServed" with capital letters (inc/seo.php:89, 196, 199). Schema.org properties are case-sensitive and the correct spellings are description / areaServed, so those values are currently ignored by consumers.
  • ServiceSchema divides by $reviewCount before checking it is non-zero (inc/seo.php:191), and $reviews is undefined when no review source is configured — PHP warnings on such service pages.
  • partials/additional-schema.php is dead code. Its only call site is commented out at footer.php:764. It contains an older, overlapping Person and Service markup — 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 against inc/seo.php.
  • Video/podcast JSON-LD is built by string interpolation in header.php, not wp_json_encode. esc_js() is not a JSON escaper — a title containing certain characters can break the block.
  • VideoObject includes the full post content as transcript, inflating every such page’s HTML.
  • wp_head_pagination_mod() slices the URL at the substring page (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 Person implementations existed historically (inc/seo.php and the dead partial). Only the Yoast graph piece is live.
  • New schema type → add an Abstract_Schema_Piece subclass in inc/seo.php plus a wpseo_schema_graph_pieces filter, 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 — as inc/faq-schema.php:130 does — never string interpolation.
  • Fixing the Description/AreaServed casing 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.