From 15567ed71b516e335e4159c16122a6ae97f01f9f Mon Sep 17 00:00:00 2001 From: Andrei Lupu Date: Sun, 9 Mar 2025 00:38:05 +0200 Subject: [PATCH 1/4] Make Google Analytics configurable via environment variables This commit adds the ability to configure or disable Google Analytics tracking in self-hosted WordPress Playground instances through environment variables. The implementation uses Vite's template syntax for conditional inclusion at build time, ensuring no analytics code is included in the final HTML when analytics is disabled. Changes: - Update index.html to use Vite template conditionals for Google Analytics - Add .env and .env.example files with default configuration - Add CONFIGURATION.md with documentation on available options - Update .gitignore to handle local environment files properly - Add reference to configuration options in README.md --- .gitignore | 3 ++ README.md | 4 ++ packages/playground/website/.env.example | 6 +++ packages/playground/website/CONFIGURATION.md | 49 ++++++++++++++++++++ packages/playground/website/index.html | 6 ++- 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 packages/playground/website/.env.example create mode 100644 packages/playground/website/CONFIGURATION.md diff --git a/.gitignore b/.gitignore index 2c123c0b78..f0a36ab11f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ testem.log packages/playground/website/cypress/downloads vite.config.ts.timestamp-*.mjs +# Environment files - keep defaults but ignore local overrides +.env.local + # System Files .DS_Store Thumbs.db diff --git a/README.md b/README.md index c46ba313cd..9522f96384 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,10 @@ A browser should open and take you to your very own client-side WordPress at [ht Any changes you make to `.ts` files will be live-reloaded. Changes to `Dockerfile` require a full rebuild. +## Self-hosting WordPress Playground + +When self-hosting WordPress Playground, you may want to customize certain aspects like analytics tracking. See the [configuration documentation](packages/playground/website/CONFIGURATION.md) for details on available options and how to apply them. + From here, the [documentation](https://wordpress.github.io/wordpress-playground/) will help you learn how WordPress Playground works and how to use it to build amazing things! And here's a few more interesting CLI commands you can run in this repo: diff --git a/packages/playground/website/.env.example b/packages/playground/website/.env.example new file mode 100644 index 0000000000..3152beb2d8 --- /dev/null +++ b/packages/playground/website/.env.example @@ -0,0 +1,6 @@ +# WordPress Playground configuration +# Copy this file to .env to customize your local deployment + +# Google Analytics/GTM Configuration +# Leave empty to disable analytics +VITE_GOOGLE_ANALYTICS_ID=G-SVTNFCP8T7 \ No newline at end of file diff --git a/packages/playground/website/CONFIGURATION.md b/packages/playground/website/CONFIGURATION.md new file mode 100644 index 0000000000..ab484c5715 --- /dev/null +++ b/packages/playground/website/CONFIGURATION.md @@ -0,0 +1,49 @@ +# WordPress Playground Configuration + +This document outlines how to configure your self-hosted WordPress Playground instance. + +## Environment Variables + +WordPress Playground uses environment variables for configuration. These can be set in the following files: + +- `.env` - Default configuration (included in repository) +- `.env.local` - Local overrides (not committed to Git) + +## Available Configuration Options + +### Google Analytics + +The Google Analytics/GTM integration can be configured using: + +``` +VITE_GOOGLE_ANALYTICS_ID=your-ga4-id +``` + +To disable Google Analytics completely, set the value to an empty string: + +``` +VITE_GOOGLE_ANALYTICS_ID= +``` + +The Google Analytics script is conditionally included during the build process based on whether this environment variable has a value. This means no tracking script is included in the final HTML when the variable is empty, improving privacy and performance for self-hosted instances that don't require analytics. + +## How to Configure Your Self-Hosted Instance + +1. Clone the repository +2. Create a `.env.local` file with your custom configuration +3. Build the project according to the main README instructions + +Example `.env.local` file: + +``` +# Custom Google Analytics ID for my self-hosted instance +VITE_GOOGLE_ANALYTICS_ID=G-MYANALYTICS123 +``` + +## Building With Custom Configuration + +The environment variables are applied at build time. Make sure your custom `.env.local` file is in place before running: + +```bash +npm run build:website +``` diff --git a/packages/playground/website/index.html b/packages/playground/website/index.html index 91806487eb..a87159b39a 100644 --- a/packages/playground/website/index.html +++ b/packages/playground/website/index.html @@ -29,9 +29,10 @@ href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet" /> + <% if (import.meta.env.VITE_GOOGLE_ANALYTICS_ID) { %> + <% } %>
From 4d4146d9ec7d182b4530acdb8afc8ec64b48b384 Mon Sep 17 00:00:00 2001 From: Andrei Lupu Date: Sun, 9 Mar 2025 23:43:28 +0200 Subject: [PATCH 2/4] Inject GTM code based on the VITE_GOOGLE_ANALYTICS_ID .env variable. Adds a Vite plugin that silently injects Google Tag Manager code into HTML files during build when VITE_GOOGLE_ANALYTICS_ID is set. Plugin respects --verbose flag for detailed logging and maintains clean HTML formatting. --- packages/playground/website/CONFIGURATION.md | 24 +++- packages/playground/website/index.html | 14 -- .../playground/website/public/gutenberg.html | 14 +- .../playground/website/public/wordpress.html | 12 -- .../website/vite-analytics-plugin.ts | 134 ++++++++++++++++++ packages/playground/website/vite.config.ts | 5 + 6 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 packages/playground/website/vite-analytics-plugin.ts diff --git a/packages/playground/website/CONFIGURATION.md b/packages/playground/website/CONFIGURATION.md index ab484c5715..fed0c1371c 100644 --- a/packages/playground/website/CONFIGURATION.md +++ b/packages/playground/website/CONFIGURATION.md @@ -25,7 +25,14 @@ To disable Google Analytics completely, set the value to an empty string: VITE_GOOGLE_ANALYTICS_ID= ``` -The Google Analytics script is conditionally included during the build process based on whether this environment variable has a value. This means no tracking script is included in the final HTML when the variable is empty, improving privacy and performance for self-hosted instances that don't require analytics. +The Google Analytics script is automatically injected into the `` section of all HTML pages during the build process. If the environment variable is not set or is empty, no analytics code will be included in the final HTML output, improving privacy and performance for self-hosted instances that don't require tracking. + +This configuration applies to: + +- The main application (`index.html`) +- The WordPress PR previewer (`public/wordpress.html`) +- The Gutenberg PR previewer (`public/gutenberg.html`) +- All demo and builder HTML files ## How to Configure Your Self-Hosted Instance @@ -45,5 +52,20 @@ VITE_GOOGLE_ANALYTICS_ID=G-MYANALYTICS123 The environment variables are applied at build time. Make sure your custom `.env.local` file is in place before running: ```bash +# Standard build npm run build:website + +# Verbose build with analytics logging +npm run build:website -- --verbose ``` + +## Technical Implementation + +The analytics integration uses a custom Vite plugin that inserts the Google Analytics script at the end of the `` section in all HTML files during the build process. This approach: + +1. Keeps analytics configuration separate from the code +2. Ensures no analytics code is included in the HTML when disabled +3. Requires no placeholder comments in the HTML source files +4. Provides a clean way to customize analytics for self-hosted instances +5. Maintains clean indentation and formatting in the output HTML +6. Operates silently by default (logs can be enabled with `--verbose`) diff --git a/packages/playground/website/index.html b/packages/playground/website/index.html index a87159b39a..1228b5f530 100644 --- a/packages/playground/website/index.html +++ b/packages/playground/website/index.html @@ -29,20 +29,6 @@ href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet" /> - <% if (import.meta.env.VITE_GOOGLE_ANALYTICS_ID) { %> - - - <% } %>
diff --git a/packages/playground/website/public/gutenberg.html b/packages/playground/website/public/gutenberg.html index 4fbdf15c5c..35078ca979 100644 --- a/packages/playground/website/public/gutenberg.html +++ b/packages/playground/website/public/gutenberg.html @@ -22,18 +22,6 @@ href="https://fonts.googleapis.com/css?family=Noto+Serif:400,700" /> - - @@ -164,7 +152,7 @@ step: 'login', username: 'admin', password: 'password', - } + }, ], }; // If there's a import-site query parameter, pass that to the blueprint diff --git a/packages/playground/website/public/wordpress.html b/packages/playground/website/public/wordpress.html index a005b3ea1d..ff7e0e9754 100644 --- a/packages/playground/website/public/wordpress.html +++ b/packages/playground/website/public/wordpress.html @@ -21,18 +21,6 @@ href="https://fonts.googleapis.com/css?family=Noto+Serif:400,700" /> - -
diff --git a/packages/playground/website/vite-analytics-plugin.ts b/packages/playground/website/vite-analytics-plugin.ts new file mode 100644 index 0000000000..a0ccd3d4d4 --- /dev/null +++ b/packages/playground/website/vite-analytics-plugin.ts @@ -0,0 +1,134 @@ +import { Plugin } from 'vite'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// Plugin options interface +interface AnalyticsPluginOptions { + verbose?: boolean; +} + +/** + * Vite plugin to inject Google Analytics into the head tag of HTML files + * + * @param options Plugin options + * @returns {Plugin} A Vite plugin that processes HTML files during build + */ +export function analyticsInjectionPlugin( + options: AnalyticsPluginOptions = {} +): Plugin { + // Default options + const { verbose = false } = options; + + // Shared analytics script template + const getAnalyticsScript = (id: string) => { + return ` + + +`; + }; + + // Helper function for conditional logging + const log = (msg: string, isError = false) => { + // Only log if it's an error or verbose mode is enabled + if (isError || verbose) { + isError ? console.error(msg) : console.log(msg); + } + }; + + return { + name: 'vite-plugin-analytics-injection', + apply: 'build', // Only apply during build, not dev + + writeBundle(options, bundle) { + const googleAnalyticsId = process.env.VITE_GOOGLE_ANALYTICS_ID; + + if (!googleAnalyticsId) { + log( + 'Google Analytics disabled - no tracking will be added to HTML files.' + ); + return; + } + + log('Processing HTML files for Google Analytics injection...'); + + // Files to process - include all HTML files that need analytics + const htmlFiles = [ + 'index.html', + 'wordpress.html', + 'gutenberg.html', + ]; + const outputDir = options.dir || ''; + + let processedCount = 0; + let skippedCount = 0; + let notFoundCount = 0; + + htmlFiles.forEach((htmlFile) => { + const outputPath = join(outputDir, htmlFile); + + if (existsSync(outputPath)) { + log(`Processing ${htmlFile} for analytics...`); + + try { + // Read file + let content = readFileSync(outputPath, 'utf8'); + + // Check if the file already has analytics (to avoid duplicate injection) + if ( + content.includes( + `gtag('config', '${googleAnalyticsId}')` + ) + ) { + log( + `Analytics already present in ${htmlFile}, skipping.` + ); + skippedCount++; + return; + } + + // Find the closing head tag + const headCloseIndex = content.indexOf(''); + if (headCloseIndex === -1) { + log( + `Could not find tag in ${htmlFile}, skipping.`, + true + ); + skippedCount++; + return; + } + + // Insert the analytics script right before the closing head tag + const analyticsScript = + getAnalyticsScript(googleAnalyticsId); + const updatedContent = + content.substring(0, headCloseIndex) + + analyticsScript + + content.substring(headCloseIndex); + + // Write back + writeFileSync(outputPath, updatedContent, 'utf8'); + log(`Successfully injected analytics into ${htmlFile}`); + processedCount++; + } catch (error) { + log(`Error processing ${htmlFile}: ${error}`, true); + } + } else { + log(`File not found in build directory: ${outputPath}`); + notFoundCount++; + } + }); + + // Always show summary even in non-verbose mode + if (processedCount > 0) { + log( + `Analytics injection: ${processedCount} files processed, ${skippedCount} skipped, ${notFoundCount} not found.` + ); + } + }, + }; +} diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index b973bb0c06..532ab8e125 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -22,6 +22,7 @@ import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; import { listAssetsRequiredForOfflineMode } from '../../vite-extensions/vite-list-assets-required-for-offline-mode'; import { addManifestJson } from '../../vite-extensions/vite-manifest'; import virtualModule from '../../vite-extensions/vite-virtual-module'; +import { analyticsInjectionPlugin } from './vite-analytics-plugin'; const proxy: CommonServerOptions['proxy'] = { '^/plugin-proxy': { @@ -40,6 +41,9 @@ export default defineConfig(({ command, mode }) => { ? 'https://wordpress-playground-cors-proxy.net/?' : 'http://127.0.0.1:5263/cors-proxy.php?'; + // Check for verbose mode + const isVerbose = process.argv.includes('--verbose'); + return { // Split traffic from this server on dev so that the iframe content and // outer content can be served from the same origin. In production it's @@ -77,6 +81,7 @@ export default defineConfig(({ command, mode }) => { }, }, plugins: [ + analyticsInjectionPlugin({ verbose: isVerbose }), react({ jsxRuntime: 'automatic', }), From bab424ca98f03a5c0a5f2aede70a990419d6c80d Mon Sep 17 00:00:00 2001 From: Andrei Lupu Date: Mon, 10 Mar 2025 01:13:20 +0200 Subject: [PATCH 3/4] Fix lint issues --- packages/playground/website/.env.example | 2 +- packages/playground/website/vite-analytics-plugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playground/website/.env.example b/packages/playground/website/.env.example index 3152beb2d8..6f35cff547 100644 --- a/packages/playground/website/.env.example +++ b/packages/playground/website/.env.example @@ -3,4 +3,4 @@ # Google Analytics/GTM Configuration # Leave empty to disable analytics -VITE_GOOGLE_ANALYTICS_ID=G-SVTNFCP8T7 \ No newline at end of file +VITE_GOOGLE_ANALYTICS_ID=G-SVTNFCP8T7 \ No newline at end of file diff --git a/packages/playground/website/vite-analytics-plugin.ts b/packages/playground/website/vite-analytics-plugin.ts index a0ccd3d4d4..067cb3217e 100644 --- a/packages/playground/website/vite-analytics-plugin.ts +++ b/packages/playground/website/vite-analytics-plugin.ts @@ -76,7 +76,7 @@ export function analyticsInjectionPlugin( try { // Read file - let content = readFileSync(outputPath, 'utf8'); + const content = readFileSync(outputPath, 'utf8'); // Check if the file already has analytics (to avoid duplicate injection) if ( From 61f5338f91830e0e2fa0211adfe55065b1e1db77 Mon Sep 17 00:00:00 2001 From: Andrei Lupu Date: Thu, 3 Apr 2025 11:05:51 +0300 Subject: [PATCH 4/4] Add a Playwright test for GTM tag, --- .../website/playwright/e2e/website-ui.spec.ts | 13 +++++++++++++ .../playground/website/vite-analytics-plugin.ts | 1 + 2 files changed, 14 insertions(+) diff --git a/packages/playground/website/playwright/e2e/website-ui.spec.ts b/packages/playground/website/playwright/e2e/website-ui.spec.ts index 329cb09769..52134ed818 100644 --- a/packages/playground/website/playwright/e2e/website-ui.spec.ts +++ b/packages/playground/website/playwright/e2e/website-ui.spec.ts @@ -262,3 +262,16 @@ test('should keep query arguments when updating settings', async ({ await wordpress.locator('body').evaluate((body) => body.baseURI) ).toMatch('/wp-admin/'); }); + +test('should not load GTM code when VITE_GOOGLE_ANALYTICS_ID is missing', async ({ + website, +}) => { + await website.goto('./'); + + // By default, the VITE_GOOGLE_ANALYTICS_ID is not set, so GTM should not be loaded + // Check if GTM script is not present in the head + const gtmScript = await website.page + .locator('script[src*="googletagmanager.com"]') + .count(); + expect(gtmScript).toBe(0); +}); diff --git a/packages/playground/website/vite-analytics-plugin.ts b/packages/playground/website/vite-analytics-plugin.ts index 067cb3217e..6a3a8f390e 100644 --- a/packages/playground/website/vite-analytics-plugin.ts +++ b/packages/playground/website/vite-analytics-plugin.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { Plugin } from 'vite'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path';