新增 GA4 / Meta Pixel 追蹤與 Cookie 同意機制

- 追蹤 ID 存 site_settings(空字串即關閉),於 /admin/settings 管理
- Base.astro 僅在用戶同意後載入分析工具,未同意前零請求
- 新增 /privacy 私隱政策頁與「重設 Cookie 偏好」按鈕
- 前台 SSR 頁面(首頁、blog、about、privacy)傳入 gaId / metaPixelId
- seed 加入預設空值,Footer 加入私隱政策連結
- 同步更新 AGENTS.md 與 README 說明
This commit is contained in:
2026-09-13 20:01:57 +08:00
parent 6dedb5469f
commit fbc562f495
13 changed files with 434 additions and 14 deletions
+173
View File
@@ -9,6 +9,8 @@ interface Props {
publishedTime?: Date | null;
image?: string;
jsonLd?: object | null | (object | null)[];
gaId?: string;
metaPixelId?: string;
}
const {
@@ -19,8 +21,12 @@ const {
publishedTime = null,
image,
jsonLd = null,
gaId = "",
metaPixelId = "",
} = Astro.props;
const hasAnalytics = Boolean(gaId || metaPixelId);
const siteName = SITE_NAME;
const origin = (Astro.site ?? Astro.url).toString().replace(/\/$/, "");
const canonical = new URL(Astro.url.pathname, origin + "/").toString();
@@ -61,6 +67,65 @@ const jsonLdHtml = jsonLdItems.length
{jsonLdHtml && <script type="application/ld+json" is:inline set:html={jsonLdHtml} />}
{
hasAnalytics && (
<script is:inline define:vars={{ gaId, metaPixelId }}>
window.__analyticsIds = { gaId, metaPixelId };
window.__loadAnalytics = function () {
var ids = window.__analyticsIds || {};
if (window.__analyticsLoaded) return;
window.__analyticsLoaded = true;
if (ids.gaId) {
window.dataLayer = window.dataLayer || [];
window.gtag = function () {
window.dataLayer.push(arguments);
};
window.gtag("js", new Date());
window.gtag("config", ids.gaId);
var gaScript = document.createElement("script");
gaScript.async = true;
gaScript.src =
"https://www.googletagmanager.com/gtag/js?id=" +
encodeURIComponent(ids.gaId);
document.head.appendChild(gaScript);
}
if (ids.metaPixelId) {
(function (f, b, e, v, n, t, s) {
if (f.fbq) return;
n = f.fbq = function () {
n.callMethod
? n.callMethod.apply(n, arguments)
: n.queue.push(arguments);
};
if (!f._fbq) f._fbq = n;
n.push = n;
n.loaded = true;
n.version = "2.0";
n.queue = [];
t = b.createElement(e);
t.async = true;
t.src = v;
s = b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t, s);
})(
window,
document,
"script",
"https://connect.facebook.net/en_US/fbevents.js",
);
window.fbq("init", ids.metaPixelId);
window.fbq("track", "PageView");
}
};
try {
if (window.localStorage.getItem("cookie_consent") === "granted") {
window.__loadAnalytics();
}
} catch (e) {}
</script>
)
}
<link rel="sitemap" href="/sitemap.xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -79,6 +144,58 @@ const jsonLdHtml = jsonLdItems.length
</head>
<body>
<slot />
{
hasAnalytics && (
<div id="cookie-consent" class="cookie-consent" hidden>
<p class="cookie-consent__text">
我哋用 Cookie 分析網站使用情況(Google Analytics 同 Meta),以改善服務。你可以選擇接受或拒絕。
</p>
<div class="cookie-consent__actions">
<a class="cookie-consent__link" href="/privacy">私隱政策</a>
<button class="cookie-consent__btn" type="button" data-consent="denied">
拒絕
</button>
<button
class="cookie-consent__btn cookie-consent__btn--primary"
type="button"
data-consent="granted"
>
接受
</button>
</div>
</div>
)
}
{
hasAnalytics && (
<script is:inline>
(function () {
var el = document.getElementById("cookie-consent");
if (!el) return;
var choice = null;
try {
choice = window.localStorage.getItem("cookie_consent");
} catch (e) {}
if (!choice) el.hidden = false;
el.addEventListener("click", function (event) {
var target = event.target;
var btn = target && target.closest
? target.closest("[data-consent]")
: null;
if (!btn) return;
var value = btn.getAttribute("data-consent");
try {
window.localStorage.setItem("cookie_consent", value);
} catch (e) {}
if (value === "granted" && typeof window.__loadAnalytics === "function") {
window.__loadAnalytics();
}
el.hidden = true;
});
})();
</script>
)
}
</body>
</html>
@@ -90,4 +207,60 @@ const jsonLdHtml = jsonLdItems.length
font-family: "Noto Sans HK", "PingFang HK", "Microsoft JhengHei", system-ui, -apple-system, sans-serif;
background: #faf8f4;
}
.cookie-consent[hidden] {
display: none;
}
.cookie-consent {
position: fixed;
inset: auto 0 0 0;
z-index: 1000;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px 24px;
padding: 16px 24px;
background: #faf8f4;
border-top: 1px solid #e5dfd6;
}
.cookie-consent__text {
margin: 0;
flex: 1 1 320px;
font-size: 13px;
line-height: 1.7;
color: #4a453f;
}
.cookie-consent__actions {
display: flex;
align-items: center;
gap: 12px;
}
.cookie-consent__link {
font-size: 13px;
color: #6e6862;
text-decoration: underline;
}
.cookie-consent__btn {
padding: 8px 20px;
font: inherit;
font-size: 13px;
font-weight: 700;
color: #1a1917;
background: transparent;
border: 1px solid #1a1917;
border-radius: 2px;
cursor: pointer;
}
.cookie-consent__btn--primary {
color: #fff;
background: #b45309;
border-color: #b45309;
}
</style>