Skip to content
better-i18n.com
本页内容

The @better-i18n/content SDK is a lightweight tracking client for Better i18n Content. Drop it into any frontend, call track() or useTrackView(), and your dashboard fills with views, languages, models, and countries — automatically.

What gets tracked #

By default, only what you explicitly track. There's no auto-pageview, no fingerprinting, no cookies.

Every track() call sends one event with:

  • Project + organization context (resolved from your API key)
  • Event name (e.g. content.view)
  • Custom properties you pass (entryId, language, framework, ...)
  • Country code (from Cloudflare's edge metadata — never IP)
  • Timestamp

See Data Model for the full schema.

Configure #

You need two values from the dashboard:

ValueWhere to find it
Project IDSettings → General → Project ID (format: your-org/your-project)
API KeySettings → API Keys → Public Key (starts with bi_pub_)

Set them in your .env.local:

.env.local
NEXT_PUBLIC_BETTER_I18N_PROJECT_ID=your-org/your-project
NEXT_PUBLIC_BETTER_I18N_KEY=bi_pub_xxxxx

Install #

npm install @better-i18n/content

Quick start (Next.js) #

Wrap your app #

app/providers.tsx
'use client'
import { ContentProvider } from '@better-i18n/content/adapters/nextjs'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ContentProvider
      config={{
        projectId: process.env.NEXT_PUBLIC_BETTER_I18N_PROJECT_ID!,
        apiKey: process.env.NEXT_PUBLIC_BETTER_I18N_KEY!,
      }}
    >
      {children}
    </ContentProvider>
  )
}

Track views on content pages #

app/blog/[slug]/page.tsx
'use client'
import { useTrackView } from '@better-i18n/content/adapters/nextjs'

export default function BlogPost({ post }: { post: Post }) {
  useTrackView('content.view', {
    entryId: post.id,
    contentModel: 'blog',
    entrySlug: post.slug,
    language: post.locale,
  })

  return <article>{post.body}</article>
}

See the data #

Open your project in the dashboard — within seconds, the Content Analytics section fills in. Total views, top entries, language breakdown, country map.

Why a public API key is safe #

The SDK uses your bi_pub_* key — designed to be embedded in client-side code. Track events are write-only:

  • Can't read content data
  • Can't read other projects' analytics
  • Server-side rate-limited per IP and per project

This is the same model PostHog, Mixpanel, and Vercel Analytics use. Stolen keys can spam your events, but they can't exfiltrate anything.

See Why is my API key safe? below for the threat model.

Transport: never block your page #

The SDK tries three transports in order and silently drops failures:

Code
sendBeacon  →  fetch(keepalive)  →  fetch
   │              │                    │
   │              └─ < 51.2 KB body    └─ regular POST
   └─ fire-and-forget at page unload

sendBeacon survives navigation and tab close — events never get lost when users leave. fetch keepalive handles the rest under the body limit. Plain fetch is the last resort.

All three are wrapped in try/catch. A failed event is dropped silently — never thrown to your app.

SSR & build-time safety #

The SDK has three guards:

  • isBuildTime() — Detects NEXT_PHASE=phase-production-build, GATSBY_BUILD_STAGE, ASTRO_BUILD. Calls during build log a warning and skip.
  • isBrowser() — Calls from a server (Node.js, edge runtime) skip unless options.allowServer: true.
  • Synchronous initContentProvider creates the tracker synchronously during the first render. The tracker itself is SSR-safe: track() checks isBrowser() internally and becomes a no-op on the server.

You can call track() in any render path — it's safe.

Graceful degradation #

If useTrackView() or useContent() is called outside of a ContentProvider, the SDK logs a one-time warning and silently disables tracking — your app never crashes.

Code
[better-i18n/content] useContent called outside ContentProvider. Tracking will be disabled.

This matches the behavior of PostHog, Segment, and other production analytics SDKs.

Other frameworks #

import { ContentProvider, useTrackView } from '@better-i18n/content/adapters/react'

<ContentProvider config={{ projectId, apiKey }}>
  <App />
</ContentProvider>

Note: Expo uses fetch (no sendBeacon on React Native).

import { provideContent, useTrack } from '@better-i18n/content/adapters/vue'

// In your root component
provideContent({ projectId, apiKey })

// In any child component
const track = useTrack()
track('content.view', { entryId: 'abc' })

Or via window.betterContent.track(...) after init().

Property validation #

Properties must be primitive: string, number, boolean, or null. Nested objects and arrays are rejected:

  • Development (NODE_ENV !== 'production'): throws Error("Invalid property...")
  • Production: silently strips the invalid property and warns in debug mode

This catches bugs early without breaking your app in prod.

TypeScript
// ✅ OK
track('content.view', { entryId: 'abc', count: 1, featured: true })

// ❌ Throws in dev, stripped in prod
track('content.view', { entryId: 'abc', author: { id: '1' } })

Why is my API key safe? #

ConcernMitigation
Key extracted from JS bundleKey is write-only — can't read content or analytics
Spam events from stolen keyServer-side rate limit (per IP, per project) + CF WAF
Cross-project accessKey scoped to a single projectId at validation time
Sensitive PII in propertiesYou control payload — never log emails, only IDs

Phase 1 mitigations: KV-cached key validation (1h TTL), CF Workers CPU/memory limits, server-side validation rejects malformed payloads.

Proxy to avoid ad blockers #

Ad blockers may block requests to content.better-i18n.com. Proxy the track endpoint through your own domain so the browser only sees first-party requests.

Next.js rewrites

The simplest option for Next.js apps. Add a rewrite rule in next.config.js:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/ca/:path*',
        destination: 'https://content.better-i18n.com/:path*',
      },
    ]
  },
}

export default nextConfig

Then point the SDK to your proxy:

TSX
<ContentProvider
  config={{
    projectId: process.env.NEXT_PUBLIC_BETTER_I18N_PROJECT_ID!,
    apiKey: process.env.NEXT_PUBLIC_BETTER_I18N_KEY!,
    analytics: {
      endpoint: '/api/ca/v1/track',
    },
  }}
>

Next.js middleware

If rewrites cause issues on your host, use a middleware (Next.js 16: proxy.js, 15 and earlier: middleware.js):

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/ca/')) {
    const url = new URL(
      request.nextUrl.pathname.replace('/api/ca', ''),
      'https://content.better-i18n.com',
    )
    url.search = request.nextUrl.search
    return NextResponse.rewrite(url)
  }
}

export const config = {
  matcher: '/api/ca/:path*',
}

Cloudflare Worker

Zero egress fees and 100K free requests/day. Deploy a Worker on your domain:

worker.ts
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url)
    if (!url.pathname.startsWith('/api/ca/')) {
      return new Response('Not found', { status: 404 })
    }

    const target = new URL(
      url.pathname.replace('/api/ca', ''),
      'https://content.better-i18n.com',
    )
    target.search = url.search

    const headers = new Headers(request.headers)
    headers.set('host', 'content.better-i18n.com')

    return fetch(target.toString(), {
      method: request.method,
      headers,
      body: request.body,
    })
  },
}

Add a route in your wrangler.toml matching /api/ca/* on your domain.

Nginx / Caddy

location /api/ca/ {
    proxy_pass https://content.better-i18n.com/;
    proxy_set_header Host content.better-i18n.com;
    proxy_ssl_server_name on;
}

Next steps #

  • API Reference — full surface area: track(), useTrackView(), options
  • Data Model — blob/double mapping, what's stored where