diff --git a/build/build.js b/build/build.js
index 0de7868..9115551 100644
--- a/build/build.js
+++ b/build/build.js
@@ -80,6 +80,7 @@ async function buildAllPlugin() {
var plugins = [
{name: 'search', input: 'search/index.js'},
{name: 'ga', input: 'ga.js'},
+ {name: 'gtag', input: 'gtag.js'},
{name: 'matomo', input: 'matomo.js'},
{name: 'emoji', input: 'emoji.js'},
{name: 'external-script', input: 'external-script.js'},
diff --git a/docs/plugins.md b/docs/plugins.md
index 3db7ff5..dce1f4e 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -71,6 +71,8 @@ This plugin ignores diacritical marks when performing a full text search (e.g.,
## Google Analytics
+> Google's Universal Analytics service will no longer process new data in standard properties beginning July 1, 2023. Prepare now by setting up and switching over to a Google Analytics 4 property and docsify's gtag.js plugin.
+
Install the plugin and configure the track id.
```html
@@ -91,6 +93,31 @@ Configure by `data-ga`.
```
+## Google Analytics 4 (GA4)
+
+Install the plugin and configure the track id.
+
+```html
+
+
+
+```
+
## Emoji
Renders a larger collection of emoji shorthand codes. Without this plugin, Docsify is able to render only a limited number of emoji shorthand codes.
diff --git a/src/plugins/gtag.js b/src/plugins/gtag.js
new file mode 100644
index 0000000..aec713c
--- /dev/null
+++ b/src/plugins/gtag.js
@@ -0,0 +1,72 @@
+/* eslint-disable no-console */
+// From ./ga.js
+
+function appendScript(id) {
+ const script = document.createElement('script');
+ script.async = true;
+ script.src = 'https://www.googletagmanager.com/gtag/js?id=' + id;
+ document.body.appendChild(script);
+}
+
+// global site tag instance initialized
+function initGlobalSiteTag(id) {
+ appendScript(id);
+
+ window.dataLayer = window.dataLayer || [];
+ window.gtag =
+ window.gtag ||
+ function () {
+ window.dataLayer.push(arguments);
+ };
+
+ window.gtag('js', new Date());
+ window.gtag('config', id);
+}
+
+// add additional products to your tag
+// https://developers.google.com/tag-platform/gtagjs/install
+function initAdditionalTag(id) {
+ window.gtag('config', id);
+}
+
+function init(ids) {
+ if (Array.isArray(ids)) {
+ // set the first id to be a global site tag
+ initGlobalSiteTag(ids[0]);
+
+ // the rest ids
+ ids.forEach((id, index) => {
+ if (index > 0) {
+ initAdditionalTag(id);
+ }
+ });
+ } else {
+ initGlobalSiteTag(ids);
+ }
+}
+
+function collect() {
+ if (!window.gtag) {
+ init($docsify.gtag);
+ }
+
+ // usage: https://developers.google.com/analytics/devguides/collection/gtagjs/pages
+ window.gtag('event', 'page_view', {
+ /* eslint-disable camelcase */
+ page_title: document.title,
+ page_location: location.href,
+ page_path: location.pathname,
+ /* eslint-disable camelcase */
+ });
+}
+
+const install = function (hook) {
+ if (!$docsify.gtag) {
+ console.error('[Docsify] gtag is required.');
+ return;
+ }
+
+ hook.beforeEach(collect);
+};
+
+$docsify.plugins = [].concat(install, $docsify.plugins);
diff --git a/test/e2e/gtag.test.js b/test/e2e/gtag.test.js
new file mode 100644
index 0000000..c3ebbcd
--- /dev/null
+++ b/test/e2e/gtag.test.js
@@ -0,0 +1,97 @@
+// Modules, constants, and variables
+// npm run test:e2e gtag.test.js
+// -----------------------------------------------------------------------------
+const docsifyInit = require('../helpers/docsify-init');
+const { test, expect } = require('./fixtures/docsify-init-fixture');
+
+const gtagList = [
+ 'AW-YYYYYY', // Google Ads
+ 'DC-ZZZZZZ', // Floodlight
+ 'G-XXXXXX', // Google Analytics 4 (GA4)
+ 'UA-XXXXXX', // Google Universal Analytics (GA3)
+];
+
+// Suite
+// -----------------------------------------------------------------------------
+test.describe('Gtag Plugin Tests', () => {
+ // page request listened, print collect url
+ function pageRequestListened(page) {
+ page.on('request', request => {
+ if (request.url().indexOf('www.google-analytics.com') !== -1) {
+ // console.log(request.url());
+ }
+ });
+
+ page.on('response', response => {
+ const request = response.request();
+ // googleads.g.doubleclick.net
+ // www.google-analytics.com
+ // www.googletagmanager.com
+ const reg =
+ /googleads\.g\.doubleclick\.net|www\.google-analytics\.com|www\.googletagmanager\.com/g;
+ if (request.url().match(reg)) {
+ // console.log(request.url(), response.status());
+ }
+ });
+ }
+
+ // Tests
+ // ---------------------------------------------------------------------------
+ test('single gtag', async ({ page }) => {
+ pageRequestListened(page);
+
+ const docsifyInitConfig = {
+ config: {
+ gtag: gtagList[0],
+ },
+ scriptURLs: ['/lib/plugins/gtag.min.js'],
+ styleURLs: ['/lib/themes/vue.css'],
+ };
+
+ await docsifyInit({
+ ...docsifyInitConfig,
+ });
+
+ const $docsify = await page.evaluate(() => window.$docsify);
+
+ // Verify config options
+ expect(typeof $docsify).toEqual('object');
+
+ // console.log($docsify.gtag, $docsify.gtag === '');
+
+ // Tests
+ expect($docsify.gtag).not.toEqual('');
+ });
+
+ test('multi gtag', async ({ page }) => {
+ pageRequestListened(page);
+
+ const docsifyInitConfig = {
+ config: {
+ gtag: gtagList,
+ },
+ scriptURLs: ['/lib/plugins/gtag.min.js'],
+ styleURLs: ['/lib/themes/vue.css'],
+ };
+
+ await docsifyInit({
+ ...docsifyInitConfig,
+ });
+
+ const $docsify = await page.evaluate(() => window.$docsify);
+
+ // Verify config options
+ expect(typeof $docsify).toEqual('object');
+
+ // console.log($docsify.gtag, $docsify.gtag === '');
+
+ // Tests
+ expect($docsify.gtag).not.toEqual('');
+ });
+
+ test('data-ga attribute', async ({ page }) => {
+ pageRequestListened(page);
+
+ // TODO
+ });
+});