Design: Meta Pixel + GA4 Conversion Tracking (Supplier App)

Date: 2026-06-17
Source spec: docs/metaTag-HomePage-Wordpress/meta-tag.md
App: yoyacoo_fe/supplier (only)
Tracking IDs (to be set in encrypted dotenv):

  • NEXT_PUBLIC_GA4_ID = G-K0D8YYJMPQ
  • NEXT_PUBLIC_META_PIXEL_ID = 1680138263295578

Goal

Fire Meta Pixel CompleteRegistration and GA4 sign_up (method: email) conversion events exactly once, only after a user has successfully completed email verification (本登録) on the supplier app. Do not fire on pre-registration (仮登録), normal login, page reload, or social signup.

Architecture

  • Base scripts loaded globally in _app.tsx via next/script (strategy="afterInteractive") — same as spec.
  • Conversion trigger is page-local logic on /content/create reading ?registered=1 query, gated by router.isReady and sessionStorage dedup flag.
  • Redirect flag set on /register success path — appends query: { registered: "1" } to the router.push call that lands on /content/create.
  • Existing GTM component (src/components/GoogleTagManager.tsx, uses NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID) is untouched — separate concern.

File-by-File Design

1. yoyacoo_fe/supplier/src/types/window.d.ts (NEW)

Ambient declaration:

export {};
declare global {
  interface Window {
    fbq?: (...args: unknown[]) => void;
  }
}
  • gtag is already typed via @types/gtag.js (already in package.json).
  • Placed in src/types/ to satisfy tsconfig path conventions.

2. yoyacoo_fe/supplier/src/pages/_app.tsx (MODIFY)

  • Add import Script from "next/script";
  • Add import { GoogleTagManager } from "@/components/GoogleTagManager"; (existing — keep rendering).
  • Compute ga4Id and pixelId from process.env.
  • Conditionally render 3 <Script> tags inside a <> fragment wrapping the existing tree (since MyApp returns a single <Provider> element today, switch to fragment). The three scripts:
    1. https://www.googletagmanager.com/gtag/js?id=${ga4Id} (loader)
    2. gtag('js', new Date()); gtag('config', '${ga4Id}') init
    3. Meta Pixel base snippet (fbq init + PageView)
  • Each script block guarded with if (ga4Id) / if (pixelId) so dev with no IDs doesn’t break.
  • Keep all existing behavior (auth init, error switch, layout) intact.

3. yoyacoo_fe/supplier/src/pages/register/index.tsx (MODIFY)

Two redirect sites — both must add registered=1:

  • Line ~174 (non-social, post-useRegister LoggedIn): currently

    void router.push({ pathname: NEXT_URL.CONTENT_CREATE });
    

    → change to include query: { registered: "1" }.

  • Line ~209 (social verify): currently

    await router.push(url);
    

    → spec literally doesn’t address social, but the existing url from socialApi.getRedirect is whatever the BE returns. Two options:

    • (a) append registered=1 ourselves to that URL
    • (b) ignore social path (spec scope = email 本登録)

    Decision: (b) — do NOT touch the social redirect. The spec explicitly says “Track on Email Verification (本登録)” and the source spec section 4’s trigger logic only mentions registered=1 from /register (which is the email-verification page). Social signup does not require email verification through the registerApi.postTemporary → email-link flow, so it is out of scope.

    However, the page-level useEffect on content/create will still safely ignore social logins because they will not arrive with ?registered=1 in the URL — sessionStorage flag is not set, but the registered=1 check fails first. No conflict.

4. yoyacoo_fe/supplier/src/pages/content/create/index.tsx (MODIFY)

  • Add useEffect at top of CreatePage component (after the existing useEffects for router.isReady).

  • The effect depends on router.isReady (re-run when it flips true) — re-check query once router is ready.

  • Inside the effect:

    1. If !router.isReady return.
    2. const registered = router.query.registered === "1".
    3. if (!registered || sessionStorage.getItem("complete_registration_tracked")) return.
    4. Fire window.fbq?.("track", "CompleteRegistration").
    5. Fire window.gtag?.("event", "sign_up", { method: "email" }).
    6. sessionStorage.setItem("complete_registration_tracked", "1").
    7. Strip the registered param via router.replace({ pathname: NEXT_URL.CONTENT_CREATE, query: omitRegistered(router.query) }, undefined, { shallow: true }). Using router.replace (not history.replaceState) is more idiomatic in Next.js; URL gets cleaned without triggering data refetch when shallow: true. (For full SSR safety, fall back to history.replaceState if router.replace causes side effects in the existing effect chain — see Verification step.)

    Decision: use router.replace with shallow: true first choice. If that triggers a re-render storm on dev, fall back to history.replaceState exactly as the spec prescribes.

