diff --git a/website/app.js b/website/app.js index 887e5f87..63cc48a0 100644 --- a/website/app.js +++ b/website/app.js @@ -34,14 +34,8 @@ function createAposConfig() { }, }, }, - // GTM setup and dependencies - '@apostrophecms/seo': { - options: { - googleTagManager: { - id: process.env.GOOGLE_TAG_MANAGER_ID, - }, - }, - }, + // Enable local SEO module with GTM integration + '@apostrophecms/seo': {}, '@apostrophecms/global': {}, // Make getEnv function available to templates '@apostrophecms/template': { diff --git a/website/modules/@apostrophecms/seo/index.js b/website/modules/@apostrophecms/seo/index.js index b4e8fada..e0bd649e 100644 --- a/website/modules/@apostrophecms/seo/index.js +++ b/website/modules/@apostrophecms/seo/index.js @@ -1,44 +1,20 @@ +const gtmUtils = require('./lib/gtm-utils'); + module.exports = { - improve: '@apostrophecms/seo', - init(self) { - // Ensure SEO components are injected into the template - self.apos.template.prepend('body', '@apostrophecms/seo:tagManagerBody'); - self.apos.template.append('head', '@apostrophecms/seo:tagManagerHead'); - self.apos.template.prepend('head', '@apostrophecms/seo:metaHead'); + options: { + googleTagManager: { + id: process.env.GOOGLE_TAG_MANAGER_ID, + }, }, + // No init hook required (layout renders components explicitly). components(self) { - /* - * Resolve GTM ID from global override or module options and validate it - */ - const sanitizeGtmId = (id) => { - const value = String(id || '').trim(); - if (/^gtm-[\da-z]+$/iu.test(value)) { - return value.toUpperCase(); - } - return ''; - }; - const resolveGtmId = (req) => { - const fromGlobal = req?.data?.global?.seoGoogleTagManager; - const fromOptions = self.options?.googleTagManager?.id; - const candidate = - (fromGlobal && String(fromGlobal).trim()) || - (fromOptions && String(fromOptions).trim()); - return sanitizeGtmId(candidate); - }; - + const getGtmId = (req) => gtmUtils.resolveGtmId(req, self.options); return { - metaHead(req, data) { - // Only on front-end page requests - if (!req?.data?.page) { - return {}; - } - return {}; - }, tagManagerBody(req, data) { if (!req?.data?.page) { return {}; } - const gtmId = resolveGtmId(req); + const gtmId = getGtmId(req); if (gtmId) { return { gtmId }; } @@ -48,7 +24,7 @@ module.exports = { if (!req?.data?.page) { return {}; } - const gtmId = resolveGtmId(req); + const gtmId = getGtmId(req); if (gtmId) { return { gtmId }; } diff --git a/website/modules/@apostrophecms/seo/lib/gtm-utils.js b/website/modules/@apostrophecms/seo/lib/gtm-utils.js new file mode 100644 index 00000000..4768430b --- /dev/null +++ b/website/modules/@apostrophecms/seo/lib/gtm-utils.js @@ -0,0 +1,21 @@ +const sanitizeGtmId = function (id) { + const value = String(id || '') + .trim() + .toUpperCase(); + // Strict GTM container format, e.g., "GTM-XXXXXXX" + if (/^GTM-[\dA-Z]+$/u.test(value)) { + return value; + } + return ''; +}; + +const resolveGtmId = function (req, options) { + const fromGlobal = req?.data?.global?.seoGoogleTagManager; + const fromOptions = options?.googleTagManager?.id; + const candidate = + (fromGlobal && String(fromGlobal).trim()) || + (fromOptions && String(fromOptions).trim()); + return sanitizeGtmId(candidate); +}; + +module.exports = { sanitizeGtmId, resolveGtmId }; diff --git a/website/package.json b/website/package.json index f9733e58..1b56d7e8 100644 --- a/website/package.json +++ b/website/package.json @@ -40,7 +40,6 @@ "dependencies": { "@apostrophecms/form": "^1.4.2", "@apostrophecms/import-export": "^3.2.0", - "@apostrophecms/seo": "^1.3.1", "@barba/core": "^2.10.3", "abort-controller": "^3.0.0", "apostrophe": "^4.17.0", diff --git a/website/views/layout.html b/website/views/layout.html index 7bc6d4a1..bfa12ac2 100644 --- a/website/views/layout.html +++ b/website/views/layout.html @@ -14,9 +14,11 @@ {# ✅ Add favicon in the
section #} {% block extraHead %} + {% component '@apostrophecms/seo:tagManagerHead' with data %} {% endblock %} {% block beforeMain %} + {% component '@apostrophecms/seo:tagManagerBody' with data %} Skip to content