├── .npmrc ├── playground ├── .env.example ├── package.json ├── tsconfig.json ├── nuxt.config.ts └── app.vue ├── .nuxtrc ├── .github ├── og.png └── workflows │ ├── ci.yml │ └── release.yml ├── tsconfig.json ├── .vscode ├── extensions.json └── settings.json ├── pnpm-workspace.yaml ├── .editorconfig ├── src ├── runtime │ ├── composables │ │ ├── useTrackPageview.ts │ │ └── useTrackEvent.ts │ ├── plugin-preconnect.client.ts │ ├── plugin-auto-pageviews.client.ts │ ├── plugin-auto-outbound-tracking.client.ts │ ├── plugin.client.ts │ └── server │ │ └── event-handler.ts └── module.ts ├── .gitignore ├── LICENSE ├── eslint.config.mjs ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | NUXT_PUBLIC_PLAUSIBLE_DOMAIN=example.com 2 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | typescript.includeWorkspace=true 3 | -------------------------------------------------------------------------------- /.github/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt-modules/plausible/HEAD/.github/og.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "exclude": ["dist", "playground"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "Vue.volar" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | 4 | onlyBuiltDependencies: 5 | - '@parcel/watcher' 6 | - esbuild 7 | - unrs-resolver 8 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt dev", 7 | "build": "nuxt build", 8 | "generate": "nuxt generate" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./.nuxt/tsconfig.app.json" }, 4 | { "path": "./.nuxt/tsconfig.server.json" }, 5 | { "path": "./.nuxt/tsconfig.shared.json" }, 6 | { "path": "./.nuxt/tsconfig.node.json" } 7 | ], 8 | "files": [] 9 | } 10 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import NuxtPlausible from '../src/module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [NuxtPlausible], 5 | 6 | compatibilityDate: '2025-09-01', 7 | 8 | plausible: { 9 | // Enable tracking events on localhost 10 | ignoredHostnames: [], 11 | autoPageviews: false, 12 | autoOutboundTracking: false, 13 | proxy: true, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/runtime/composables/useTrackPageview.ts: -------------------------------------------------------------------------------- 1 | import type { EventOptions, Plausible } from '@barbapapazes/plausible-tracker' 2 | import { useNuxtApp } from '#imports' 3 | 4 | /** 5 | * Manually track a page view 6 | * 7 | * @remarks 8 | * Pass optional event data to be sent with the `eventData` argument. Defaults to the current page's data merged with the default options provided during the Plausible initialization. 9 | * 10 | * @example 11 | * useTrackPageview() 12 | */ 13 | export function useTrackPageview(options?: EventOptions) { 14 | if (import.meta.client) { 15 | ;(useNuxtApp().$plausible as Plausible)?.trackPageview(options) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/runtime/plugin-preconnect.client.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useHead, useRuntimeConfig } from '#imports' 2 | 3 | export default defineNuxtPlugin({ 4 | name: 'plausible:preconnect', 5 | setup() { 6 | const { plausible: options } = useRuntimeConfig().public 7 | 8 | // Add preconnect to Plausible API host for better performance 9 | // Note: This plugin is only loaded when proxy is disabled 10 | if (options.enabled && options.apiHost) { 11 | useHead({ 12 | link: [ 13 | { 14 | rel: 'preconnect', 15 | href: options.apiHost, 16 | }, 17 | ], 18 | }) 19 | } 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | # .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /src/runtime/plugin-auto-pageviews.client.ts: -------------------------------------------------------------------------------- 1 | import type { Plausible } from '@barbapapazes/plausible-tracker' 2 | import type {} from 'nuxt/app' 3 | import { defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from '#imports' 4 | import { useAutoPageviews } from '@barbapapazes/plausible-tracker/extensions/auto-pageviews' 5 | 6 | export default defineNuxtPlugin({ 7 | name: 'plausible:auto-pageviews', 8 | setup() { 9 | const { plausible: options } = useRuntimeConfig().public 10 | const { $plausible } = useNuxtApp() 11 | 12 | if (options.enabled && options.autoPageviews) { 13 | useAutoPageviews($plausible as Plausible).install() 14 | } 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/runtime/plugin-auto-outbound-tracking.client.ts: -------------------------------------------------------------------------------- 1 | import type { Plausible } from '@barbapapazes/plausible-tracker' 2 | import type {} from 'nuxt/app' 3 | import { defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from '#imports' 4 | import { useAutoOutboundTracking } from '@barbapapazes/plausible-tracker/extensions/auto-outbound-tracking' 5 | 6 | export default defineNuxtPlugin({ 7 | name: 'plausible:auto-outbound-tracking', 8 | setup() { 9 | const { plausible: options } = useRuntimeConfig().public 10 | const { $plausible } = useNuxtApp() 11 | 12 | if (options.enabled && options.autoOutboundTracking) { 13 | useAutoOutboundTracking($plausible as Plausible).install() 14 | } 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/runtime/plugin.client.ts: -------------------------------------------------------------------------------- 1 | import type {} from 'nuxt/app' 2 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 3 | import { createPlausibleTracker } from '@barbapapazes/plausible-tracker' 4 | 5 | export default defineNuxtPlugin({ 6 | name: 'plausible', 7 | setup() { 8 | const { plausible: options } = useRuntimeConfig().public 9 | 10 | if (!options.enabled) 11 | return 12 | 13 | const plausible = createPlausibleTracker({ 14 | ...options, 15 | logIgnored: options.logIgnoredEvents, 16 | domain: options.domain || window.location.hostname, 17 | apiHost: options.proxy ? options.proxyBaseEndpoint : options.apiHost, 18 | }) 19 | 20 | return { 21 | provide: { 22 | plausible, 23 | }, 24 | } 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/runtime/composables/useTrackEvent.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EventName, 3 | EventOptions, 4 | Plausible, 5 | } from '@barbapapazes/plausible-tracker' 6 | import { useNuxtApp } from '#imports' 7 | 8 | /** 9 | * Tracks a custom event 10 | * 11 | * @remarks 12 | * Track your defined goals by passing the goal's name as the argument `eventName`. 13 | * 14 | * @example 15 | * // Tracks the `signup` goal 16 | * useTrackEvent('signup') 17 | * 18 | * // Tracks the `Download` goal passing a `method` property. 19 | * useTrackEvent('Download', { props: { method: 'HTTP' } }) 20 | */ 21 | export function useTrackEvent(eventName: EventName, options?: EventOptions) { 22 | if (import.meta.client) { 23 | ;(useNuxtApp().$plausible as Plausible)?.trackEvent(eventName, options) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: pnpm/action-setup@v4 18 | - uses: actions/setup-node@v6 19 | with: 20 | node-version: 24 21 | - run: pnpm install 22 | - run: pnpm run dev:prepare 23 | - run: pnpm run lint 24 | 25 | typecheck: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v5 29 | - uses: pnpm/action-setup@v4 30 | - uses: actions/setup-node@v6 31 | with: 32 | node-version: 24 33 | - run: pnpm install 34 | - run: pnpm run dev:prepare 35 | - run: pnpm run test:types 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v6 21 | with: 22 | node-version: 24 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - name: Publish changelog 26 | run: npx changelogithub 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Install 31 | run: pnpm install 32 | 33 | - name: Build type stubs 34 | run: pnpm run dev:prepare 35 | 36 | - name: Publish to npm 37 | run: npm install -g npm@latest && npm publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /src/runtime/server/event-handler.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleOptions } from '../../module' 2 | import { useRuntimeConfig } from '#imports' 3 | import { createError, defineEventHandler, getRequestIP, proxyRequest } from 'h3' 4 | import { joinURL } from 'ufo' 5 | 6 | export default defineEventHandler((event) => { 7 | const config = useRuntimeConfig(event) 8 | const options = config.public.plausible as Required 9 | 10 | if (!options?.apiHost) { 11 | throw createError({ 12 | statusCode: 500, 13 | message: 'Plausible API host not configured', 14 | }) 15 | } 16 | 17 | try { 18 | const target = joinURL(options.apiHost, 'api/event') 19 | return proxyRequest(event, target, { 20 | headers: { 21 | 'X-Forwarded-For': getRequestIP(event, { xForwardedFor: true }), 22 | }, 23 | }) 24 | } 25 | catch (error) { 26 | console.error(error) 27 | 28 | throw createError({ 29 | statusCode: 502, 30 | message: 'Failed to proxy request to Plausible API', 31 | }) 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT Johann Schopplich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESLint flat config support 3 | "eslint.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use ESLint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto-fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto-fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "@stylistic/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable ESLint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 2 | import perfectionist from 'eslint-plugin-perfectionist' 3 | 4 | export default createConfigForNuxt({ 5 | features: { 6 | tooling: true, 7 | stylistic: true, 8 | }, 9 | dirs: { 10 | src: ['./playground'], 11 | }, 12 | }) 13 | // Workaround for https://github.com/eslint/eslint/issues/19134 14 | .append({ 15 | rules: { 16 | '@typescript-eslint/no-unused-expressions': 'off', 17 | }, 18 | }) 19 | .append({ 20 | plugins: { 21 | perfectionist, 22 | }, 23 | rules: { 24 | 'import/order': 'off', 25 | 'perfectionist/sort-exports': ['error', { order: 'asc', type: 'natural' }], 26 | 'perfectionist/sort-imports': ['error', { 27 | groups: [ 28 | 'type', 29 | ['parent-type', 'sibling-type', 'index-type'], 30 | 'builtin', 31 | 'external', 32 | ['internal', 'internal-type'], 33 | ['parent', 'sibling', 'index'], 34 | 'side-effect', 35 | 'object', 36 | 'unknown', 37 | ], 38 | newlinesBetween: 'ignore', 39 | order: 'asc', 40 | type: 'natural', 41 | }], 42 | 'perfectionist/sort-named-exports': ['error', { order: 'asc', type: 'natural' }], 43 | 'perfectionist/sort-named-imports': ['error', { order: 'asc', type: 'natural' }], 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxtjs/plausible", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "packageManager": "pnpm@10.15.0", 6 | "description": "Natively integrates Plausible Analytics into Nuxt", 7 | "author": "Johann Schopplich ", 8 | "license": "MIT", 9 | "homepage": "https://github.com/nuxt-modules/plausible#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/nuxt-modules/plausible.git" 13 | }, 14 | "bugs": "https://github.com/nuxt-modules/plausible/issues", 15 | "keywords": [ 16 | "analytics", 17 | "nuxt", 18 | "plausible" 19 | ], 20 | "exports": { 21 | ".": { 22 | "types": "./dist/types.d.mts", 23 | "default": "./dist/module.mjs" 24 | } 25 | }, 26 | "main": "./dist/module.mjs", 27 | "types": "./dist/types.d.mts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "scripts": { 32 | "prepack": "nuxt-module-build build", 33 | "dev": "nuxt dev playground", 34 | "dev:build": "nuxt build playground", 35 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground", 36 | "lint": "eslint .", 37 | "lint:fix": "eslint . --fix", 38 | "test:types": "tsc --noEmit", 39 | "release": "bumpp" 40 | }, 41 | "dependencies": { 42 | "@barbapapazes/plausible-tracker": "^0.5.6", 43 | "@nuxt/kit": "^4.0.3", 44 | "defu": "^6.1.4", 45 | "ufo": "^1.6.1" 46 | }, 47 | "devDependencies": { 48 | "@nuxt/eslint-config": "^1.9.0", 49 | "@nuxt/module-builder": "^1.0.2", 50 | "@nuxt/schema": "^4.0.3", 51 | "@types/node": "^24.3.0", 52 | "bumpp": "^10.2.3", 53 | "eslint": "^9.34.0", 54 | "eslint-plugin-perfectionist": "^4.15.0", 55 | "nuxt": "^4.0.3", 56 | "typescript": "^5.9.2", 57 | "vue-tsc": "^3.0.6" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addImports, 3 | addPlugin, 4 | addServerHandler, 5 | createResolver, 6 | defineNuxtModule, 7 | useLogger, 8 | } from '@nuxt/kit' 9 | import { defu } from 'defu' 10 | import { joinURL, withLeadingSlash } from 'ufo' 11 | import { name, version } from '../package.json' 12 | 13 | const DEFAULT_HOSTNAMES = ['localhost'] 14 | 15 | export interface ModuleOptions { 16 | /** 17 | * Whether the tracker shall be enabled. 18 | * 19 | * @default true 20 | */ 21 | enabled?: boolean 22 | 23 | /** 24 | * Whether page views shall be tracked when the URL hash changes. 25 | * 26 | * @remarks 27 | * Enable this if your Nuxt app has the `hashMode` router option enabled. 28 | * 29 | * @default false 30 | */ 31 | hashMode?: boolean 32 | 33 | /** 34 | * Whether events shall be tracked when running the site locally. 35 | * 36 | * @deprecated Please use `ignoredHostnames` instead. 37 | * @default false 38 | */ 39 | trackLocalhost?: boolean 40 | 41 | /** 42 | * Hostnames to ignore when tracking events. 43 | * 44 | * @default ['localhost'] 45 | */ 46 | ignoredHostnames?: string[] 47 | 48 | /** 49 | * Ignore the hostname if it is a subdomain of `ignoredHostnames`. 50 | * 51 | * @default false 52 | */ 53 | ignoreSubDomains?: boolean 54 | 55 | /** 56 | * The domain to bind tracking event to. 57 | * 58 | * @default window.location.hostname 59 | */ 60 | domain?: string 61 | 62 | /** 63 | * The API host where the events will be sent to. 64 | * 65 | * @default 'https://plausible.io' 66 | */ 67 | apiHost?: string 68 | 69 | /** 70 | * Track the current page and all further pages automatically. 71 | * 72 | * @remarks 73 | * Disable this if you want to manually manage pageview tracking. 74 | * 75 | * @default true 76 | */ 77 | autoPageviews?: boolean 78 | 79 | /** 80 | * Track all outbound link clicks automatically. 81 | * 82 | * @remarks 83 | * If enabled, a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) automagically detects link nodes throughout the application and binds `click` events to them. 84 | * 85 | * @default false 86 | */ 87 | autoOutboundTracking?: boolean 88 | 89 | /** 90 | * Log events to the console if they are ignored. 91 | * 92 | * @default false 93 | */ 94 | logIgnoredEvents?: boolean 95 | 96 | /** 97 | * Whether to proxy the event endpoint through the current origin. 98 | * 99 | * @default false 100 | */ 101 | proxy?: boolean 102 | 103 | /** 104 | * The base endpoint to proxy the Plausible event endpoint through. 105 | * 106 | * @remarks 107 | * When proxying is enabled, the frontend will send events to this endpoint instead of the Plausible API host. 108 | * 109 | * @default '/_plausible' 110 | */ 111 | proxyBaseEndpoint?: string 112 | } 113 | 114 | export default defineNuxtModule({ 115 | meta: { 116 | name, 117 | version, 118 | configKey: 'plausible', 119 | compatibility: { 120 | nuxt: '>=3', 121 | }, 122 | }, 123 | defaults: { 124 | enabled: true, 125 | hashMode: false, 126 | domain: '', 127 | ignoredHostnames: undefined, 128 | ignoreSubDomains: false, 129 | trackLocalhost: undefined, 130 | apiHost: 'https://plausible.io', 131 | autoPageviews: true, 132 | autoOutboundTracking: false, 133 | logIgnoredEvents: false, 134 | proxy: false, 135 | proxyBaseEndpoint: '/_plausible', 136 | }, 137 | setup(options, nuxt) { 138 | const logger = useLogger('plausible') 139 | const { resolve } = createResolver(import.meta.url) 140 | 141 | // Set default hostnames if `ignoredHostnames` is not set 142 | options.ignoredHostnames ??= [...DEFAULT_HOSTNAMES] 143 | 144 | // Dedupe `ignoredHostnames` items 145 | options.ignoredHostnames = Array.from(new Set(options.ignoredHostnames)) 146 | 147 | if (options.trackLocalhost !== undefined) { 148 | logger.warn('The `trackLocalhost` option has been deprecated. Please use `ignoredHostnames` instead.') 149 | } 150 | // Migrate `trackLocalhost` to `ignoredHostnames` 151 | else if (options.trackLocalhost) { 152 | options.ignoredHostnames = options.ignoredHostnames.filter( 153 | domain => domain !== 'localhost', 154 | ) 155 | } 156 | 157 | // Add module options to public runtime config 158 | nuxt.options.runtimeConfig.public.plausible = defu( 159 | nuxt.options.runtimeConfig.public.plausible as Required, 160 | options, 161 | ) 162 | 163 | // Transpile runtime 164 | nuxt.options.build.transpile.push(resolve('runtime')) 165 | 166 | if (nuxt.options.runtimeConfig.public.plausible.proxy) { 167 | const proxyBaseEndpoint = withLeadingSlash(nuxt.options.runtimeConfig.public.plausible.proxyBaseEndpoint) 168 | const hasUserProvidedProxyBase = [...nuxt.options.serverHandlers, ...nuxt.options.devServerHandlers].some(handler => handler.route?.startsWith(proxyBaseEndpoint)) 169 | 170 | if (hasUserProvidedProxyBase) { 171 | throw new Error(`The route \`${proxyBaseEndpoint}\` is already in use. Please use the \`proxyBaseEndpoint\` option to change the base URL of the proxy endpoint.`) 172 | } 173 | 174 | addServerHandler({ 175 | route: joinURL(proxyBaseEndpoint, 'api/event'), 176 | handler: resolve('runtime/server/event-handler'), 177 | method: 'post', 178 | }) 179 | } 180 | 181 | addImports( 182 | ['useTrackEvent', 'useTrackPageview'].map(name => ({ 183 | name, 184 | as: name, 185 | from: resolve(`runtime/composables/${name}`), 186 | })), 187 | ) 188 | 189 | addPlugin({ 190 | src: resolve('runtime/plugin.client'), 191 | mode: 'client', 192 | }) 193 | 194 | // Split plugins to reduce bundle size 195 | 196 | if (options.autoPageviews) { 197 | addPlugin({ 198 | src: resolve('runtime/plugin-auto-pageviews.client'), 199 | mode: 'client', 200 | order: 1, 201 | }) 202 | } 203 | 204 | if (options.autoOutboundTracking) { 205 | addPlugin({ 206 | src: resolve('runtime/plugin-auto-outbound-tracking.client'), 207 | mode: 'client', 208 | order: 2, 209 | }) 210 | } 211 | 212 | // Add preconnect link when proxy is not used 213 | if (!options.proxy) { 214 | addPlugin({ 215 | src: resolve('runtime/plugin-preconnect.client'), 216 | mode: 'client', 217 | order: -1, // Run early to add preconnect before other resources 218 | }) 219 | } 220 | }, 221 | }) 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nuxt Plausible module](./.github/og.png) 2 | 3 | # Nuxt Plausible 4 | 5 | [![npm version](https://img.shields.io/npm/v/@nuxtjs/plausible?color=a1b858&label=)](https://www.npmjs.com/package/@nuxtjs/plausible) 6 | 7 | Native integration of [Plausible Analytics](https://plausible.io/sites) for [Nuxt](https://nuxt.com). 8 | 9 | ## Features 10 | 11 | - 🌻 No configuration necessary 12 | - 📯 Track events and page views manually with [composables](#composables) 13 | - 🔀 Optional API proxy to avoid ad-blockers 14 | - 📂 [`.env` file support](#configuration) 15 | - 🧺 Sensible default options 16 | - 🦾 SSR-ready 17 | 18 | ## Setup 19 | 20 | ```bash 21 | npx nuxt module add plausible 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | Add `@nuxtjs/plausible` to the `modules` section of your Nuxt configuration: 27 | 28 | ```ts 29 | // `nuxt.config.ts` 30 | export default defineNuxtConfig({ 31 | modules: ['@nuxtjs/plausible'], 32 | }) 33 | ``` 34 | 35 | Done! Plausible will now run in your application's client. 36 | 37 | > [!TIP] 38 | > By default, `@nuxtjs/plausible` will use `window.location.hostname` for the Plausible `domain` configuration key, which should suit most use-cases. If you need to customize the domain, you can do so in the [module options](#module-options). 39 | 40 | ## Configuration 41 | 42 | All [supported module options](#module-options) can be configured using the `plausible` key in your Nuxt configuration: 43 | 44 | ```ts 45 | export default defineNuxtConfig({ 46 | modules: ['@nuxtjs/plausible'], 47 | 48 | plausible: { 49 | // Prevent tracking on localhost 50 | ignoredHostnames: ['localhost'], 51 | }, 52 | }) 53 | ``` 54 | 55 | > [!TIP] 56 | > To allow tracking events on localhost, set the `ignoredHostnames` option to an empty array. 57 | 58 | ### Runtime Config 59 | 60 | Alternatively, leveraging [automatically replaced public runtime config values](https://nuxt.com/docs/api/configuration/nuxt-config#runtimeconfig) by matching environment variables at runtime, set your desired option in your project's `.env` file: 61 | 62 | ```bash 63 | # Sets the `plausible.domain` option to `example.com` 64 | NUXT_PUBLIC_PLAUSIBLE_DOMAIN=example.com 65 | ``` 66 | 67 | With this setup, you can omit the `plausible` key in your Nuxt configuration. 68 | 69 | ### Proxy Configuration 70 | 71 | The module provides a proxy API feature that allows you to route Plausible events through your Nitro server instead of sending them directly to Plausible's servers. This is useful if you want to prevent ad blockers from blocking requests to Plausible's domain. When proxy is enabled, the tracker will automatically route requests through the current origin. 72 | 73 | To enable the proxy API, set the `proxy` option to `true`: 74 | 75 | ```ts 76 | export default defineNuxtConfig({ 77 | modules: ['@nuxtjs/plausible'], 78 | 79 | plausible: { 80 | proxy: true, 81 | }, 82 | }) 83 | ``` 84 | 85 | > [!NOTE] 86 | > When enabled, all Plausible events will be sent to your server first, which then forwards them to Plausible's API. The default proxy endpoint is `/_plausible`, but you can customize the path using the `proxyBaseEndpoint` module option. 87 | 88 | ## Module Options 89 | 90 | | Option | Type | Default | Description | 91 | | ---------------------- | ---------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 92 | | `enabled` | `boolean` | `true` | Whether the tracker shall be enabled. | 93 | | `hashMode` | `boolean` | `false` | Whether page views shall be tracked when the URL hash changes. Enable this if your Nuxt app uses the `hashMode` router option instead of the default history mode. | 94 | | `domain` | `string` | `'window.location.hostname'` | The domain to bind tracking event to. | 95 | | `ignoredHostnames` | `string[]` | `['localhost']` | Hostnames to ignore when tracking events. | 96 | | `ignoreSubDomains` | `boolean` | `false` | Ignore the hostname if it is a subdomain of `ignoredHostnames`. | 97 | | `apiHost` | `string` | `https://plausible.io` | The API host where the events will be sent to. | 98 | | `autoPageviews` | `boolean` | `true` | Track the current page and all further pages automatically. Disable this if you want to manually manage pageview tracking. | 99 | | `autoOutboundTracking` | `boolean` | `false` | Track all outbound link clicks automatically. If enabled, a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) automagically detects link nodes throughout the application and binds `click` events to them. | 100 | | `logIgnoredEvents` | `boolean` | `false` | Log events to the console if they are ignored. | 101 | | `proxy` | `boolean` | `false` | Whether to proxy the event endpoint through the current origin. | 102 | | `proxyBaseEndpoint` | `string` | `'/_plausible'` | The base endpoint to proxy the Plausible event endpoint through. | 103 | 104 | ## Composables 105 | 106 | As with other composables in the Nuxt ecosystem, they are auto-imported and can be used in your application's components. 107 | 108 | > [!NOTE] 109 | > Since the Plausible instance is available in the client only, executing the composables on the server will have no effect. 110 | 111 | ### `useTrackEvent` 112 | 113 | Track a custom event. Track your defined goals by passing the goal's name as the argument `eventName`. 114 | 115 | **Type Declarations** 116 | 117 | ```ts 118 | function useTrackEvent( 119 | eventName: string, 120 | options?: EventOptions, 121 | eventData?: PlausibleOptions, 122 | ): void 123 | ``` 124 | 125 | **Example** 126 | 127 | ```ts 128 | // Tracks the `signup` goal 129 | useTrackEvent('signup') 130 | 131 | // Tracks the `Download` goal passing a `method` property. 132 | useTrackEvent('Download', { props: { method: 'HTTP' } }) 133 | ``` 134 | 135 | ### `useTrackPageview` 136 | 137 | Manually track a page view. 138 | 139 | Pass optional event data to be sent with the `eventData` argument. Defaults to the current page's data merged with the default options provided during the Plausible initialization. 140 | 141 | **Type Declarations** 142 | 143 | ```ts 144 | function useTrackPageview( 145 | eventData?: PlausibleOptions, 146 | options?: EventOptions, 147 | ): void 148 | ``` 149 | 150 | ## 💻 Development 151 | 152 | 1. Clone this repository 153 | 2. Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 154 | 3. Install dependencies using `pnpm install` 155 | 4. Run `pnpm run dev:prepare` 156 | 5. Start development server using `pnpm run dev` 157 | 158 | ## Credits 159 | 160 | - [@Barbapapazes](https://github.com/Barbapapazes) for his [Plausible tracker rewrite](https://github.com/Barbapapazes/plausible-tracker) 161 | 162 | ## License 163 | 164 | [MIT](./LICENSE) License © 2022-PRESENT [Johann Schopplich](https://github.com/johannschopplich) 165 | --------------------------------------------------------------------------------