Connect Ballad to Vercel Analytics
Close the measurement loop: enable Vercel Web Analytics, emit signup and demo_request events, and connect it to Ballad so traffic and conversions feed Signals.
Ballad measures whether your content is working, not just whether it shipped. Connecting Vercel Analytics closes the loop: what you publish drives traffic, traffic converts, and both feed the next plan.
This takes about ten minutes and is mostly configuration. It assumes your site is on Vercel and already rendering Ballad content — if not, start with Set up your Next.js blog.
What Ballad reads
| Signal | Where it comes from | Setup |
|---|---|---|
| Pageviews and visitors per post | Vercel Web Analytics | enable it — no code |
| Signups, demo requests | custom events you emit | two track() calls |
Per-post traffic arrives automatically once analytics is on. Conversions do not — nothing can infer a signup from a pageview, so you have to say when one happened.
1. Turn on Web Analytics
In the Vercel dashboard: your project → Analytics → enable Web Analytics.
This is the step people skip. The package below is completely inert until the project has analytics enabled — no error, no warning, just no data.
2. Add the package
npm install @vercel/analytics
Mount it once in your root layout:
// app/layout.tsx
import { Analytics } from '@vercel/analytics/next';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}
The component ships its own "use client" and Suspense boundary, so it drops
into a server-component layout directly — you don't need a wrapper, and it
doesn't make your layout a client component.
Local development sends nothing, by design. You'll see
Debug mode is enabled by default in developmentin the console, and the remote script is never injected. That is expected — verify by checking thatwindow.vais a function and that a pageview is logged, not by waiting for data to appear in the dashboard.
3. Emit conversion events
Ballad reads two events by name. The names are the contract:
| Event | Fire it when |
|---|---|
signup | someone completes signup |
demo_request | someone submits a demo or contact request |
'use client';
import { track } from '@vercel/analytics';
async function onSubmit(event) {
event.preventDefault();
const res = await fetch('/api/signup', { method: 'POST', body });
if (!res.ok) {
setError('Something went wrong. Please try again.');
return;
}
track('signup'); // only after it actually succeeded
setDone(true);
}
Three things matter here, and each one quietly corrupts the funnel if you get it wrong.
Fire on success, not on submit. If the request fails and you've already tracked, your funnel counts a signup you never received. Ballad plans the next round of content off these numbers — a conversion rate inflated by failures tells it something worked when it didn't.
Don't fire on a click. A "Contact sales" button that navigates to a form is not a demo request; the person hasn't done anything yet. Track the submit, not the intent to maybe submit.
Only emit an event you actually have. If your site has no demo flow, don't
wire demo_request to something approximate. A missing event reads as zero,
which is true. A phantom event reads as demand that doesn't exist.
track() is client-side, so the call has to live in a client component. That
usually means the form itself, which is already interactive — you rarely need
a new "use client" boundary for this.
4. Keep your post URLs matching your collection
Ballad attributes traffic to a post by path, mapping
/{collection}/{slug} to the item with that slug.
So if your Ballad collection is blog, your posts must live at
/blog/<slug>. Serve the same content at /writing/<slug> or
/posts/<slug> and the pageviews still get recorded by Vercel — they just
won't attach to any post, and per-post traffic in Signals stays empty while
your dashboard shows plenty of visitors.
If you need a different route, rename the collection to match rather than the other way round.
5. Connect it to Ballad
In Ballad, open Settings → Site and connect Vercel Analytics. Ballad needs a Vercel access token with read access to the project, plus the project ID (and team ID, if it's under a team). Ballad polls the analytics API with those and folds the results into Signals.
Scope the token to what it needs. It's a credential, and read access to analytics is all Ballad requires.
6. Optional: Speed Insights
Core Web Vitals from real visits, in the same shape:
npm install @vercel/speed-insights
import { SpeedInsights } from '@vercel/speed-insights/next';
// …then <SpeedInsights /> next to <Analytics /> in the layout
Enable it separately in the dashboard — Project → Speed Insights. Unlike Web Analytics it's a metered feature on some plans, so check before switching it on. Ballad doesn't read it; it's for you.
When the numbers don't show up
Almost every case is one of these:
- Analytics isn't enabled for the project in the Vercel dashboard.
- You're looking at local dev, where sends are suppressed.
- The deployment predates the change. Vercel captures configuration at build time, so enabling analytics or adding an environment variable doesn't affect deployments that already exist. Redeploy.
- Your post paths don't match the collection slug — see step 4.
- You're inside the reporting window. Aggregate queries only reach back as far as your plan's retention, so a fresh project has little history to synthesise from. Signals will be thin at first, and honest about it.
Checklist
- Web Analytics enabled in the Vercel dashboard
-
<Analytics />mounted in the root layout -
track('signup')fires on success, not on submit -
demo_requestwired only if a real demo flow exists - Post URLs are
/{collection}/{slug} - Vercel token and project ID added in Ballad → Settings → Site
- Redeployed after enabling anything
With that in place the loop is closed end to end: Ballad publishes, the post earns traffic, conversions get counted, and the next plan is written against what actually happened rather than what felt like it should have.