If you're building a modern web application with Next.js and care about user privacy while still getting meaningful analytics, Plausible is an excellent choice. Unlike Google Analytics, Plausible is lightweight (< 1KB), privacy-friendly (no cookies, GDPR compliant), and provides all the essential metrics you need without the bloat.
In this guide, I'll walk you through integrating Plausible Analytics with a Next.js 14 application using the App Router, including support for internationalization (i18n), custom events, and environment-specific configurations. This is the exact setup I use for Nibi, my financial planning app for Canadian freelancers.
Why Plausible + Next.js?
Before diving into the implementation, here's why this combination works so well:
- Performance: Plausible's script is under 1KB, compared to Google Analytics' 45KB+
- Privacy-First: No cookie banners needed, fully GDPR/CCPA compliant
- Developer Experience: The
next-plausible
package provides TypeScript support and React hooks - Real-time Data: See your visitors in real-time without any delay
- Simple Interface: Clean, focused dashboard without overwhelming features
Prerequisites
Before starting this tutorial, make sure you have:
- A Next.js 14+ project using the App Router (not Pages Router)
- A Plausible account (free 30-day trial available at plausible.io)
- Your domain added to Plausible (you'll get a snippet after adding your site)
- Node.js 18+ and npm or yarn installed
- Basic knowledge of Next.js and React
Your Plausible snippet will look something like this:
html
<script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js"></script>
Step 1: Install the next-plausible Package
First, we'll install the official community package recommended by Plausible for Next.js integration:
bash
npm install next-plausible
# or
yarn add next-plausible
# or
pnpm add next-plausible
This package provides:
- Optimized script loading for Next.js
- TypeScript definitions
- React hooks for custom events
- Automatic handling of client-side navigation
Step 2: Basic Integration in App Router
The App Router structure in Next.js 14 is different from the Pages Router. Here's how to integrate Plausible:
For a Simple App (without i18n)
If you have a standard Next.js app without internationalization, update your root layout:
app/layout.tsx
tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import PlausibleProvider from 'next-plausible'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Your App Name',
description: 'Your app description',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<PlausibleProvider domain="yourdomain.com" />
</head>
<body className={inter.className}>{children}</body>
</html>
)
}
For a Multilingual App (with i18n)
If you're using dynamic routing with locales like I do with Nibi, your structure might look like app/[locale]/layout.tsx
:
app/[locale]/layout.tsx
tsx
import '../globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import PlausibleProvider from 'next-plausible'
const inter = Inter({ subsets: ['latin'] })
export async function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'fr' }]
}
export const metadata: Metadata = {
title: 'Your App Name',
description: 'Your app description',
}
export default function RootLayout({
children,
params: { locale }
}: {
children: React.ReactNode
params: { locale: string }
}) {
return (
<html lang={locale}>
<head>
<PlausibleProvider
domain="yourdomain.com"
trackOutboundLinks
taggedEvents
/>
</head>
<body className={inter.className}>{children}</body>
</html>
)
}
Step 3: Configure Advanced Features
Plausible offers several optional features that you can enable based on your needs:
tsx
<PlausibleProvider
domain="yourdomain.com"
customDomain="https://plausible.io" // Use this if self-hosting
trackOutboundLinks={true} // Track clicks on external links
trackFileDownloads={true} // Track file download links
taggedEvents={true} // Enable tagged events
revenue={true} // Track revenue in custom events
pageviewProps={{ // Add custom properties to every pageview
locale: locale,
version: 'v1.0.0'
}}
selfHosted={false} // Set to true if self-hosting Plausible
enabled={true} // Conditionally enable/disable tracking
/>
Step 4: Environment-Specific Configuration
You don't want to track development visits in your analytics. Here's how to set up environment-specific tracking:
Create Environment Variables
.env.local
bash
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
NEXT_PUBLIC_PLAUSIBLE_ENABLED=true
.env.development
bash
NEXT_PUBLIC_PLAUSIBLE_ENABLED=false
Update Your Layout
tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const isDevelopment = process.env.NODE_ENV === 'development'
return (
<html lang="en">
<head>
<PlausibleProvider
domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || "yourdomain.com"}
enabled={!isDevelopment} // Only track in production
trackOutboundLinks
taggedEvents
/>
</head>
<body>{children}</body>
</html>
)
}
Step 5: Track Custom Events
One of Plausible's strengths is its simple yet powerful custom events. Here's how to implement them:
Basic Event Tracking
tsx
'use client'
import { usePlausible } from 'next-plausible'
export default function SignupButton() {
const plausible = usePlausible()
const handleClick = () => {
// Track the event
plausible('signup_click')
// Your actual logic
console.log('Signup clicked!')
}
return (
<button onClick={handleClick}>
Sign Up Now
</button>
)
}
Events with Properties
You can add metadata to your events for more detailed analytics:
tsx
'use client'
import { usePlausible } from 'next-plausible'
export default function PricingCard({ plan }: { plan: string }) {
const plausible = usePlausible()
const handleUpgrade = () => {
// Track with properties
plausible('upgrade_click', {
props: {
plan: plan,
location: 'pricing_page',
currency: 'USD'
}
})
}
return (
<button onClick={handleUpgrade}>
Upgrade to {plan}
</button>
)
}
Revenue Tracking
If you enabled revenue tracking, you can track monetary values:
tsx
plausible('purchase', {
revenue: {
amount: 99.99,
currency: 'USD'
},
props: {
product: 'lifetime_access'
}
})
Step 6: Common Implementation Patterns
Track Form Submissions
tsx
'use client'
import { usePlausible } from 'next-plausible'
import { useState } from 'react'
export default function NewsletterForm() {
const plausible = usePlausible()
const [email, setEmail] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await subscribeToNewsletter(email)
// Track successful submission
plausible('newsletter_signup', {
props: {
location: 'footer',
type: 'early_access'
}
})
} catch (error) {
// Track failed submission
plausible('newsletter_signup_error', {
props: {
error: error.message
}
})
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
/>
<button type="submit">Subscribe</button>
</form>
)
}
Track 404 Pages
app/not-found.tsx
tsx
'use client'
import { useEffect } from 'react'
import { usePlausible } from 'next-plausible'
export default function NotFound() {
const plausible = usePlausible()
useEffect(() => {
plausible('404', {
props: {
path: window.location.pathname
}
})
}, [plausible])
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
)
}
Track Scroll Depth
tsx
'use client'
import { useEffect } from 'react'
import { usePlausible } from 'next-plausible'
export default function LongArticle() {
const plausible = usePlausible()
useEffect(() => {
let maxScroll = 0
const trackScroll = () => {
const scrollPercentage = Math.round(
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
)
if (scrollPercentage > maxScroll) {
maxScroll = scrollPercentage
// Track milestones
if (maxScroll === 25 || maxScroll === 50 || maxScroll === 75 || maxScroll === 100) {
plausible('scroll_depth', {
props: {
depth: `${maxScroll}%`
}
})
}
}
}
window.addEventListener('scroll', trackScroll)
return () => window.removeEventListener('scroll', trackScroll)
}, [plausible])
return <article>{/* Your content */}</article>
}
Step 7: Configure Goals in Plausible Dashboard
After implementing event tracking, configure goals in your Plausible dashboard:
- Log in to your Plausible account
- Navigate to your site settings
- Click on "Goals"
- Add your custom events as goals:
signup_click
newsletter_signup
purchase
404
This enables conversion tracking and funnel analysis.
Step 8: Deploy to Production
For Vercel Deployment
Add your environment variables to Vercel:
bash
vercel env add NEXT_PUBLIC_PLAUSIBLE_DOMAIN
# Enter: yourdomain.com
vercel env add NEXT_PUBLIC_PLAUSIBLE_ENABLED
# Enter: true
Or via the Vercel Dashboard:
- Go to Project Settings β Environment Variables
- Add
NEXT_PUBLIC_PLAUSIBLE_DOMAIN
=yourdomain.com
- Add
NEXT_PUBLIC_PLAUSIBLE_ENABLED
=true
Deploy your application:
bash
git add .
git commit -m "Add Plausible Analytics integration"
git push
Step 9: Verify Your Integration
Check Script Loading
- Open your production site
- Open Developer Tools β Network tab
- Filter by "plausible"
- You should see the script loading
Test Events in Console
javascript
// Test if Plausible is loaded
window.plausible('test_event')
Check Real-time Dashboard
- Visit your site
- Go to your Plausible dashboard
- You should see yourself as a current visitor

