2026-05-18 13:55:06 -05:00
|
|
|
import SEO from '@/components/SEO'
|
fix(seo): descriptions were two sentences run together on all eleven detail pages
Live until now, on every service and industry page:
"...seamless integration Delivered by Queue North, a veteran-owned..."
"...for medical providers Queue North delivers phone, contact center..."
clampDescription joined its fragments with a space, and no shortDesc in
services.js or industries.js ends in a full stop. Nothing added one. The
description is the first thing a searcher reads, and it read as a run-on on
eleven of the eighteen pages.
buildDescription replaces it and joins fragments AS SENTENCES. It also takes an
`approved` description, which it emits verbatim: never joined, never clamped.
Levi's approved copy for the two long-form pages is 164 and 191 characters, and
the old 158-character clamp would have cut his sentences off with an ellipsis.
ServiceDetail reads it from `page.seo`, which the content commit fills in.
ServiceDetail and IndustryDetail now build their canonical from SITE_URL rather
than repeating the origin. The three files release.sh greps for that literal are
untouched, so the origin guard still has three copies to compare.
Verified against a build, page by page: all 11 detail descriptions gained
exactly a full stop and changed in no other way, and no other page's description
moved.
Closes #234.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:28:25 -05:00
|
|
|
import { SITE_URL, buildBreadcrumbLd, buildDescription } from '@/lib/seo'
|
2026-05-13 00:29:45 -05:00
|
|
|
import { useParams } from 'react-router-dom'
|
2026-05-12 01:04:17 -05:00
|
|
|
import { services } from '@/data/services'
|
2026-05-17 17:42:03 -05:00
|
|
|
import { Link } from 'react-router-dom'
|
2026-05-12 01:04:17 -05:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
feat(seo): the industry and support pages now point at the two service pages
Both approved sheets ask for it: Unified Communications section 15 and Contact
Center section 18 each list the pages that should link back to them. Home, the
services index, the header and the footer already did. Managed Support and the
four industry pages did not, and the industry pages linked to no service at all.
Each industry page's own description already tells the reader Queue North
"delivers phone, contact center, network, and IT support" for that industry, so
all four qualify. The anchors are descriptive rather than the page names:
"Business phone and UCaaS solutions" and "Contact center and CCaaS solutions".
Rendered through the shared RelatedLinks component, in the industry sidebar card
and after "Ideal For" on a service page. It renders nothing without links, so
the five service pages that have none are untouched: only 5 of the 18 pages gain
a Related services block, and they are exactly the five the sheets name.
Verified against the build: both hrefs are present on managed-support and on all
four industry pages, the built-HTML audit passes with every link and fragment
resolving, and no other page gained the block.
Closes #221.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:59:49 -05:00
|
|
|
import RelatedLinks from '@/components/content/RelatedLinks'
|
2026-05-26 12:22:11 -05:00
|
|
|
import { ArrowLeft, ArrowRight, CheckCircle2, Info, Zap } from 'lucide-react'
|
2026-05-18 13:12:18 -05:00
|
|
|
|
|
|
|
|
const serviceImageAlt = {
|
|
|
|
|
'contact-center': 'Modern contact center operations dashboard and support team',
|
|
|
|
|
'infrastructure-cabling': 'Close-up of network cabling connected to infrastructure equipment',
|
|
|
|
|
'wireless-access': 'Wireless coverage planning map used for enterprise Wi-Fi design',
|
|
|
|
|
}
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-13 00:29:45 -05:00
|
|
|
const ServiceDetail = () => {
|
|
|
|
|
const { slug } = useParams()
|
|
|
|
|
const service = services.find(s => s.id === slug)
|
2026-05-12 01:04:17 -05:00
|
|
|
|
|
|
|
|
if (!service) {
|
|
|
|
|
return (
|
2026-05-17 17:11:29 -05:00
|
|
|
<section className="bg-background py-16 md:py-24">
|
2026-05-18 13:55:06 -05:00
|
|
|
<SEO
|
feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
|
|
|
title="Service Not Found | Queue North"
|
|
|
|
|
description="The service page you are looking for does not exist. Browse the services Queue North provides."
|
2026-05-18 13:55:06 -05:00
|
|
|
url="https://queuenorth.com/services"
|
feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
|
|
|
noindex
|
2026-05-18 13:55:06 -05:00
|
|
|
/>
|
2026-05-17 17:11:29 -05:00
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<h1 className="text-3xl font-bold text-primary-navy mb-4">Service Not Found</h1>
|
|
|
|
|
<p className="text-xl text-soft-text mb-8">The service you're looking for doesn't exist.</p>
|
2026-05-17 17:42:03 -05:00
|
|
|
<Link to="/services" className="text-primary-navy hover:underline">
|
2026-05-17 17:11:29 -05:00
|
|
|
Back to Services
|
2026-05-17 17:42:03 -05:00
|
|
|
</Link>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</section>
|
2026-05-12 01:04:17 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
fix(seo): descriptions were two sentences run together on all eleven detail pages
Live until now, on every service and industry page:
"...seamless integration Delivered by Queue North, a veteran-owned..."
"...for medical providers Queue North delivers phone, contact center..."
clampDescription joined its fragments with a space, and no shortDesc in
services.js or industries.js ends in a full stop. Nothing added one. The
description is the first thing a searcher reads, and it read as a run-on on
eleven of the eighteen pages.
buildDescription replaces it and joins fragments AS SENTENCES. It also takes an
`approved` description, which it emits verbatim: never joined, never clamped.
Levi's approved copy for the two long-form pages is 164 and 191 characters, and
the old 158-character clamp would have cut his sentences off with an ellipsis.
ServiceDetail reads it from `page.seo`, which the content commit fills in.
ServiceDetail and IndustryDetail now build their canonical from SITE_URL rather
than repeating the origin. The three files release.sh greps for that literal are
untouched, so the origin guard still has three copies to compare.
Verified against a build, page by page: all 11 detail descriptions gained
exactly a full stop and changed in no other way, and no other page's description
moved.
Closes #234.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:28:25 -05:00
|
|
|
// A service with owner-approved copy carries its own title and description in
|
|
|
|
|
// `page.seo`, and both are emitted verbatim. Everything else composes.
|
|
|
|
|
const serviceTitle = service.page?.seo?.title ?? `${service.name} | Queue North`
|
|
|
|
|
const serviceDesc = buildDescription({
|
|
|
|
|
approved: service.page?.seo?.description,
|
|
|
|
|
parts: [service.shortDesc, 'Delivered by Queue North, a veteran-owned 8x8 and Cisco Certified Partner.'],
|
|
|
|
|
})
|
|
|
|
|
const serviceUrl = `${SITE_URL}/services/${service.id}`
|
feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
|
|
|
const serviceBreadcrumbLd = buildBreadcrumbLd([
|
|
|
|
|
{ name: 'Services', path: '/services' },
|
|
|
|
|
{ name: service.name, path: `/services/${service.id}` },
|
|
|
|
|
])
|
2026-05-17 20:03:42 -05:00
|
|
|
const serviceDetailLd = {
|
|
|
|
|
'@context': 'https://schema.org',
|
|
|
|
|
'@type': 'Service',
|
|
|
|
|
name: service.name,
|
fix(seo): descriptions were two sentences run together on all eleven detail pages
Live until now, on every service and industry page:
"...seamless integration Delivered by Queue North, a veteran-owned..."
"...for medical providers Queue North delivers phone, contact center..."
clampDescription joined its fragments with a space, and no shortDesc in
services.js or industries.js ends in a full stop. Nothing added one. The
description is the first thing a searcher reads, and it read as a run-on on
eleven of the eighteen pages.
buildDescription replaces it and joins fragments AS SENTENCES. It also takes an
`approved` description, which it emits verbatim: never joined, never clamped.
Levi's approved copy for the two long-form pages is 164 and 191 characters, and
the old 158-character clamp would have cut his sentences off with an ellipsis.
ServiceDetail reads it from `page.seo`, which the content commit fills in.
ServiceDetail and IndustryDetail now build their canonical from SITE_URL rather
than repeating the origin. The three files release.sh greps for that literal are
untouched, so the origin guard still has three copies to compare.
Verified against a build, page by page: all 11 detail descriptions gained
exactly a full stop and changed in no other way, and no other page's description
moved.
Closes #234.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:28:25 -05:00
|
|
|
description: service.page?.seo?.description ?? service.shortDesc,
|
2026-05-17 20:03:42 -05:00
|
|
|
provider: {
|
|
|
|
|
'@type': 'Organization',
|
|
|
|
|
name: 'Queue North Technologies',
|
|
|
|
|
url: 'https://queuenorth.com',
|
|
|
|
|
},
|
|
|
|
|
areaServed: {
|
|
|
|
|
'@type': 'Place',
|
2026-05-17 20:44:18 -05:00
|
|
|
name: 'United States',
|
2026-05-17 20:03:42 -05:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 01:04:17 -05:00
|
|
|
return (
|
2026-05-17 17:11:29 -05:00
|
|
|
<>
|
2026-05-18 13:55:06 -05:00
|
|
|
<SEO
|
|
|
|
|
title={serviceTitle}
|
|
|
|
|
description={serviceDesc}
|
|
|
|
|
url={serviceUrl}
|
feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
|
|
|
jsonLd={[serviceDetailLd, serviceBreadcrumbLd]}
|
2026-05-18 13:55:06 -05:00
|
|
|
/>
|
2026-05-12 01:04:17 -05:00
|
|
|
{/* Page Hero */}
|
2026-05-26 12:22:11 -05:00
|
|
|
<section className="relative isolate overflow-hidden bg-primary-navy py-16 md:py-24 text-white">
|
|
|
|
|
<div className="absolute inset-0 -z-10">
|
|
|
|
|
<img
|
|
|
|
|
src={service.image || '/assets/hero-tech.webp'}
|
|
|
|
|
alt={serviceImageAlt[service.id] || `${service.name} service visual`}
|
|
|
|
|
className="h-full w-full object-cover object-center"
|
feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
|
|
|
loading="eager"
|
|
|
|
|
fetchPriority="high"
|
|
|
|
|
decoding="async"
|
2026-05-26 12:22:11 -05:00
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-primary-navy/84" />
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-r from-primary-navy via-primary-navy/92 to-primary-navy/50" />
|
|
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
2026-05-26 12:22:11 -05:00
|
|
|
<div className="max-w-3xl">
|
2026-05-18 13:12:18 -05:00
|
|
|
<div>
|
2026-05-26 12:22:11 -05:00
|
|
|
<Link to="/services" className="mb-5 inline-flex items-center gap-2 text-sm font-semibold text-primary-cyan hover:text-white">
|
|
|
|
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
2026-05-18 13:12:18 -05:00
|
|
|
Services
|
|
|
|
|
</Link>
|
2026-05-26 12:22:11 -05:00
|
|
|
<h1 className="text-4xl md:text-5xl font-bold mb-6">{service.name}</h1>
|
|
|
|
|
<p className="text-xl text-white/75 max-w-3xl leading-relaxed">{service.shortDesc}</p>
|
2026-05-18 13:12:18 -05:00
|
|
|
<div className="mt-8">
|
2026-05-27 22:33:54 -05:00
|
|
|
<Link to="/contact#contact-form" className="inline-flex h-11 items-center justify-center gap-2 rounded-md bg-white px-5 text-sm font-semibold text-primary-navy hover:bg-section-alt transition-colors">
|
2026-05-18 13:12:18 -05:00
|
|
|
Request This Service
|
|
|
|
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-17 17:01:08 -05:00
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
2026-05-26 12:22:11 -05:00
|
|
|
<section className="bg-background py-16 lg:py-24">
|
2026-05-17 17:11:29 -05:00
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
|
|
|
|
{/* Left Column - Main Content */}
|
|
|
|
|
<div className="lg:col-span-2">
|
|
|
|
|
<div className="mb-12">
|
|
|
|
|
<h2 className="text-2xl font-bold text-primary-navy mb-4">What This Solves</h2>
|
|
|
|
|
<p className="text-lg text-soft-text mb-6 leading-relaxed">
|
|
|
|
|
{service.fullDesc}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-17 17:11:29 -05:00
|
|
|
<div className="mb-12">
|
|
|
|
|
<h2 className="text-2xl font-bold text-primary-navy mb-4">Key Benefits</h2>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
{service.benefits.map((benefit, index) => (
|
2026-05-26 12:22:11 -05:00
|
|
|
<div key={index} className="flex items-start gap-3 rounded-md border border-border bg-white p-4 shadow-sm">
|
|
|
|
|
<CheckCircle2 className="h-5 w-5 text-primary-blue flex-shrink-0 mt-0.5" aria-hidden="true" />
|
|
|
|
|
<span className="text-sm font-medium text-text">{benefit}</span>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
|
|
|
|
))}
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-17 17:11:29 -05:00
|
|
|
<div className="mb-12">
|
|
|
|
|
<h2 className="text-2xl font-bold text-primary-navy mb-4">Ideal For</h2>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
{service.idealFor.map((item, index) => (
|
2026-05-26 12:22:11 -05:00
|
|
|
<div key={index} className="flex items-start gap-3 rounded-md border border-border bg-white p-4 shadow-sm">
|
|
|
|
|
<Zap className="h-5 w-5 text-teal-600 flex-shrink-0 mt-0.5" aria-hidden="true" />
|
|
|
|
|
<span className="text-sm font-medium text-text">{item}</span>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
|
|
|
|
))}
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
feat(seo): the industry and support pages now point at the two service pages
Both approved sheets ask for it: Unified Communications section 15 and Contact
Center section 18 each list the pages that should link back to them. Home, the
services index, the header and the footer already did. Managed Support and the
four industry pages did not, and the industry pages linked to no service at all.
Each industry page's own description already tells the reader Queue North
"delivers phone, contact center, network, and IT support" for that industry, so
all four qualify. The anchors are descriptive rather than the page names:
"Business phone and UCaaS solutions" and "Contact center and CCaaS solutions".
Rendered through the shared RelatedLinks component, in the industry sidebar card
and after "Ideal For" on a service page. It renders nothing without links, so
the five service pages that have none are untouched: only 5 of the 18 pages gain
a Related services block, and they are exactly the five the sheets name.
Verified against the build: both hrefs are present on managed-support and on all
four industry pages, the built-HTML audit passes with every link and fragment
resolving, and no other page gained the block.
Closes #221.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:59:49 -05:00
|
|
|
|
|
|
|
|
{/* Renders nothing for a service with no related links, so the
|
|
|
|
|
pages that have none are unchanged. */}
|
|
|
|
|
<RelatedLinks links={service.related} className="mb-12" />
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-17 17:11:29 -05:00
|
|
|
{/* Right Column - Sidebar */}
|
|
|
|
|
<div className="lg:col-span-1">
|
2026-05-26 12:22:11 -05:00
|
|
|
<Card className="lg:sticky top-24 border-t-[3px] border-t-primary-blue">
|
2026-05-17 17:11:29 -05:00
|
|
|
<CardHeader>
|
2026-05-26 12:22:11 -05:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<span className="flex h-10 w-10 items-center justify-center rounded-md bg-sky-50 text-primary-blue">
|
|
|
|
|
<Info className="h-5 w-5" aria-hidden="true" />
|
|
|
|
|
</span>
|
|
|
|
|
<CardTitle className="text-primary-navy text-xl">Quick Info</CardTitle>
|
|
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div>
|
2026-05-17 20:03:42 -05:00
|
|
|
<h3 className="font-semibold text-text mb-2">Service</h3>
|
2026-05-17 17:11:29 -05:00
|
|
|
<p className="text-soft-text">{service.name}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="pt-4 border-t border-border">
|
2026-05-27 22:33:54 -05:00
|
|
|
<Link to="/contact#contact-form" className="block w-full bg-primary-navy text-white px-4 py-3 rounded-md text-center font-medium hover:bg-primary-navy-dark transition-colors">
|
2026-05-17 17:11:29 -05:00
|
|
|
Request This Service
|
2026-05-17 17:42:03 -05:00
|
|
|
</Link>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
|
|
|
|
<div className="pt-2">
|
2026-05-17 17:42:03 -05:00
|
|
|
<Link to="/services" className="text-primary-navy hover:underline">
|
2026-05-17 17:11:29 -05:00
|
|
|
← Back to Services
|
2026-05-17 17:42:03 -05:00
|
|
|
</Link>
|
2026-05-17 17:11:29 -05:00
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
2026-05-17 17:11:29 -05:00
|
|
|
</section>
|
|
|
|
|
</>
|
2026-05-12 01:04:17 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ServiceDetail
|