Skip to content

Tech Spec

This page is written for the engineer doing the onboarding. It explains what you add to a page, what happens in the browser after you add it, and how to confirm it worked.

If you only need the copy-paste snippet, see Install on Any Website. If you want the endpoint-level detail, jump to the API reference.

1. What you add to the page

One tag, in the <head>, deferred:

<head>
  <title>My title</title>
  <meta name="description" content="My description">
  <script data-domain="website.com" src="https://cdn.mentionlink.com/script.min.js" defer></script>
</head>
  • data-domain must be your domain, exactly as registered with us. Leaving the website.com placeholder in is the single most common onboarding mistake and the API rejects it with a 400.
  • defer matters. It lets the browser download the script in parallel with everything else, but guarantees execution happens after the server-rendered HTML is parsed. Mentionlink never blocks rendering and never makes a visitor wait on our computation.
  • The script runs entirely in the visitor's browser, after the page has loaded.

Nothing else is required on your side. There is no build step, no npm package to install on your site, and no server-side integration.

2. How the script is built and served

We write the script in TypeScript for type-safety and compile it down to a JavaScript baseline supported by every browser released since 2020 — roughly 95% of worldwide Internet traffic. If you know you have a meaningful audience on older browsers, tell us and we're happy to extend support.

It ships from a geodistributed CDN, minified and compressed. Uncompressed the bundle is around 100 KB; Zstandard-compressed it's around 35 KB, and we also serve Gzip and Brotli depending on what the browser accepts, so an uncompressed bundle is almost never sent. Most of that weight is our error-monitoring dependency, which we consider worth carrying.

Unhandled JavaScript exceptions are reported to real-time error monitoring, so our engineering team is notified immediately rather than finding out from you. We assess impact, and where warranted deploy a hotfixed script to the CDN and purge the cache so every site picks it up at once.

The backend runs on edge workers, so it scales horizontally and answers from a location close to the visitor.

3. Before the page will do anything

