Service and sector pages
Service pages under /services/ are the firm’s practice-area marketing pages —
a three-level hierarchy with a context-aware sidebar, related insights, related
experts, reviews and FAQs. Sector pages under /sectors/ are the industry-facing
equivalent, structurally simpler.
These are the main paid- and organic-search landing pages, so their layout and
their LegalService structured data both matter commercially.
Why it exists
Section titled “Why it exists”The firm sells by practice area, and clients search by problem (“shareholder dispute”) not by department. The hierarchy — top-level service → child → grandchild — lets broad pages rank while narrow pages answer specific queries, with the sidebar always offering siblings or children so a visitor who lands deep can climb.
Almost everything on the page is conditional on ACF fields because different practice areas need different proof: some lead with reviews, some with work highlights, some with a drink-drive calculator (motoring offences).
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/bpcollins/single-service.php |
The whole service page. 841 lines, top to bottom in render order |
wp-content/themes/bpcollins/inc/cpt.php:42 |
service CPT — hierarchical, rewrite slug services |
wp-content/themes/bpcollins/inc/cpt.php:114 |
sector CPT — hierarchical, rewrite slug sectors |
wp-content/themes/bpcollins/single-sector.php |
Sector page (411 lines, same shape, fewer branches) |
wp-content/themes/bpcollins/inc/seo.php:110 |
ServiceSchema — the LegalService JSON-LD graph piece, including aggregate rating |
wp-content/themes/bpcollins/inc/cpt.php:79 |
service-type taxonomy on service (non-public, admin classification only) |
How it works
Section titled “How it works”Depth drives a body class. single-service.php:46-59 walks parents and adds
service-parent, service-child or service-grandchild to the <article>.
Styling and some layout decisions hang off that class rather than off template
logic.
The sidebar is a single if/elseif chain (single-service.php:220-326), first
match wins:
- Child services exist → “Our Title services” list of children.
- No children but has a parent → “Our Parent services” list of siblings
(
post__not_inthe current page). - Related insights exist (posts whose
specialist_servicesACF valueLIKEs this service’s ID) → “Insights on Title”, 2 cards. - Otherwise, insights tagged to the top-level ancestor → “Insights on Ancestor”.
- Otherwise the 3 most recent posts, “Latest updates”.
Branches 3–5 are all skipped when the ACF field hide_latest_posts is set.
Sidebar links get a child / grandchild class from the
$get_sidebar_link_class closure (single-service.php:127-149) based on the
linked page’s relationship to the current one.
Below that, still in the sidebar: an optional custom heading + link list
(sidebar_custom_heading, sidebar_custom_links), an optional promo block
(sidebar_custom_block, with a link/heading/content/background image), a
testimonial quote plus optional extra quotes rendered as a Swiper carousel
(sidebar_testimonial_quote, additional_testimonial_quotes), and the
accreditations partial.
Main column. the_content(), then an insight-cta-bar block if the ACF
content field is set, then the FAQ accordion when display_faqs is on and
faq_select has entries. The FAQ loop echoes post_title/post_content from
selected faq CPT posts and then calls
bpcollins_collect_faq_schema_items($faq_select) (single-service.php:108) to
register them for the page’s FAQPage JSON-LD — see the FAQs feature page.
Blocks after the article (in order, each conditional):
work highlights repeater → featured people / team heading → drink-drive
calculator (show_drink_driving_section) → team-strip → reviews-carousel (unless
disable_review_carousel) → staggered-content → featured-blocks → links-list →
location-tabs → cta-bar → form-block → logo-carousel.
All of those block templates are required directly, and their CSS/JS are
enqueued by hand in the array at single-service.php:3-26. Mapbox GL JS is
enqueued unconditionally on every service page (single-service.php:28) because
location-tabs may render.
Logo carousel inheritance. If the service has no custom logos, the template
falls back to the parent service’s custom_logos, and hides the section with
d-none if the parent’s own logo_carousel setting is disabled
(single-service.php:496-521).
Structured data. ServiceSchema::generate() (inc/seo.php:132) emits
LegalService with the firm’s postal address hardcoded (inc/seo.php:200-207),
a sameAs list of social/Trustpilot/Companies House URLs, and — when reviews are
found — an aggregateRating averaged over up to 12 review posts’ star_rating
fields. Review selection prefers the page’s page_reviews, then the global
rc_latest_reviews, then rc_featured_reviews (inc/seo.php:147-179).
flowchart TD A[single-service.php] --> B[depth → service-parent/child/grandchild class] A --> C[main: content → insight-cta-bar → FAQ accordion] C --> D[bpcollins_collect_faq_schema_items] A --> E{sidebar chain} E -->|children exist| E1[child services] E -->|has parent| E2[sibling services] E -->|related insights| E3[insights on this service] E -->|ancestor insights| E4[insights on top-level ancestor] E -->|else| E5[3 latest posts] A --> F[blocks: work highlights, team, reviews, maps, forms, logos] A --> G[Yoast graph: LegalService + aggregateRating]Configuration
Section titled “Configuration”No environment variables. Per-service ACF fields that change the page:
display_faqs,faq_title,faq_selecthide_latest_posts,sidebar_custom_heading,sidebar_custom_links,sidebar_custom_block,sidebar_testimonial_quote,additional_testimonial_quotes,accreditations_title,accreditationsshow_drink_driving_section,disable_review_carousel,page_reviewslogo_carousel,show_custom_logos,custom_logoswork_highlights_repeater,featured_people,team_heading,linked_servicescontent(drives the inline insight CTA bar)
Global (Global Options ACF page): rc_latest_reviews, rc_featured_reviews.
Invariants and gotchas
Section titled “Invariants and gotchas”serviceposts and theservicestaxonomy onteamare different objects. Related-experts lookups convert service post slugs to taxonomy term slugs (functions.php:983-998,blocks/hub-search/block-hub-search.php:339-355), andsingle-team.php:112matches by title. Renaming a service without renaming its term breaks those links silently.- Related insights use
meta_queryLIKE '"<id>"'against the serialised ACF relationship value (single-service.php:158-164). It relies on the quoting of serialised arrays; it is not a taxonomy query and cannot be indexed usefully. - The
$faq_titlevariable is reused as the loop variable (single-service.php:91), so after the first FAQ item the heading variable no longer holds the heading. Harmless today because the heading renders first — but do not move that block. $contentis echoed atsingle-service.php:80and never set in this template. It appears to be a leftover; it renders nothing.aggregateRatingdivides by$reviewCountwithout a zero guard (inc/seo.php:191). If$reviewsis empty the division emits a PHP warning before theif ($reviewCount > 0)check saves the output.$reviewsis also undefined if none of the three review sources are set.- The firm’s postal address is hardcoded in the schema class
(
inc/seo.php:200-207), not read from Company Info options. A move of office means editing PHP. - Yoast is put in development mode by
add_filter('yoast_seo_development_mode', '__return_true')atinc/seo.php:40. That disables Yoast’s own remote/caching behaviour on production too. It appears deliberate but is worth knowing before debugging Yoast oddities. - Mapbox GL is loaded on every service page whether a map renders or not.
- Sector pages duplicate much of the service template’s sidebar and block logic
in
single-sector.phprather than sharing it.
Changing it safely
Section titled “Changing it safely”- New section on service pages → add the block directory under
blocks/, thenrequireit at the right point insingle-service.phpand add its slug to the enqueue array at the top of the file. Missing the second step gives you unstyled markup. - New sidebar variant → extend the if/elseif chain, and remember the chain is first-match: putting a new branch above branch 1 hides the child-services list.
- Changing the schema →
inc/seo.php. Adding a graph piece means a newAbstract_Schema_Piecesubclass plus awpseo_schema_graph_piecesfilter, as the two existing classes do. - Renaming a service → rename the matching
servicesterm onteamin the same sitting, and check the Redirection plugin for the old URL. - Rebuild assets from
wp-content/themes/bpcollinswith gulp before testing. - Deliberately not abstracted:
single-service.phpandsingle-sector.phpare parallel copies. Merging them is a real refactor with real regression risk across every practice-area page — do not do it as a drive-by. - Manual verification: pick one page at each depth (parent, child, grandchild) and
check the sidebar heading, the FAQ accordion, and the Rich Results test for
LegalService+FAQPageoutput.
None. No automated tests exist in this repository; bitbucket-pipelines.yml only
deploys. Verify in a browser and with Google’s Rich Results Test.