Ballad
Measurement

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.

~10 min9 sectionsNext.js App Router

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

SignalWhere it comes fromSetup
Pageviews and visitors per postVercel Web Analyticsenable it — no code
Signups, demo requestscustom events you emittwo 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 development in the console, and the remote script is never injected. That is expected — verify by checking that window.va is 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:

EventFire it when
signupsomeone completes signup
demo_requestsomeone 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_request wired 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.

Next guideSet up your Next.js blog