Skip to content

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.

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

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)

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:

  1. Child services exist → “Our Title services” list of children.
  2. No children but has a parent → “Our Parent services” list of siblings (post__not_in the current page).
  3. Related insights exist (posts whose specialist_services ACF value LIKEs this service’s ID) → “Insights on Title”, 2 cards.
  4. Otherwise, insights tagged to the top-level ancestor → “Insights on Ancestor”.
  5. 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]

No environment variables. Per-service ACF fields that change the page:

  • display_faqs, faq_title, faq_select
  • hide_latest_posts, sidebar_custom_heading, sidebar_custom_links, sidebar_custom_block, sidebar_testimonial_quote, additional_testimonial_quotes, accreditations_title, accreditations
  • show_drink_driving_section, disable_review_carousel, page_reviews
  • logo_carousel, show_custom_logos, custom_logos
  • work_highlights_repeater, featured_people, team_heading, linked_services
  • content (drives the inline insight CTA bar)

Global (Global Options ACF page): rc_latest_reviews, rc_featured_reviews.

  • service posts and the services taxonomy on team are 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), and single-team.php:112 matches by title. Renaming a service without renaming its term breaks those links silently.
  • Related insights use meta_query LIKE '"<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_title variable 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.
  • $content is echoed at single-service.php:80 and never set in this template. It appears to be a leftover; it renders nothing.
  • aggregateRating divides by $reviewCount without a zero guard (inc/seo.php:191). If $reviews is empty the division emits a PHP warning before the if ($reviewCount > 0) check saves the output. $reviews is 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') at inc/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.php rather than sharing it.
  • New section on service pages → add the block directory under blocks/, then require it at the right point in single-service.php and 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 new Abstract_Schema_Piece subclass plus a wpseo_schema_graph_pieces filter, as the two existing classes do.
  • Renaming a service → rename the matching services term on team in the same sitting, and check the Redirection plugin for the old URL.
  • Rebuild assets from wp-content/themes/bpcollins with gulp before testing.
  • Deliberately not abstracted: single-service.php and single-sector.php are 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 + FAQPage output.

None. No automated tests exist in this repository; bitbucket-pipelines.yml only deploys. Verify in a browser and with Google’s Rich Results Test.