# Marketing site examples

> Concrete ClickClacks examples for a marketing site: Book a demo clicks and bookings, FAQ opens per question, the pricing toggle and plan buttons, outbound, phone and email links, video plays, scroll depth and sections, hero versus footer CTAs, rage clicks and 404 pages.

- Canonical URL: https://clickclacks.io/docs/examples/marketing-site
- Section: Examples
- Last updated: 2026-09-26

The questions people ask about acme.com: who clicks Book a demo, which FAQs get read, how far people scroll. For each one: what autocapture already answers, the custom event that answers it exactly, and where to read it in the app.

Every snippet assumes the [script tag](https://clickclacks.io/docs/install.md) is on the page. Put it, and the [queue stub](https://clickclacks.io/docs/events.md#before-load), in the `<head>`. Browser custom events carry the page automatically, so every one can be filtered and broken down by **Page**. The `data-…` attributes in the HTML are for your own script to read. ClickClacks doesn’t read them.

## Who clicks “Book a demo”, on which page, and do they book? {#book-demo}

### Fastest: autocapture {#book-demo-fast}

If “Book a demo” is an `<a>` or a `<button>`, every click already arrives as `$click` with its label as **Clicked text** (up to 40 characters) and the page as **Page**. Nothing to add.

### Precise: a custom event {#book-demo-precise}

Labels change when someone rewrites the copy, and two buttons with the same label look alike. A custom event with a `placement` property survives both:

```html title="index.html"
<a href="/demo" class="book-demo" data-placement="hero">Book a demo</a>
<!-- … further down the page … -->
<a href="/demo" class="book-demo" data-placement="footer">Book a demo</a>

<script>
  document.querySelectorAll('.book-demo').forEach(function (button) {
    button.addEventListener('click', function () {
      window.clickclacks?.('event', 'Demo booking started', {
        placement: button.dataset.placement,
      })
    })
  })
</script>
```

For the booking itself, the simplest signal is a page. Most scheduling tools can send people to a page on your site after they book, such as `/demo/booked`, and its `$pageview` is your “booked” event. Scheduling widgets that open in an iframe are a separate page, so clicks inside them never reach your tracker.

### Read the answer {#book-demo-read}

Clicks by page, in [Insights](https://clickclacks.io/docs/guides/insights.md):

1. Click **New insight**. Set series A to `$click` under **Autocaptured** (or to `Demo booking started`), counted as **Unique people**.
2. For `$click`, click **Where** and add **Clicked text** _contains_ `book a demo`. _contains_ ignores case and any arrow or icon text.
3. Click **Add breakdown** › **Event property** › **Page**, **Top 10**. For the custom event, break down by **Placement** instead.
4. Switch the chart to **Bar** to rank pages over the whole range. Save it as _Book a demo clicks_.

Clicks to bookings, in [Funnels](https://clickclacks.io/docs/guides/funnels.md):

1. Click **New funnel**. Step 1: `$click`, then **+ Filter** › **Clicked text** _contains_ `book a demo`.
2. Step 2: `$pageview`, **+ Filter** › **Page** _is_ `/demo/booked`.
3. Set **Conversion window** to **7 days** and save it as _Demo clicks to bookings_.
4. Choose **Break down by** › **Page**. A funnel breakdown reads step 1, so this is the page the button was clicked on.

> **What you should see**
>
> A bar per page, such as `/pricing`, `/` and `/features`, and in the funnel a conversion rate for each. Page never includes the query string, so `/pricing?utm_source=newsletter` counts as `/pricing`. The funnel charts the top 3 pages with at least 500 people entering, and lists the rest in the table.

## Which FAQs do people open, and do they book afterwards? {#faq}

### Fastest: autocapture, if the questions are buttons {#faq-fast}

Accordions built from `<button>` elements send each question as **Clicked text**. The limits: only the first 40 characters are kept, and a click that closes a question counts the same as one that opens it.

The common `<details>` and `<summary>` pattern doesn’t work this way. A `<summary>` is neither a link nor a button, so its clicks carry no text, and every one has the same selector, `summary:nth-of-type(1)`. Use the custom event.

### Precise: a custom event with the question {#faq-precise}

```html title="faq.html"
<details class="faq">
  <summary>Can I import contacts from HubSpot?</summary>
  <p>Yes. Open Contacts › Import and choose HubSpot.</p>
</details>

<script>
  document.querySelectorAll('details.faq').forEach(function (faq) {
    faq.addEventListener('toggle', function () {
      if (!faq.open) return // count opens, not closes
      window.clickclacks?.('event', 'FAQ opened', {
        question: faq.querySelector('summary').textContent.trim(),
      })
    })
  })
</script>
```

### Read the answer {#faq-read}

Most-opened questions:

1. In **Insights**, set series A to `FAQ opened`, counted as **Total events** (every open) or **Unique people** (who opened it).
2. Click **Add breakdown** › **Event property** › **Question**, **Top 25**.
3. Switch the chart to **Bar** or **Table**. Save it as _FAQ opens by question_.

Questions opened before booking:

1. In **Funnels**, build `FAQ opened` → `$pageview` where **Page** _is_ `/demo/booked`, with a **7 days** window.
2. Choose **Break down by** › **Question**.

> **What you should see**
>
> One bar per question, most opened first, each with its full text. In the funnel, the questions with the highest conversion to a booking. Those are worth moving higher up the page. Questions that get opened a lot but convert poorly show where the page leaves doubts.

## Pricing: who switches to annual, and which plan do they pick? {#pricing}

### Fastest: autocapture {#pricing-fast}

A toggle made of two buttons reading _Monthly_ and _Annual_ sends those words as **Clicked text**. A radio-input toggle doesn’t: clicks on form fields never carry text. Plan buttons usually all say _Start free trial_, so autocapture can only tell them apart by an `id` on each (**Clicked element ID**).

### Precise: custom events for the toggle and the plan {#pricing-precise}

```html title="pricing.html"
<fieldset class="billing-period">
  <label><input type="radio" name="billing-period" value="monthly" checked> Monthly</label>
  <label><input type="radio" name="billing-period" value="annual"> Annual</label>
</fieldset>

<a href="/signup?plan=starter" class="plan-cta" data-plan="starter">Start free trial</a>
<a href="/signup?plan=team" class="plan-cta" data-plan="team">Start free trial</a>

<script>
  function billingPeriod() {
    return document.querySelector('[name="billing-period"]:checked').value
  }

  document.querySelectorAll('[name="billing-period"]').forEach(function (input) {
    input.addEventListener('change', function () {
      window.clickclacks?.('event', 'Billing period switched', { period: input.value })
    })
  })

  document.querySelectorAll('.plan-cta').forEach(function (button) {
    button.addEventListener('click', function () {
      window.clickclacks?.('event', 'Plan selected', {
        plan: button.dataset.plan,
        period: billingPeriod(),
      })
    })
  })
</script>
```

### Read the answer {#pricing-read}

1. In **Insights**, chart `Plan selected` as **Unique people**. **Add breakdown** › **Event property** › **Plan**, and switch to **Bar**.
2. Change the breakdown to **Period** to see how many choose annual.
3. For the toggle, chart `Billing period switched` broken down by **Period**.
4. For plan to sign-up, build a funnel `Plan selected` → `Signed up` and choose **Break down by** › **Plan**.

> **What you should see**
>
> A bar per plan, and the split between monthly and annual at the moment of choosing. In the funnel, which plan’s visitors actually finish signing up.

## Where do outbound, phone and email links go? {#links}

### Fastest: autocapture, partly {#links-fast}

Every link click is a `$click` with **Clicked element** `a` and its **Clicked text**. The tracker doesn’t record where a link points, though, so autocapture can’t tell an outbound link, a `tel:` link or a `mailto:` link from any other. If your phone link shows the number, its text is the number, and that’s as far as autocapture goes.

### Precise: one listener for every link {#links-precise}

```html title="links.html"
<script>
  document.addEventListener('click', function (event) {
    var link = event.target.closest && event.target.closest('a[href]')
    if (!link) return
    var url = new URL(link.href, location.href)

    if (url.protocol === 'tel:') {
      window.clickclacks?.('event', 'Contact link clicked', { type: 'phone' })
    } else if (url.protocol === 'mailto:') {
      window.clickclacks?.('event', 'Contact link clicked', { type: 'email' })
    } else if (/^https?:$/.test(url.protocol) && url.hostname !== location.hostname) {
      // The domain only: a full URL can carry tokens or personal data.
      window.clickclacks?.('event', 'Outbound link clicked', { domain: url.hostname })
    }
  })
</script>
```

The event carries only the type or the destination’s domain, never the number, the address or a full URL. Links into your own other domains, such as `app.acme.com`, count as outbound here, and their domain says so.

### Read the answer {#links-read}

1. In **Insights**, chart `Outbound link clicked` as **Total events**, break it down by **Domain**, **Top 10**, as a **Bar** chart.
2. Chart `Contact link clicked` as **Unique people**, broken down by **Type**, as a **Line** by **Weekly** interval.
3. To see which pages people call or email from, change the breakdown to **Page**.

> **What you should see**
>
> A ranked list of the sites you send people to, such as your docs host or a partner, and a weekly count of people who tried to call or email you.

## How many people play the product video, and how many watch to the end? {#video}

### Fastest: autocapture can’t answer this {#video-fast}

Pressing play on a `<video>` isn’t a link or button click, and embedded players (YouTube, Vimeo, Wistia) run in an iframe, whose clicks never reach your page. Nothing tells ClickClacks that a video played or ended.

### Precise: two custom events {#video-precise}

```html title="video.html"
<video src="/media/product-tour.mp4" data-video="product-tour" controls></video>

<script>
  document.querySelectorAll('video[data-video]').forEach(function (video) {
    var name = video.dataset.video
    var played = false
    video.addEventListener('play', function () {
      if (played) return // the first play only, not every resume
      played = true
      window.clickclacks?.('event', 'Video played', { video: name })
    })
    video.addEventListener('ended', function () {
      window.clickclacks?.('event', 'Video finished', { video: name })
    })
  })
</script>
```

For an embedded player, send the same two events from the player’s own JavaScript API when it reports playing and ended.

### Read the answer {#video-read}

1. In **Insights**, set series A to `Video played`, **Unique people**, **Where** **Video** _is_ `product-tour`.
2. Click **Add series**. Set series B to `Video finished` with the same filter.
3. Click **Add formula**, keep `B / A * 100` and name it _Watched to the end_.
4. Use **Big number** for the whole range, or **Line** by week to follow it.

> **What you should see**
>
> How many people pressed play, and the share of them who reached the end. A low share usually means the video is too long for where it sits.

## How far do people scroll, and do they reach the plans? {#scroll}

### Fastest: the heatmap’s scroll depth {#scroll-fast}

Autocapture sends one `$scroll` per page view, when the visitor leaves the page, with the lowest point they saw in pixels. [Heatmaps](https://clickclacks.io/docs/guides/heatmaps.md) turn that into answers with no setup: open the page, pick a device and choose **Scroll depth**. You get the share of people who reach 25, 50, 75 and 100% of the page, with the median fold marked.

### Precise: one event per section {#scroll-precise}

Percentages move whenever the page gets longer, and pixels differ per device. To know whether people reached a _particular_ section, send an event when it comes into view:

```html title="sections.html"
<section data-section="plans">…</section>
<section data-section="faq">…</section>

<script>
  var seen = {}
  var observer = new IntersectionObserver(function (entries) {
    entries.forEach(function (entry) {
      var section = entry.target.dataset.section
      if (!entry.isIntersecting || seen[section]) return
      seen[section] = true // once per page load
      window.clickclacks?.('event', 'Section viewed', { section: section })
    })
  }, { rootMargin: '0px 0px -50% 0px' }) // its top has reached the middle of the screen

  document.querySelectorAll('[data-section]').forEach(function (el) {
    observer.observe(el)
  })
</script>
```

### Read the answer {#scroll-read}

1. In **Insights**, set series A to `$pageview`, **Unique people**, **Where** **Page** _is_ `/pricing`.
2. Set series B to `Section viewed`, **Unique people**, **Where** **Section** _is_ `plans`.
3. Add the formula `B / A * 100`, named _Reached the plans_.
4. To compare every section, chart `Section viewed` alone, **Where** **Page** _is_ `/pricing`, broken down by **Section**.

> **What you should see**
>
> The share of pricing-page visitors who got as far as the plans, which the scroll-depth bands can only approximate. Sections near the bottom fall off sharply. If a key section sits below the median fold, move it up.

## Which CTA placement gets more clicks, the hero or the footer? {#cta}

### Fastest: autocapture, with an `id` on each {#cta-fast}

Two buttons with the same label look the same in **Clicked text**. Give each one an `id` and autocapture tells them apart as **Clicked element ID**. It’s a change to your HTML, not your JavaScript:

```html title="index.html"
<a href="/signup" id="cta-hero">Start free trial</a>
<!-- … -->
<a href="/signup" id="cta-footer">Start free trial</a>
```

Or use the `placement` property from the [Book a demo event](https://clickclacks.io/docs/examples/marketing-site.md#book-demo-precise) above, which reads the same way.

### Read the answer {#cta-read}

1. In **Insights**, chart `$click` as **Unique people**, **Where** **Clicked element ID** _is any of_ `cta-hero`, `cta-footer`.
2. **Add breakdown** › **Event property** › **Clicked element ID**, and switch to **Bar**.
3. In **Heatmaps**, open the same page’s **Scroll depth** to see what share of people ever reach the footer.

> **What you should see**
>
> The hero almost always wins on raw clicks, because everyone sees it. To compare fairly, divide the footer’s clicks by the people who scroll that far. A footer button that few reach but many of them click is doing its job.

## Is a button broken? Finding rage clicks {#rage}

### Fastest: nothing to add {#rage-fast}

Rage clicks come from autocapture. [Friction](https://clickclacks.io/docs/guides/friction.md) lists an element once people click it 5 or more times within 10 seconds, and at least 25 different people have done so in the last 7 days. For a single page, the [Elements](https://clickclacks.io/docs/guides/heatmaps.md#views) view in Heatmaps shows every clicked element with its dead clicks and rage clicks, however few.

### Precise: say when the action fails {#rage-precise}

When the element runs your own code, send an event when it fails. Now you count the failures themselves, not just the frustration they cause:

```js title="checkout.js"
applyCoupon(code).catch(function (error) {
  window.clickclacks?.('event', 'Coupon failed', { reason: error.code || 'unknown' })
  showCouponError(error)
})
```

### Read the answer {#rage-read}

1. Open **Friction** on **Happening**. Rows name the element and the page, for example _People are rage-clicking “Apply coupon” on /checkout_.
2. For one page, open it in **Heatmaps**, choose **Elements** and sort by rage clicks.
3. For the custom event, chart `Coupon failed` in **Insights**, broken down by **Reason**.

> **What you should see**
>
> The element people hammer, on which page, and how many people it hits. Friction also flags a custom event that fires 3 or more times just before the session ends, such as repeated `Coupon failed`, as an abandoned retry.

## Which 404 pages do people hit, and from where? {#not-found}

### Fastest: autocapture can’t tell {#not-found-fast}

A missing page still sends a normal `$pageview` for the address that was asked for. The tracker doesn’t see the HTTP status or the page title, so nothing marks it as a 404.

### Precise: a custom event on the 404 template {#not-found-precise}

```html title="404.html"
<!-- On your 404 template only -->
<script>
  var from = 'direct'
  try {
    var ref = new URL(document.referrer)
    // A broken link on your own site: its page. Anywhere else: the site's host.
    from = ref.host === location.host ? ref.pathname : ref.host
  } catch (e) {}

  window.clickclacks?.('event', 'Page not found', { from: from })
</script>
```

In a single-page app, send the same event from your not-found route.

### Read the answer {#not-found-read}

1. In **Insights**, chart `Page not found` as **Total events**.
2. **Add breakdown** › **Event property** › **Page**, **Top 25**, and switch to **Table**.
3. Change the breakdown to **From** to find the broken links: a path is a page on your site, a host is another site.

> **What you should see**
>
> The missing addresses people hit most, and the pages or sites that link to them. Fix the internal links and redirect the addresses other sites use.

## Related {#related}

- [All examples](https://clickclacks.io/docs/examples.md)
- [Forms and leads](https://clickclacks.io/docs/examples/forms.md): Form drop-off and newsletter sign-ups.
- [Autocapture](https://clickclacks.io/docs/autocapture.md): Every property $click, $scroll and $pageview carry.
- [Insights](https://clickclacks.io/docs/guides/insights.md): Series, breakdowns and formulas.
