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-K0D8YYJMPQNEXT_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.tsxvianext/script(strategy="afterInteractive") — same as spec. - Conversion trigger is page-local logic on
/content/createreading?registered=1query, gated byrouter.isReadyandsessionStoragededup flag. - Redirect flag set on
/registersuccess path — appendsquery: { registered: "1" }to therouter.pushcall that lands on/content/create. - Existing GTM component (
src/components/GoogleTagManager.tsx, usesNEXT_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;
}
}
gtagis already typed via@types/gtag.js(already inpackage.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
ga4IdandpixelIdfromprocess.env. - Conditionally render 3
<Script>tags inside a<>fragment wrapping the existing tree (sinceMyAppreturns a single<Provider>element today, switch to fragment). The three scripts:https://www.googletagmanager.com/gtag/js?id=${ga4Id}(loader)gtag('js', new Date()); gtag('config', '${ga4Id}')init- 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-
useRegisterLoggedIn): currentlyvoid 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
urlfromsocialApi.getRedirectis whatever the BE returns. Two options:- (a) append
registered=1ourselves 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=1from/register(which is the email-verification page). Social signup does not require email verification through theregisterApi.postTemporary→ email-link flow, so it is out of scope.However, the page-level
useEffectoncontent/createwill still safely ignore social logins because they will not arrive with?registered=1in the URL —sessionStorageflag is not set, but theregistered=1check fails first. No conflict.- (a) append
4. yoyacoo_fe/supplier/src/pages/content/create/index.tsx (MODIFY)
Add
useEffectat top ofCreatePagecomponent (after the existinguseEffects forrouter.isReady).The effect depends on
router.isReady(re-run when it flips true) — re-check query once router is ready.Inside the effect:
- If
!router.isReadyreturn. const registered = router.query.registered === "1".if (!registered || sessionStorage.getItem("complete_registration_tracked")) return.- Fire
window.fbq?.("track", "CompleteRegistration"). - Fire
window.gtag?.("event", "sign_up", { method: "email" }). sessionStorage.setItem("complete_registration_tracked", "1").- Strip the
registeredparam viarouter.replace({ pathname: NEXT_URL.CONTENT_CREATE, query: omitRegistered(router.query) }, undefined, { shallow: true }). Usingrouter.replace(nothistory.replaceState) is more idiomatic in Next.js; URL gets cleaned without triggering data refetch whenshallow: true. (For full SSR safety, fall back tohistory.replaceStateif router.replace causes side effects in the existing effect chain — see Verification step.)
Decision: use
router.replacewithshallow: truefirst choice. If that triggers a re-render storm on dev, fall back tohistory.replaceStateexactly as the spec prescribes.- If
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
GoogleTagManagercomponent - Server-side Conversion API
- E2E automated test additions (manual verification only, per spec’s Testing section)
Verification
cd yoyacoo_fe/supplier && yarn typecheck— must passyarn analyse— must pass (lint + format)yarn build— must succeed- Manual happy path (with real
NEXT_PUBLIC_GA4_IDandNEXT_PUBLIC_META_PIXEL_IDset):- Fresh incognito →
/new_register→ submit email → click link in mailbox → fill/registerform → submit → land on/content/create?registered=1 - GA4 DebugView:
sign_upevent withmethod: emailfires once - Meta Pixel Helper Chrome ext:
CompleteRegistrationfires once - Address bar:
registered=1is removed within a tick
- Fresh incognito →
- Manual negative cases:
- Reload
/content/create(with or without?registered=1): no events - Direct visit to
/content/createfrom bookmark: no events - Login as existing user → redirects to
/mypage(or similar), no/content/create?registered=1reached - Social signup flow: still works, no
CompleteRegistrationfires
- Reload
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.supplieryoyacoo_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_ID→G-K0D8YYJMPQNEXT_PUBLIC_META_PIXEL_ID→1680138263295578
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.