Advanced Configuration
Self-Hosted Plausible
If you're self-hosting Plausible:
tsx
<PlausibleProvider
domain="yourdomain.com"
customDomain="https://analytics.yourdomain.com"
selfHosted={true}
/>
Proxy to Avoid Ad Blockers
Some ad blockers block Plausible. You can proxy the script through your domain:
next.config.js
javascript
module.exports = {
async rewrites() {
return [
{
source: '/stats/js/script.js',
destination: 'https://plausible.io/js/script.js'
},
{
source: '/stats/api/event',
destination: 'https://plausible.io/api/event'
}
]
}
}
Then update your provider:
tsx
<PlausibleProvider
domain="yourdomain.com"
customDomain="https://yourdomain.com/stats"
/>
Troubleshooting Common Issues
Issue: Events Not Tracking
Solution: Ensure you're using the hook inside client components:
tsx
'use client' // Don't forget this!
import { usePlausible } from 'next-plausible'
Issue: Development Visits Being Tracked
Solution: Check your environment configuration:
tsx
enabled={process.env.NODE_ENV === 'production'}
Issue: Script Not Loading
Solution: Ensure PlausibleProvider is in the <head>
tag:
tsx
<head>
<PlausibleProvider domain="yourdomain.com" />
</head>
Issue: TypeScript Errors
Solution: Install types if needed:
bash
npm install --save-dev @types/react
Best Practices
- Keep Event Names Consistent: Use snake_case for all events (e.g.,
button_click
, notbuttonClick
orbutton-click
) - Limit Event Properties: Plausible has limits on custom properties. Keep them simple and meaningful.
- Don't Track PII: Never include personally identifiable information in events or properties.
- Use Meaningful Event Names: Instead of
click
, useheader_navigation_click
for better insights. - Group Related Events: Use consistent naming patterns like
form_submit
,form_error
,form_abandon
.
Performance Considerations
The next-plausible
package is already optimized, but here are additional tips:
- The script is loaded with
defer
by default - It's only 1KB, so it won't impact your Core Web Vitals
- Events are batched and sent asynchronously
- No impact on Time to Interactive (TTI)
Conclusion
Integrating Plausible Analytics with Next.js 14's App Router is straightforward and provides a privacy-friendly alternative to traditional analytics platforms. With this setup, you get:
- β Lightweight analytics (< 1KB)
- β GDPR compliance without cookie banners
- β Real-time visitor data
- β Custom event tracking
- β TypeScript support
- β Great developer experience
The combination of Next.js and Plausible offers the perfect balance of performance, privacy, and insights. Your users will appreciate the privacy-first approach, and you'll still get all the metrics you need to improve your application.
Member discussion