2026-05-12 01:04:17 -05:00
|
|
|
import { useState, useEffect } from 'react'
|
2026-05-13 22:07:35 -05:00
|
|
|
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from '@/components/ui/Sheet'
|
|
|
|
|
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
2026-05-17 14:33:41 -05:00
|
|
|
import { Link, useLocation } from 'react-router-dom'
|
2026-05-12 01:04:17 -05:00
|
|
|
|
|
|
|
|
const Header = () => {
|
|
|
|
|
const [isScrolled, setIsScrolled] = useState(false)
|
2026-05-13 22:07:35 -05:00
|
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
2026-05-26 13:04:57 -05:00
|
|
|
const [openDropdown, setOpenDropdown] = useState(null)
|
2026-05-17 14:33:41 -05:00
|
|
|
const location = useLocation()
|
2026-05-12 01:04:17 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
setIsScrolled(window.scrollY > 10)
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener('scroll', handleScroll)
|
|
|
|
|
return () => window.removeEventListener('scroll', handleScroll)
|
|
|
|
|
}, [])
|
|
|
|
|
|
2026-05-26 13:04:57 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
setOpenDropdown(null)
|
|
|
|
|
}, [location.pathname])
|
|
|
|
|
|
2026-05-12 01:04:17 -05:00
|
|
|
const navLinks = [
|
|
|
|
|
{ name: 'Home', href: '/' },
|
|
|
|
|
{ name: 'Services', href: '/services' },
|
|
|
|
|
{ name: 'Industries', href: '/industries' },
|
|
|
|
|
{ name: 'About', href: '/about' },
|
|
|
|
|
{ name: 'Contact', href: '/contact' },
|
|
|
|
|
{ name: 'Support', href: '/support' },
|
|
|
|
|
]
|
|
|
|
|
|
2026-05-13 22:07:35 -05:00
|
|
|
const serviceLinks = [
|
|
|
|
|
{ name: 'Unified Communications', href: '/services/unified-communications' },
|
|
|
|
|
{ name: 'Contact Center', href: '/services/contact-center' },
|
|
|
|
|
{ name: 'Managed Support', href: '/services/managed-support' },
|
|
|
|
|
{ name: 'Consulting & Training', href: '/services/consulting-training' },
|
|
|
|
|
{ name: 'Infrastructure Cabling', href: '/services/infrastructure-cabling' },
|
|
|
|
|
{ name: 'Wireless Access', href: '/services/wireless-access' },
|
|
|
|
|
{ name: 'Local Networking', href: '/services/local-networking' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const industryLinks = [
|
|
|
|
|
{ name: 'Healthcare', href: '/industries/healthcare' },
|
|
|
|
|
{ name: 'Retail', href: '/industries/retail' },
|
|
|
|
|
{ name: 'Manufacturing', href: '/industries/manufacturing' },
|
|
|
|
|
{ name: 'Education & Finance', href: '/industries/education-finance' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const closeMobileMenu = () => setMobileMenuOpen(false)
|
2026-05-26 13:04:57 -05:00
|
|
|
const closeDropdown = () => setOpenDropdown(null)
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-17 14:33:41 -05:00
|
|
|
const isActive = (href) => location.pathname === href
|
|
|
|
|
|
2026-05-13 22:07:35 -05:00
|
|
|
return (
|
|
|
|
|
<header className={`sticky top-0 z-40 w-full transition-all duration-300 ${isScrolled ? 'bg-primary-navy shadow-md' : 'bg-primary-navy/95'}`}>
|
|
|
|
|
<div className="container mx-auto px-4">
|
2026-05-25 19:44:10 -05:00
|
|
|
<div className="flex h-16 md:h-18 items-center justify-between">
|
2026-05-13 22:07:35 -05:00
|
|
|
{/* Logo */}
|
|
|
|
|
<div className="flex items-center gap-3">
|
2026-05-17 22:47:03 -05:00
|
|
|
<Link to="/" className="flex items-center gap-3" aria-label="Queue North Technologies Home">
|
2026-05-18 09:44:44 -05:00
|
|
|
<img
|
|
|
|
|
src="/logo.png"
|
|
|
|
|
alt="Queue North Technologies"
|
2026-05-26 12:44:10 -05:00
|
|
|
className="brand-logo-on-dark h-12 md:h-16 w-auto flex-shrink-0"
|
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"
|
|
|
|
|
decoding="async"
|
|
|
|
|
width="200"
|
|
|
|
|
height="200"
|
2026-05-17 14:33:41 -05:00
|
|
|
/>
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
<span className="font-bold text-sm sm:text-xl xl:text-2xl text-white whitespace-nowrap tracking-tight">Queue North Technologies</span>
|
2026-05-17 14:33:41 -05:00
|
|
|
</Link>
|
2026-05-13 22:07:35 -05:00
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-13 22:07:35 -05:00
|
|
|
{/* Desktop Nav */}
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
{/*
|
|
|
|
|
The desktop row starts at lg, not md. Tightening the gaps at md (#214)
|
|
|
|
|
did not fix iPad portrait, it only hid the overflow better: measured at
|
|
|
|
|
a true 768px the three items want 787px of their natural width against
|
|
|
|
|
736px of container, so flex shrank the CTA to 114px, wrapped "Request
|
|
|
|
|
Consultation" inside it, and still pushed it 25px past the viewport,
|
|
|
|
|
where body{overflow-x:hidden} sliced it off with no scrollbar. That
|
|
|
|
|
gap-4 was verified in a desktop window whose scrollbar left ~753px, so
|
|
|
|
|
md never engaged and the fix was never actually exercised.
|
|
|
|
|
|
|
|
|
|
768 to 1023 gets the menu button instead, which is the better tablet
|
|
|
|
|
experience anyway: 44px rows rather than 17px ones, and the Services
|
|
|
|
|
and Industries submenus are reachable, where the desktop dropdowns
|
|
|
|
|
open on hover and a touch device has no hover.
|
|
|
|
|
|
|
|
|
|
The wordmark stays at text-xl until xl. At exactly 1024 the row was
|
|
|
|
|
988px inside 992px of container: four pixels, which a font fallback
|
|
|
|
|
or one more nav item would eat. At text-xl it has room.
|
|
|
|
|
*/}
|
|
|
|
|
<nav className="hidden lg:flex items-center gap-5 xl:gap-6" aria-label="Main navigation">
|
2026-05-26 13:04:57 -05:00
|
|
|
{navLinks.map((link) => {
|
|
|
|
|
const hasDropdown = link.name === 'Services' || link.name === 'Industries'
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={link.name}
|
|
|
|
|
className="relative"
|
|
|
|
|
onMouseEnter={() => hasDropdown && setOpenDropdown(link.name)}
|
|
|
|
|
onMouseLeave={closeDropdown}
|
|
|
|
|
onBlur={(event) => {
|
|
|
|
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
|
|
|
closeDropdown()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
closeDropdown()
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-05-17 21:44:48 -05:00
|
|
|
>
|
2026-05-26 13:04:57 -05:00
|
|
|
<Link
|
|
|
|
|
to={link.href}
|
|
|
|
|
onFocus={() => hasDropdown && setOpenDropdown(link.name)}
|
|
|
|
|
onClick={closeDropdown}
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
className={`block py-3 text-sm font-medium transition-colors ${isActive(link.href) ? 'text-white underline underline-offset-4' : 'text-white/70 hover:text-white'}`}
|
2026-05-26 13:04:57 -05:00
|
|
|
>
|
|
|
|
|
{link.name}
|
|
|
|
|
</Link>
|
|
|
|
|
{/* Services Dropdown */}
|
|
|
|
|
{link.name === 'Services' && (
|
|
|
|
|
<div className={`absolute top-full left-0 w-64 bg-white rounded-md shadow-xl border border-gray-200 pt-2 ${openDropdown === 'Services' ? 'block' : 'hidden'}`}>
|
|
|
|
|
<div className="p-2">
|
|
|
|
|
{serviceLinks.map((service) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={service.name}
|
|
|
|
|
to={service.href}
|
|
|
|
|
onClick={closeDropdown}
|
|
|
|
|
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{service.name}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-05-17 21:44:48 -05:00
|
|
|
</div>
|
2026-05-26 13:04:57 -05:00
|
|
|
)}
|
|
|
|
|
{/* Industries Dropdown */}
|
|
|
|
|
{link.name === 'Industries' && (
|
|
|
|
|
<div className={`absolute top-full left-0 w-64 bg-white rounded-md shadow-xl border border-gray-200 pt-2 ${openDropdown === 'Industries' ? 'block' : 'hidden'}`}>
|
|
|
|
|
<div className="p-2">
|
|
|
|
|
{industryLinks.map((industry) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={industry.name}
|
|
|
|
|
to={industry.href}
|
|
|
|
|
onClick={closeDropdown}
|
|
|
|
|
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{industry.name}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-05-17 21:44:48 -05:00
|
|
|
</div>
|
2026-05-26 13:04:57 -05:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2026-05-13 22:07:35 -05:00
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{/* CTA Button */}
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
{/* shrink-0 and whitespace-nowrap so a row that no longer fits overflows
|
|
|
|
|
visibly and the device sweep catches it, instead of flex quietly
|
|
|
|
|
squeezing the button and wrapping its label to keep the total inside
|
|
|
|
|
a viewport it is already past. */}
|
|
|
|
|
<div className="hidden lg:block shrink-0">
|
|
|
|
|
<Link to="/contact#contact-form" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-9 px-3 bg-primary-cyan text-primary-navy hover:bg-cyan-600 transition-colors">
|
2026-05-13 22:07:35 -05:00
|
|
|
Request Consultation
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Mobile Menu */}
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
<div className="lg:hidden">
|
2026-05-13 22:07:35 -05:00
|
|
|
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
|
|
|
|
<SheetTrigger asChild>
|
fix(ui): header, Cisco logo and tap targets, found by rendering on devices
Every page on ten emulated phones and tablets: 190 loads, 21 blocking and 530
high findings, on pages that had passed Batch 11 and Batch 16.
#214 was fixed in v0.9.5 and released still broken. The check was a desktop
window sized to 768, which has a scrollbar, so the layout viewport was ~753px
and the md breakpoint the fix was about never engaged. At a true 768 the brand,
six nav links and the CTA want 787px against 736px of container, so no amount of
gap tightening closes it. The desktop row now starts at lg, and 768 to 1023 gets
the Sheet menu, which is the better tablet experience regardless: 44px rows
instead of 17px ones, and submenus that do not depend on hover. The CTA takes
shrink-0 and whitespace-nowrap, so the next row that does not fit overflows
visibly instead of being squeezed past the edge while the total still measures
as fitting.
The Cisco mark fills 66% of its 700x700 canvas, so both tiles scaled it up
inside overflow-hidden and cut 13px off the trademark on / and 24px on /about.
Cropping the viewBox to the artwork renders it at 62x46 on /, the same size as
the 8x8 logo beside it, unscaled and unclipped.
491 standalone links were 17 to 26px tall. .tap-target grows the hit box and
takes it back out of the layout, and every list using it moves to space-y-4:
at space-y-2 neighbouring boxes overlap and measure 33px while not being there.
Footer links also take block, so the row is the target. A 39x36 word is judged
as a compact target wanting 44px; a 200x36 row wants 32.
scripts/device-sweep.mjs and scripts/lib/css-audit.js are the instrument. They
stay out of verify.sh: playwright is global here and the sweep needs a running
server, and a guard that cannot run on a clean clone is one that gets skipped.
After the fixes: zero blocking, zero high across all ten devices. Each defect
was then re-introduced and reported again (clipped x1, media_overflow x1,
past_viewport x19, touch_target x342).
closes #238
closes #239
closes #240
closes #241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 07:09:32 -05:00
|
|
|
{/* p-2.5, so the 24px icon sits in a 44x44 target. It is now the
|
|
|
|
|
only navigation up to 1023px, and 40x40 was under the mark. */}
|
|
|
|
|
<button className="p-2.5 text-white hover:text-primary-cyan transition-colors focus:outline-none focus:ring-2 focus:ring-primary-cyan rounded-md" aria-label="Open navigation menu">
|
2026-05-13 22:07:35 -05:00
|
|
|
<span className="sr-only">Open menu</span>
|
|
|
|
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</SheetTrigger>
|
2026-05-27 14:11:48 -05:00
|
|
|
<SheetContent side="right" aria-describedby={undefined} className="w-[85vw] max-w-[300px] sm:w-[350px] bg-primary-navy text-white">
|
2026-05-13 22:07:35 -05:00
|
|
|
<VisuallyHidden.Root asChild>
|
|
|
|
|
<SheetTitle>Navigation Menu</SheetTitle>
|
|
|
|
|
</VisuallyHidden.Root>
|
|
|
|
|
|
2026-05-27 21:49:21 -05:00
|
|
|
{/* Logo + phone */}
|
|
|
|
|
<div className="flex items-center gap-3 pb-4 border-b border-white/10">
|
|
|
|
|
<Link to="/" onClick={closeMobileMenu} aria-label="Queue North Technologies Home" className="flex-shrink-0">
|
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
|
|
|
<img src="/logo.png" alt="Queue North Technologies" className="brand-logo-on-dark h-12 w-auto"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
decoding="async"
|
|
|
|
|
width="200"
|
|
|
|
|
height="200"
|
|
|
|
|
/>
|
2026-05-18 12:11:56 -05:00
|
|
|
</Link>
|
2026-05-27 21:54:44 -05:00
|
|
|
<Link to="/" onClick={closeMobileMenu} className="font-bold text-sm tracking-tight whitespace-nowrap text-white leading-tight">
|
|
|
|
|
Queue North Technologies
|
|
|
|
|
</Link>
|
2026-05-18 12:11:56 -05:00
|
|
|
</div>
|
2026-05-13 22:07:35 -05:00
|
|
|
|
2026-05-18 12:11:56 -05:00
|
|
|
{/* Scrollable nav */}
|
|
|
|
|
<nav className="flex-1 min-h-0 overflow-y-auto" aria-label="Mobile navigation">
|
|
|
|
|
{/* Main links — Services and Industries handled as sections below */}
|
|
|
|
|
<ul className="space-y-1 mb-6">
|
|
|
|
|
{navLinks
|
|
|
|
|
.filter(link => link.name !== 'Services' && link.name !== 'Industries')
|
|
|
|
|
.map((link) => (
|
|
|
|
|
<li key={link.name}>
|
|
|
|
|
<Link
|
|
|
|
|
to={link.href}
|
|
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className={`flex items-center py-3 px-2 rounded-md text-base font-medium transition-colors ${isActive(link.href) ? 'text-white bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
|
|
|
|
|
>
|
|
|
|
|
{link.name}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
{/* Services section */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<Link
|
|
|
|
|
to="/services"
|
|
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className={`block py-2 px-2 text-xs font-semibold uppercase tracking-wider mb-1 transition-colors ${isActive('/services') ? 'text-primary-cyan' : 'text-primary-cyan/80 hover:text-primary-cyan'}`}
|
|
|
|
|
>
|
|
|
|
|
Services
|
|
|
|
|
</Link>
|
|
|
|
|
<ul className="space-y-1">
|
|
|
|
|
{serviceLinks.map((service) => (
|
|
|
|
|
<li key={service.name}>
|
|
|
|
|
<Link
|
|
|
|
|
to={service.href}
|
|
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className={`flex items-center py-3 px-4 rounded-md text-sm transition-colors ${isActive(service.href) ? 'text-white font-semibold bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
|
|
|
|
|
aria-label={service.name}
|
|
|
|
|
>
|
|
|
|
|
{service.name}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
2026-05-12 01:04:17 -05:00
|
|
|
|
2026-05-18 12:11:56 -05:00
|
|
|
{/* Industries section */}
|
|
|
|
|
<div>
|
|
|
|
|
<Link
|
|
|
|
|
to="/industries"
|
|
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className={`block py-2 px-2 text-xs font-semibold uppercase tracking-wider mb-1 transition-colors ${isActive('/industries') ? 'text-primary-cyan' : 'text-primary-cyan/80 hover:text-primary-cyan'}`}
|
|
|
|
|
>
|
|
|
|
|
Industries
|
2026-05-13 22:07:35 -05:00
|
|
|
</Link>
|
2026-05-18 12:11:56 -05:00
|
|
|
<ul className="space-y-1">
|
|
|
|
|
{industryLinks.map((industry) => (
|
|
|
|
|
<li key={industry.name}>
|
|
|
|
|
<Link
|
|
|
|
|
to={industry.href}
|
|
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className={`flex items-center py-3 px-4 rounded-md text-sm transition-colors ${isActive(industry.href) ? 'text-white font-semibold bg-white/10' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
|
|
|
|
|
aria-label={industry.name}
|
|
|
|
|
>
|
|
|
|
|
{industry.name}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
2026-05-13 22:07:35 -05:00
|
|
|
</div>
|
2026-05-18 12:11:56 -05:00
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{/* CTA */}
|
|
|
|
|
<div className="pt-4 border-t border-white/10">
|
|
|
|
|
<Link
|
2026-05-27 22:33:54 -05:00
|
|
|
to="/contact#contact-form"
|
2026-05-18 12:11:56 -05:00
|
|
|
onClick={closeMobileMenu}
|
|
|
|
|
className="inline-flex items-center justify-center gap-2 w-full rounded-md text-sm font-semibold h-11 px-4 bg-primary-cyan text-primary-navy hover:bg-white transition-colors duration-200"
|
|
|
|
|
aria-label="Get a free quote"
|
|
|
|
|
>
|
|
|
|
|
Get a Free Quote
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
|
|
|
|
</svg>
|
|
|
|
|
</Link>
|
2026-05-13 22:07:35 -05:00
|
|
|
</div>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
2026-05-12 01:04:17 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-13 22:07:35 -05:00
|
|
|
</div>
|
|
|
|
|
</header>
|
2026-05-12 01:04:17 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 14:33:41 -05:00
|
|
|
export default Header
|