Edge Cases

Case Behavior
Pre-registration (/new_register) submit No redirect to /content/create?registered=1, no event
Direct visit to /content/create registered query is undefined → no event
Reload of /content/create?registered=1 sessionStorage flag already set → no event
Normal login → lands on /content/create No registered=1 query → no event
Social signup (Google/Facebook) → lands on /content/create (via socialApi.getRedirect().url) No registered=1 query → no event
Dev with empty env vars Scripts don’t render; events silently no-op (defensive ?.() calls)
AdBlocker / no-script Guarded by typeof window.fbq === 'function' (only fire if loaded)
router.isReady is false on first render Effect re-runs once isReady becomes true
Page refreshed via back button from sub-page Effect re-runs; sessionStorage flag is per-tab — only this tab’s first landing counts

Out of Scope

  • Customer / admin apps (per user clarification)
  • Existing GoogleTagManager component
  • Server-side Conversion API
  • E2E automated test additions (manual verification only, per spec’s Testing section)

Verification

  1. cd yoyacoo_fe/supplier && yarn typecheck — must pass
  2. yarn analyse — must pass (lint + format)
  3. yarn build — must succeed
  4. Manual happy path (with real NEXT_PUBLIC_GA4_ID and NEXT_PUBLIC_META_PIXEL_ID set):
    • Fresh incognito → /new_register → submit email → click link in mailbox → fill /register form → submit → land on /content/create?registered=1
    • GA4 DebugView: sign_up event with method: email fires once
    • Meta Pixel Helper Chrome ext: CompleteRegistration fires once
    • Address bar: registered=1 is removed within a tick
  5. Manual negative cases:
    • Reload /content/create (with or without ?registered=1): no events
    • Direct visit to /content/create from bookmark: no events
    • Login as existing user → redirects to /mypage (or similar), no /content/create?registered=1 reached
    • Social signup flow: still works, no CompleteRegistration fires

Encrypted dotenv updates (manual step, requires DOTENV_PRIVATE_KEY)

The repo uses dotenvx to encrypt env values. Two files need new entries:

  • yoyacoo_fe/.dotenv/.env.staging.supplier
  • yoyacoo_fe/.dotenv/.env.production.supplier

Both already contain NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=encrypted:…. We add alongside:

NEXT_PUBLIC_GA4_ID=encrypted:<new>
NEXT_PUBLIC_META_PIXEL_ID=encrypted:<new>

Values to encrypt:

  • NEXT_PUBLIC_GA4_IDG-K0D8YYJMPQ
  • NEXT_PUBLIC_META_PIXEL_ID1680138263295578

This must be done by a developer who has the DOTENV_PRIVATE_KEY locally, using:

dotenvx set NEXT_PUBLIC_GA4_ID "G-K0D8YYJMPQ" -f .dotenv/.env.staging.supplier
dotenvx set NEXT_PUBLIC_META_PIXEL_ID "1680138263295578" -f .dotenv/.env.staging.supplier
# repeat for .env.production.supplier

CI/test files (.env.ci) and the unencrypted local files (.env.example, .env.development.local) also need updates so dev/test can run with the IDs:

  • .env.example: add the two lines with the real values as the contract doc.
  • .env.development.local: add the two lines with the real values for local dev.
  • .env.ci: add the two lines with the real values for CI (or test-only fake values, depending on team policy — confirm).

Note on Plaintext for NEXT_PUBLIC_*: The repo’s .dotenv/readme.md states “this repo is front-end only, do not put secrets that are dangerous if exposed — NEXT_PUBLIC_* vars are bundled into the client and effectively public.” Meta Pixel ID and GA4 ID are intentionally public (they appear in the client JS), so encrypting them is mostly for consistency with the existing repo convention. Both plaintext-in-local-env and encrypted-in-dotenv are acceptable. Decision: add to all four files (.env.example, .env.development.local, .dotenv/.env.staging.supplier, .dotenv/.env.production.supplier, .env.ci) with the real plaintext values where unencrypted, and the dotenvx-encrypted form where encrypted. The encryption step is a separate manual follow-up requiring the team’s private key.