Journal
9 min readSix homepage cards went nowhere.
Six service cards on our homepage hover-lifted like buttons and contained no link. What page-by-page review cannot see, and how to check for it.
- dead click targets
- rendered DOM audit
- internal link graph
- mobile scroll depth
Six service cards sat on our homepage. Each carried a card-interactive class, a border that
brightened on pointer enter, and a framer-motion lift of a few pixels. None of them contained a link.
A buyer read "Custom Websites · 4 to 8 weeks", watched the card rise under the cursor, tapped it, and stayed exactly where they were. The primary path from the homepage to every commercial page on the site did not exist. It only looked like it did, which is the worse failure — someone who sees no link goes looking, someone who taps a dead one decides the site is broken.
This is our own site. We found it, we shipped the fix, and the fix is not the interesting part.
What exactly was wrong with the cards?
ServiceCard imported service.slug and used it once, to select an icon from a map. No <Link>, no
href, no click handler anywhere in the component. The hover state was real, the affordance was real,
the destination was absent.
We verified three ways, because a single check is a guess:
- Source. The component rendered a motion
divwithwhileHoverand nothing that could receive a click. - Rendered DOM. On the live page, none of the six service names sat inside an
<a>. No client-side wrapper was adding one after hydration. - Behaviour. Hover lifted the card. Tap did nothing.
Then it compounded. The homepage built its grid from servicePreview = services.slice(0, 6), and we
publish seven services. The seventh — Performance, SEO & UX, our 1-to-2-week audit engagement — was in
no card, no menu, and no footer link. A visitor could read the entire homepage without learning it
existed. The service that exists to find problems on other people's sites was the one our homepage
quietly lost.
What remained were two routes to a service page. One "All services" text link at 36% scroll depth, and four footer links at 97%.
| Element on the mobile homepage | Scroll depth | Route toward an enquiry |
|---|---|---|
| Hero CTA | 3% | Direct |
| Six service cards | above 36% | None — no link present |
| "All services" text link | 36% | One link, easy to pass |
| Next conversion action in the document | 89% | Direct |
| Footer service links (four of seven) | 97% | Reached after the whole page |
Why did a page-by-page audit miss it?
Because we had already audited every page across six dimensions, and every page passed.
That is not an excuse, it is the finding. The homepage was well composed. The service pages were complete and internally consistent. Nothing on any single page was wrong. The defect was in the relationship between two pages that were each fine on their own.
Page-by-page review is a review of presence. It asks whether what is on the page is right — the copy, the hierarchy, the metadata, the proof. A missing edge in a site's link graph is not on any page. It has no element to fail and no line in a file to point at. Absence does not render.
Call the class connective absences. They share a shape: every component is correct, the wiring between components is not.
- A page nothing links to, reachable only by typing its URL.
- A card that reads as a control and is a
div. - A list preview with a hard-coded
slice()that silently drops items when the source list grows. - A long page whose only conversion actions sit at the two extremes of the scroll.
- A menu that covers four of seven items because it was written when there were four.
None of these are visible from inside a single page's review. All of them are visible to a pass that opens the rendered DOM, opens a mobile viewport, and asks one question: what connects to what.
| Question | Page-by-page review | Rendered-DOM and viewport pass |
|---|---|---|
| Is the copy on this page correct? | Yes | Poorly |
| Does this element actually link somewhere? | Rarely — it reads as a card either way | Yes, by DOM assertion |
| Is any published page unreachable? | No | Yes, from the link graph |
| Does a preview show everything it implies? | No | Yes, by count |
| How far apart are the conversion actions? | No | Yes, by measurement |
The two passes are not competing. One reviews the nodes, the other reviews the edges, and we had only ever run the first.
What did the mobile measurement show?
The homepage document measured 12,394px at a phone width. That is roughly fifteen screens.
The hero CTA sat at 3% depth. The next conversion action in the document sat at 89%. Between those two points a visitor scrolled through services, process, work, and testimonials — the entire persuasive middle of the page — with nothing clickable that moved them toward an enquiry. 86% of the mobile scroll carried no path forward.
That number is the reason the dead cards mattered so much. On a page with a conversion action every two screens, six unlinked cards are an annoyance. On a page where the cards were the only interactive thing between 3% and 89%, they were the funnel.
Desktop hides this. A three-across grid on a wide viewport compresses the same content into far less scrolling, so the gap between actions looks reasonable to whoever is building the page, which is almost always someone at a desk. [NEED: desktop document height for the same page]
How would you run this check on your own site?
Five steps. Four are a few lines in a console; the fifth is ninety seconds with the Tab key.
1. Build the link graph from rendered HTML. Fetch every route, parse the anchors, write the edges to a file. Compare that against your route manifest or sitemap. Any route with zero inbound edges is an orphan, and orphans are usually not the pages you would have guessed.
2. Assert that anything which looks interactive is interactive. Query the rendered DOM for elements
carrying your hover classes, then check each one resolves through closest('a, button'). In our case
the whole finding reduces to a one-line assertion over [class*="card-interactive"]. Any container with
a whileHover prop deserves the same check sitting next to it in review.
3. Tab through the page. This takes ninety seconds and needs no code. An element that cannot receive keyboard focus is not a control, whatever it looks like under a mouse. Nothing in the component could receive focus, which was the same defect wearing different clothes — with no control in the DOM, a screen reader had nothing to announce.
4. Measure the document, then map the gaps. Get document.body.scrollHeight at 390px wide, list
every conversion action with its offset, convert to percentages. Look for the longest stretch with
nothing in it. That stretch is your real conversion problem, and it will not appear in any per-page
report.
5. Count what your previews drop. Grep for slice( in list rendering. Each one is a number written
by a past version of your data. Either render everything, or derive the limit and show the remainder,
or write the truncation somewhere a person will read it.
The general principle underneath all five: review the site as a graph, not as a stack of pages. Design review, content review, and accessibility review are all node-shaped by default. Nobody owns the edges unless someone is assigned to them.
What did we change?
The card is now wrapped in a Link to /services/${service.slug}, with an aria-label naming the
service, and the motion element kept inside so the hover lift survives. servicePreview is now
services — all seven render. A mid-page CTA went into the mobile dead zone.
The diff is small. The pass that produced it was not, and that asymmetry is the whole post.
The Link wrapper went out last week. Nothing has had time to move in search results, and we have no post-change figures to report. We also have no clean baseline to compare against: analytics went in recently, so we cannot say what the click-through from the homepage to a service page was while the cards were dead. [NEED: pre-fix homepage-to-service click-through] Anyone claiming a lift from a change this recent is reading noise.
Did the same blindness show up anywhere else?
Yes, in the same pass, on the case studies.
Seven of eight case studies carried a section headed "Gallery — interface moments from the project". Each gallery contained the hero and cover images already shown higher on the same page. The section promised more of the work and delivered the same two pictures a second time.
Every one of those pages passed page-by-page review, and they should have — nothing in the section was broken, mislabelled, or ugly. The defect only appears when the section's promise is compared with what the page already showed, which is a relationship between two parts of a page rather than a property of either. Same class of miss, smaller stakes.
What is still unresolved?
The galleries, partly. We decided against the heading rather than the pictures: the page now drops any gallery entry that duplicates the hero or cover image, and hides the section when nothing survives. That leaves it rendering on one project. Sourcing real screenshots for the other seven is still open.
The check is still manual. We want the "looks interactive, is not interactive" assertion running in CI, and writing it without false positives is harder than it sounds — plenty of legitimate elements have hover states and no destination, including the cards on the About page. A rule that flags all of them is a rule the team will switch off within a week.
The scroll length itself. 12,394px is still fifteen screens, and a mid-page CTA is a patch on a page that may simply be too long. Whether the right fix is a better-placed button or a shorter homepage is an open question we intend to answer with data rather than taste. [NEED: mobile scroll-depth distribution for the homepage]
And the count of what else is missing. We found this by opening a mobile viewport and reading the DOM once. One afternoon, one class of defect. We have no reason to believe it was exhaustive, and we would rather say that than imply the site is now clean.