The script is only half of the integration. A page produces mentions when all of the following are true:

  1. The tenant exists. Your domain is registered on our side; otherwise the API answers 404 tenant not found.
  2. The URL is allowed. Each tenant has a URL allowlist/blocklist so you can roll out section by section — for example /recipes/* only, minus /recipes/vegan/*. A blocked URL gets a 403 and the script stops silently. See Configuration and Customization.
  3. An affiliate program is connected. See Amazon and Walmart.
  4. The page has been processed at least once. The very first view of a new URL is queued, not answered — see step 4.5.

4. What happens in the browser, step by step

4.1 Boot

Because of defer, the script executes before DOMContentLoaded fires. It then:

  • Guards against double execution. If a second copy is on the page (a duplicated tag, a tag manager, a plugin) it reports the problem and bails out rather than double-linking the page.
  • Reads its configuration from the data-domain attribute on its own tag, plus optional runtime flags (see Debug flags).
  • Injects our stylesheet (theme.min.css) once, into <head>.
  • Starts the two passive beacons described in step 7.
  • Schedules the main run.

4.2 When the main run starts

Ideally we'd wait for the page's load event. In practice load is often never fired, because some other third-party stylesheet, image, or script on the page hangs. So the script races two triggers and takes whichever comes first:

  • load fires, or
  • up to three seconds have elapsed since DOMContentLoaded.

This is deliberate defensive design: Mentionlink works around other misbehaving scripts on your page rather than depending on them.

4.3 The configuration handshake

The first network call is a HEAD request to /v2/pageview, carrying the script version, your domain, and the page URL.

The response has no body. Everything comes back as x-mentionlink-* response headers, and the payload-carrying ones are Base64-encoded because HTTP headers can't safely carry arbitrary text. They tell the script:

Header What it controls
…-substrings-selector-base64 A CSS selector limiting where in the page text may be linked. Defaults to everything.
…-images-selector-base64 A CSS selector for which images are eligible for captioning. Defaults to img[data-mentionlink-image].
…-images-prehash-pattern-base64 / …-images-prehash-replacement-base64 A regex rewrite applied to image URLs before hashing, so that a CDN-resized variant and the original resolve to the same image.
…-cards-selector-base64 A CSS selector for the elements product cards may be inserted after (the article body, a recipe's lists, and so on). Defaults to p, ol, ul, figcaption.
…-custom-css-base64 / …-custom-js-base64 Per-tenant styling and behavior tweaks.
…-custom-params-base64 Extra parameters forwarded to the card renderer.
…-open-in-new-tab, …-show-cards, …-show-hotspots Feature toggles for this tenant.
…-use-sdk Which endpoint captions the photos of this tenant: true sends them along with the page, false sends each of them to /v2/imageview on its own.

This is the mechanism that lets us tune a site — restrict linking to the article body, exclude a sidebar, fix an image-URL quirk — without you shipping any code. When onboarding a new template, this is usually the knob that gets turned.

4.4 Extracting the page

Next the script builds the payload it will send us. Two pieces:

A simplified HTML rendering of the page. It walks the DOM and produces a flattened, whitespace-normalized string that keeps only what is semantically useful for matching:

  • Kept and marked: <a>, <h1><h6>, <ul>, <ol>, and images (as a hash placeholder).
  • Dropped entirely: <script>, <style>, <nav>, <form>, <iframe>, <button>, <input>, <select>, <textarea>, <noscript>, and friends.
  • Dropped by attribute: anything under data-mentionlink-ignore (your explicit opt-out) or aria-live (live regions — they change under us and are usually not content).

An image inventory. For every image matching the images selector and not excluded, the script resolves the effective URL, applies the prehash rewrite, computes a short non-cryptographic hash of it, and stamps that hash onto the element as data-mentionlink-hash. That stamp is the join key: when a caption or hotspot comes back later, it is addressed by hash, not by DOM position — so it lands on the right image even if the DOM has moved in the meantime.

Where the inventory goes depends on …-use-sdk. With it on, the photos travel inside the page payload and are captioned along with it. With it off — the default — the page payload carries an empty inventory and every photo is POSTed to /v2/imageview separately once the page's own response has been applied, a few at a time. Either way the hashes are stamped the same, so captions land on the same images.

We send text and image URLs. We do not send cookies, form contents, or anything from ignored regions.

4.5 The POST, and the queue

The extracted payload is POSTed to /v2/pageview. There are three outcomes:

  • 200 OK — we already have results for this page. The response is a stream (see step 5).
  • 202 Accepted — first time we've seen this page. We accept it, queue it, and answer immediately. A retry-after header tells the script how long to wait.
  • 429 Too Many Requests — an identical request is already queued (another visitor, another tab, the script's own poll). Same retry-after contract.

On 202/429 the script polls until the cache is warm, pacing itself by retry-after (clamped to sane bounds, roughly 1–30s, defaulting to 10s), with a hard budget of about a dozen attempts or two minutes — whichever runs out first. If the budget runs out, the script simply stops; the work still completes on our queues, and the next visitor to that page gets the results instantly.

403 and 404 are terminal — the script stops without retrying.

Note

This is why a brand-new page looks "empty" on the very first load, and why load-testing the endpoint mostly reports non-2xx responses. Both are expected. See Troubleshooting Errors.

5. Hydration: how results land in the DOM

A 200 response is JSON Lines over HTTP streaming: one JSON object per line, text/plain, sent as each result comes off the wire rather than buffered until complete. The script reads the stream incrementally and applies each line the moment it arrives, so links start appearing before the response has finished.

Each line has a type. The three that matter:

A mention is a substring plus a destination url (and, when we have one, catalog/item identifiers used for the cards).

To apply it, the script walks text nodes under the configured root and:

  • Matches case-insensitively, at word boundaries — so "iPad mini" doesn't match inside a longer word.
  • Refuses to touch text inside <a>, <h1><h6>, <nav>, or any ignored/live region. We never re-link something you already linked, and we never link a headline.
  • Resolves overlaps in favor of the longer match: if "Nintendo Switch" was already linked and "Nintendo Switch 2" arrives, the shorter link is flattened back to text first, then the longer one is applied.
  • Validates the destination is http:/https: before writing it into the DOM.
  • Wraps the matched range in <a class="mentionlink" rel="sponsored nofollow noopener">, optionally with target="_blank" (plus an aria-description so screen readers announce it) when the tenant has new-tab links enabled.

The result is a normal anchor in your DOM. Your CSS applies to it; our stylesheet only adds the Mentionlink-specific bits.

vision — hydrating an image into a figure with hotspots

A vision is addressed by the image hash from step 4.4 and carries a caption plus zero or more hotspots.

The figure. If the image already lives in a <figure>, we reuse it. Otherwise we wrap the image in a <figure> and append a <figcaption> with the caption text. This is why captions survive your existing markup instead of fighting it.

The hotspots. Each hotspot is a shape (currently rectangles), a set of coordinates, a destination URL, and a label. Two things are built per image:

  • A real HTML image map — a <map> with one <area> per hotspot — which is what makes the region itself clickable.
  • An overlay anchor per hotspot, absolutely positioned inside the figure, carrying the visible label.

Coordinates arrive normalized as fractions of the image's dimensions, not pixels. The script scales them to the rendered size on insert, and re-runs that layout via a ResizeObserver (batched into an animation frame), so hotspots stay correct through responsive reflow, lazy image sizing, and carousel transitions.

Because labels can collide on a small image, the layout pass runs a collision check and degrades gracefully: a label that fits stays expanded, one that doesn't collapses to a dot, and one that still doesn't fit is hidden. Hotspots are laid out smallest-area-first so small targets aren't buried under large ones.

Finally, hotspots and text mentions are linked: hovering or focusing a text mention highlights the hotspots pointing at the same product (hover is bound only on non-touch devices, so a tap on mobile doesn't get swallowed as a hover).

override — tenant-level corrections

Overrides let us add a link we know is right, or suppress one we know is wrong, for a given substring. Additions are applied like mentions; removals suppress any matching mention that arrives later on the same stream. This is the escape hatch used during onboarding when you spot a bad match — you tell us, we override, no redeploy on your side.

Product cards ("hopscotch")

When the tenant has cards enabled, mentions that carry catalog/item identifiers are also collected into a horizontally scrollable strip of product cards inserted after the elements the cards selector allows (<p>, <ol>, <ul>, and <figcaption> by default). Each card is a lazy-loaded image rendered by our card endpoint, so the card artwork costs the page one cached image request and no client-side rendering work.

Cards are collected in document order, and each one joins the first allowed element that either contains its mention or follows it in the page — so everything accumulated since the previous match is flushed together, and a single card is enough for a strip. Allowed elements that aren't actually visible (a collapsed <details>, display: none, and so on) are skipped, as are mentions the reader can't see.

6. Single-page apps and galleries

Three integration points exist for SPAs, slideshows, and infinite-scroll templates:

  • mentionlinkchanged — dispatch this event on document after a client-side navigation and the script re-runs against the new content, flagged as an interaction rather than a fresh page load.
  • window.getMentionlinkURL() — define this and return the canonical URL the current view should be attributed to. Return null to fall back to document.location.href. Useful when your router puts state in the URL that isn't part of the page's identity.
  • window.mustStopMentionlink() — define this and return true to abort an in-flight run. Useful when the user has navigated away mid-stream.

For image galleries, the script also reads schema.org JSON-LD: a page typed ImageGallery with a mainEntityOfPage reports the gallery URL alongside the slide URL, so per-slide URLs in a slideshow roll up to the gallery rather than fragmenting into hundreds of separate pages.

Three events are dispatched on document for testing and coordination: mentionlinkstarted, mentionlinkfinished, and mentionlinkvisioned.

7. What we measure

Two passive beacons, both sent with navigator.sendBeacon so they never block or delay navigation:

  • /v1/pagedwell — scroll depth (as a normalized viewport rectangle) and dwell time, sampled on scroll and on tab visibility changes. This is what tells us whether a page is actually being read.
  • /v1/linkclick — fired on mousedown for a mention link, hotspot, or product card, with the click position normalized against the document. This is what tells us clickthrough rate per element type.

8. SEO and crawlers

Because everything happens client-side after load, what a crawler sees depends on how it crawls:

  1. Plain HTTP requests — Mentionlink links are not seen. The script only runs inside a browser, after the page has loaded.
  2. Browser-based requests — the script runs and the links are seen. Every link we add carries rel="sponsored nofollow noopener", so it is correctly declared as a paid link and passes no ranking signal.

Your own server-rendered HTML is never modified. Nothing we do changes what a plain-HTTP crawler indexes.

9. Debug flags for QA

Append a hash fragment to any page carrying the script:

Fragment Effect
#mentionlink=debug Verbose console.debug output: resolved configuration, image hashes, poll attempts, matched text nodes.
#mentionlink=showhotspots Force image hotspots on, regardless of the tenant toggle.
#mentionlink=showcards Force product cards on, regardless of the tenant toggle.
#mentionlink=demo Animate links and captions as they're applied — useful for screen recordings and demos.

Additional flags exist for internal testing; ask us if you need one for a specific scenario.

10. Rolling out a site or a section

A typical first 30 days looks like this:

  1. We read your sitemap and pull the full list of pages.
  2. We queue them as a backlog, processed in the background rather than on visitor traffic. Any page we haven't reached yet is processed on its first visit instead, and newly published pages always are.
  3. We roll out progressively, by URL pattern. Blocking or allowing a section is a configuration change on our side, not a deploy on yours — which is what makes it safe to start with one template and widen from there.

Then we confirm the numbers, in this order:

  1. Page dwell — are these pages actually being read?
  2. Clickthrough rate — are the links being used?
  3. Conversion rate — are the clicks converting?
  4. Extrapolation — what do those rates imply once applied to site-wide traffic?

11. Onboarding checklist for a new page or template

  1. Tag present and correct. View source, confirm exactly one Mentionlink <script> tag, with defer and the right data-domain.
  2. Console. Reload with DevTools open. The script logs its name and version on start. No banner means the tag never executed.
  3. Network — HEAD /v2/pageview. Confirm it returns configuration headers. A 404 here means the domain isn't registered; a 403 means this URL is excluded by config.
  4. Network — POST /v2/pageview. A 202 on a page we've never seen is correct. Wait, reload, and confirm you get a 200 with a streaming body.
  5. Mentions land. Confirm links appear in the article body, and confirm they do not appear in navigation, headlines, related-post rails, or comments. If they do, that's a selector or an ignore-attribute fix — send us the page.
  6. Images. If visioning is enabled for the tenant, confirm images pick up a data-mentionlink-hash attribute and, once processed, a <figure>/<figcaption>. If the same image appears under many CDN URLs, tell us — that's what the prehash rewrite is for.
  7. Hotspots. With #mentionlink=showhotspots, resize the window and confirm hotspots track the image and degrade cleanly instead of overlapping.
  8. Exclusions. Wrap anything Mentionlink should never touch in data-mentionlink-ignore:

    <div data-mentionlink-ignore>
      <p>Text you don't want Mentionlink to hyperlink.</p>
    </div>
    
  9. Layout. Confirm captions, cards, and hotspots don't break your grid. Per-tenant CSS is available if they do.

API reference

The API is documented with an OpenAPI 3.1 specification, browsable as Swagger UI:

The endpoints referenced above:

Endpoint Purpose
HEAD /v2/pageview Configuration handshake. Returns x-mentionlink-* headers, no body.
POST /v2/pageview Submit a page; streams back overrides, mentions, and visions as JSON Lines, or queues the page (202/429 with retry-after).
POST /v2/imageview Same contract for a single image, which is how photos are processed unless the handshake reports …-use-sdk. Configuration comes from the /v2/pageview handshake.
GET /v1/cardview/{format}/{catalog}/{item}/{substring} Renders a product card image.
POST /v1/pagedwell Scroll-depth and dwell beacon.
POST /v1/linkclick Click beacon.

For load-testing guidance against these endpoints, see Load Testing.

Appendix: why a revenue sharing model?

Mentionlink is a service, not a tool, and the pricing model follows from that. We are continuously improving the quality of our product matching along two axes:

  • Precision — how well we match a plain-text product mention to the best-matching product on the affiliate marketplace.
  • Recall — how completely we identify all the product mentions on a page.

Precision comes first, because it correlates with conversion rate: a wrong match is worse than no match. Recall comes second, because it correlates with revenue — every mention we miss is an opportunity a reader never got.

Under a flat fee, our incentive would be to cut costs, which in practice means cheaper matching and worse precision. Under revenue sharing, the only way we earn more is to match better, so we can justify spending on state-of-the-art models where they measurably improve the outcome. Your incentives and ours point the same direction.

Getting help

Send us the URL, a screenshot, and the console/network output from the checklist above — that's almost always enough for us to turn the right knob on our side.

Email support@mentionlink.com.