Next.js blog
Track a blog built with @founderhq/next-blog.
A blog built with @founderhq/next-blog sends its page views and clicks to
FounderHQ. Each page view says which blog page it was. Each click says which
part of the blog the visitor used.
Before you start
You need a publishable key (fhq_pk_...) for your brand. See
Getting started.
What it tracks
- Page views with blog context. Every blog page view carries the page type and the slug. An article page view also carries its author and category.
- Named clicks in the blog. A click on the outline, an article card, an author, a category, pagination, a CTA, the newsletter button, or a share button carries a name for that part of the blog.
Page views are sent only when a page opens. Clicks come from click capture, not from code in each button.
Turn it on
The installer (npx @founderhq/next-blog@latest init) writes these
variables to .env.example. Set them in your environment:
| Variable | Value |
|---|---|
NEXT_PUBLIC_FOUNDERHQ_EVENTS_WRITE_KEY | Your publishable key (fhq_pk_...). Without it, the blog's tracker does nothing. |
NEXT_PUBLIC_FOUNDERHQ_INGEST_URL | https://i.getfounderhq.com, the SDK's default host. Leave it unset to use the default. |
NEXT_PUBLIC_FOUNDERHQ_EVENTS_CONSENT_DEFAULT | Optional. granted, pending, or denied: the consent state a visitor starts in. Leave it unset for granted. |
An unset host uses the default. An empty string does not: the SDK sends to
it as a host. The generated src/lib/founderhq-blog.ts passes
NEXT_PUBLIC_FOUNDERHQ_INGEST_URL || undefined, so an empty value works like
an unset one. If you build the events object yourself, do the same.
The generated blog layout renders <BlogEventsTracker> with these values.
You do not add code.
How it uses your site's SDK client
Run one SDK client per write key on a page. Two clients with the same key count every page twice. So the blog reuses your site's client when there is one.
BlogEventsTracker waits until the page has loaded, or 1.5 seconds after the
blog mounts if loading takes longer. Then it calls
getFounderHqClient(writeKey):
- Your site already runs
@founderhq/eventswith the same key. The tracker adds nothing. Your client records the page views and clicks. A client inmode: "pageviews"records no clicks. - No client uses the key. The tracker starts its own client with
autocapture: false. It records page views, and clicks inside the blog only. When the visitor leaves the blog, it sends what it has queued and stops.
The wait matters because a site often starts its SDK from code that loads with the page, or in a root component's effect, which React runs after the blog's. By the time the page has loaded, that client exists. The 1.5-second limit keeps a slow image or third-party script from costing the page views.
If your site's client starts even later, for example after a consent step,
it takes over from the tracker's client: the tracker's client sends what it
has queued and stops, and the visitor stays one person. From then on every
page counts once. Only the page that is open at that moment can count twice.
The tracker starts its client with the SDK's
yield_to_site_client
option for this.
The tracker's own client follows the SDK's consent rules. It reads a consent choice that your site's SDK remembered for the key. With no remembered choice, a visitor starts with consent granted, and the tracker records page views, clicks, and blog properties.
Your site shows a consent banner? Set
NEXT_PUBLIC_FOUNDERHQ_EVENTS_CONSENT_DEFAULT=pending. The tracker then
counts page views without cookies and records nothing else, until your
site's SDK remembers the visitor's answer.
If your site runs the SDK itself from page load, you do not need the variable: the tracker finds your site's client and never starts its own.
Page properties
Each blog page renders <BlogPageProperties>. It adds
fhq-prop meta tags,
which the SDK adds to that page's page view.
It also adds <meta name="fhq-page-path"> with the path of the page it
describes. The SDK reads the tags from <head>, and applies them only to a
page view of that path. So a back or forward navigation never gets the
properties of the page the visitor left.
On a slow navigation that shows a loading skeleton first, the SDK waits up to about 1 second for the new page's tags. After that, the page view goes out without the blog properties.
| Property | Value | Pages |
|---|---|---|
page_name | blog_index | Index, index pages 2 and later, search |
page_name | blog_article | Article |
page_name | blog_author | Authors, one author's articles |
page_name | blog_category | Categories, one category's articles |
page_name | blog_tag | Tags, one tag's articles |
object_id | The slug of the article, author, category, or tag | Article, and one author's, category's, or tag's articles |
blog_author | The author's display name | Article |
blog_category | The article's first category name | Article |
The values are your own public content names. They contain no data about the visitor.
Click names
The blog's <main> element carries data-fhq-autocapture. The SDK
captures clicks inside it even when your client has autocapture: false.
Links and buttons carry
data-fhq-prop-* attributes,
so each click gets a blog_component property:
blog_component | What the visitor clicked | Extra property |
|---|---|---|
toc | An entry in the article outline | blog_heading: the heading id |
related | A card under "Related articles" | blog_article: the slug it opens |
article | An article card on the index, search, or an archive | blog_article: the slug it opens |
author | The author name on an article, or an entry on the Authors page | none |
category | The category link on an article, a filter tab on the index, or an entry on the Categories page | none |
tag | An entry on the Tags page | none |
pagination | Previous or Next | none |
cta | The button in a CTA block | none |
newsletter | The subscribe button in a newsletter block | none |
share | Share on X, Share on LinkedIn, or Copy link | none |
back | The "Blog" breadcrumb, or "Browse all articles" on an empty list | none |
The SDK never records what the visitor types, so the newsletter email stays out of every event.
Add your own
The blog components are copied into your app (src/components/blog/blog-components.tsx).
Edit them like any other component:
- Name a click. Add
data-fhq-prop-<key>to the element or a parent. The nearest value wins. - Describe a page. Render
<meta name="fhq-prop-<key>" content="…" />in the page component, next to<BlogPageProperties>, which gives the tags the page's path. Do not usegenerateMetadata: Next.js can stream it after the page view. - Keep a part out. Add
data-fhq-no-captureto the element. No click inside it is captured.
<a href="/pricing" data-fhq-prop-blog-component="cta" data-fhq-prop-offer="trial">
Start free
</a>Keys use hyphens in markup and underscores in FounderHQ:
data-fhq-prop-offer-code becomes offer_code.
Where you see it
| What | Where in FounderHQ |
|---|---|
| Page views | Analytics → Online now, in the list of visitors and in each visitor's steps |
| Rage, dead, and outbound clicks | Analytics → Online now, in the same places |
Plain clicks, with their blog_component | The visitor's timeline in Analytics → People. Online now hides plain clicks. |
Verify
- Open an article on your blog.
- In FounderHQ, open Analytics → Events & Visits.
- You see a
$pageviewwithpage_name: blog_articleand the article's slug asobject_id. - Click a related article card.
- You see an
$autocaptureevent withblog_component: related.
Troubleshoot
Every blog page counts twice
Two clients use the same key. If you host events.js yourself, replace
your copy with the current one, so the tracker can find your site's
client.
When your site's client starts after the tracker's, the page open at that moment can count twice. Later pages count once. This is expected.
Page views arrive with no blog properties, and no clicks arrive
The visitor declined consent on your site, and the blog's client honors that answer. It then counts page views without cookies and records nothing else.