Skip to Content

demeter Component

demeter is a Surface Workflow component that fires Demeter SDK callbacks for Sophi Intelligence updates. It runs as a component workflow in parallel with the other components (article-body, gtm-datalayer), consuming the properties.page, properties.visitor, and properties.outcome produced by the parent Surface Workflow (see Website Surface Workflow) — it makes no decisions of its own. It follows the surface-fetch-edge recipe.

Like all Surface Workflow components, demeter is authored and configured inside MOS (the surface editor) per client, not part of the Cloudflare Worker repository. The code below is the reference implementation a client copies in and adapts.

Responsibilities

  • Inject the Sophi demeter init snippet (<script id="sophi-init">) so calls to demeter(...) queue safely in sophi.demeter.actions even before the real Demeter SDK has loaded.
  • Fire demeter("pageview", ...) — every page, article or section.
  • Fire demeter("wall", ...) — only when a wall is displayed on an article (outcome.wallVisibility === 'always' and outcome.wallType !== 'none' on an article page).
  • Fire demeter("userProperties", ...) — visitor type metadata on every page.

Output

  • A before content element: the sophi-init script, so window.demeter and its action queue exist ahead of anything else on the page that might call it.
  • after content elements: the callback script performing the pushes above, plus (when debug logging is on) a console.log summarizing this component’s inputs/outputs.

Behavior Notes

  • Section name: derived from the pathname’s first path segment (e.g. /news/some-article/article_xxx.htmlnews), defaulting to other when the pattern doesn’t match — see getSectionName below.
  • Path normalization: a leading // (protocol-relative syntax) is collapsed to a single / before parsing with the WHATWG URL API, which otherwise treats it as a network-path reference and either throws or silently drops part of the path.
  • Wall callback: only fires when the page is an article and the parent workflow’s outcome is wallVisibility: 'always' with a wallType other than none — section pages never fire it, regardless of outcome.

Extension Points

Section name extraction

  • Purpose: classify the current page into a section label for the pageview and wall callbacks.
  • Default starting point: getSectionName below assumes a /section/.../slug URL convention and takes the first path segment.
  • Status: a client whose CMS uses a different URL scheme (or that has section metadata available via resource.meta) replaces getSectionName with their own derivation.

Debug Logging

When env.ENABLE_LOGGING === 'debug', a console.log element is emitted before the callback script, summarizing this component’s inputs (page.pageType, outcome.wallVisibility/ wallType, visitor.visitorType), the derived flags (isArticle, section, effectiveWallType, firesWallCallback), and which callbacks the script below will fire.

Reference Implementation

// ─── Demeter Analytics Component ────────────────────────────────────────────── // // Fires Demeter SDK callbacks for Sophi Intelligence updates. Runs as a component // workflow in parallel with other components (article-body, gtm-datalayer). // // Callbacks fired: // demeter("pageview", ...) — every page, article or section // demeter("wall", ...) — only when a wall is displayed on an article // demeter("userProperties", ...) — visitor type metadata on every page type WallType = 'paywall' | 'regwall' | 'none'; type WebElement = { type: 'html'; content: string }; // Queues demeter(...) calls in sophi.demeter.actions until the real SDK loads and drains them. const sophiInitScript: WebElement = { type: 'html', content: `<script id="sophi-init"> window.sophi = window.sophi || {}; sophi.demeter = { actions: [], initStatus: "" }; // Lightweight lifecycle tracking helper window._s = function (c) { var d = window.sophi.demeter; var f = String(Math.floor(performance.now() / 10) / 100).replace( /^0\\./, ".", ); d.initStatus = (d.initStatus ? d.initStatus + "," : "") + c + f; }; window.demeter = window.demeter || function () { var resolve, reject; const s = new Promise((res, rej) => { resolve = res; reject = rej; }); sophi.demeter.actions.push([arguments, resolve, reject]); return s; }; _s("i"); </script>`, }; const workflow: SurfaceWorkflow = async ({ resource, surfaceProperties, env }) => { const debug = env.ENABLE_LOGGING === 'debug'; const path = resource.id || '/'; let pathname = '/'; try { // Collapse a leading "//" (protocol-relative syntax) into a single "/". // The WHATWG URL parser treats a leading "//" as a network-path // reference and parses whatever follows as a hostname — this either // throws (e.g. "//", "///") or silently drops part of the path // (e.g. "//foo/bar" → pathname "/", hostname "foo"). const normalized = typeof path === 'string' ? path.replace(/^\/{2,}/, '/') : path; pathname = new URL(normalized, 'https://example.com').pathname || '/'; } catch { pathname = typeof path === 'string' && path.startsWith('/') ? path : '/'; } const section = getSectionName(pathname); // ─── Read page info from Surface Workflow ───────────────────────────────── const page = (surfaceProperties?.page ?? {}) as Record<string, unknown>; const pageType = String(page.pageType ?? 'section'); const article = pageType === 'article'; // ─── Read visitor info from Surface Workflow ────────────────────────────── const visitor = (surfaceProperties?.visitor ?? {}) as Record<string, unknown>; const visitorType = String(visitor.visitorType ?? 'anonymous'); // ─── Determine effective wall type ──────────────────────────────────────── // Wall is only shown on article pages when wallVisibility is 'always' const outcome = (surfaceProperties?.outcome ?? {}) as Record<string, unknown>; const wallVisibility = String(outcome.wallVisibility ?? 'never'); const wallType = String(outcome.wallType ?? 'none'); const effectiveWallType: WallType = (article && wallVisibility === 'always' && wallType !== 'none') ? wallType as WallType : 'none'; const elements: WebElement[] = []; if (debug) { const payload = JSON.stringify({ component: 'demeter', surfaceProperties: { page: { pageType }, outcome: { wallVisibility, wallType }, visitor: { visitorType }, }, flags: { isArticle: article, section, effectiveWallType, firesWallCallback: effectiveWallType !== 'none' }, output: `pageview callback${effectiveWallType !== 'none' ? ' + wall callback' : ''} + userProperties callback`, }); elements.push({ type: 'html', content: `<script>console.log("[MOS Debug] demeter", ${payload});</script>` }); } elements.push(demeterScript(section, effectiveWallType, article, visitorType)); return { content: { before: [sophiInitScript], after: elements, }, }; }; // ─── Helpers ────────────────────────────────────────────────────────────────── /** * Extracts the section name from a URL pathname. * e.g. "/news/some-article/article_xxx.html" → "news" * Returns "other" if the pattern doesn't match. */ function getSectionName(pathname: string): string { const match = pathname.match(/^\/([^/]+)\//); return match ? match[1] : 'other'; } /** * Builds a `<script>` element that invokes the Demeter callback API. * * Fires: * demeter("pageview", { args: { article, section } }) — always * demeter("wall", { args: { wallType, section } }) — when wall is shown * demeter("userProperties", { args: { visitorType } }) — always (with visitor type) */ function demeterScript( section: string, wallType: WallType, article: boolean, visitorType?: string, ): WebElement { const wallCallback = wallType !== 'none' ? `\ndemeter("wall", { args: { wallType: ${JSON.stringify(wallType)}, section: ${JSON.stringify(section)} } });` : ''; const userPropertiesCallback = visitorType ? `\ndemeter("userProperties", { args: { visitorType: ${JSON.stringify(visitorType)} } });` : ''; return { type: 'html', content: `<script> demeter("pageview", { args: { article: ${article}, section: ${JSON.stringify(section)} } });${wallCallback}${userPropertiesCallback} </script>`, }; } export default workflow; export { getSectionName };
Last updated on