新增 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
+1
View File
@@ -25,6 +25,7 @@ Astro 7 + React 19 + Chakra UI v3,部署在 Cloudflare Workers(單一 Worker
- **`wrangler.jsonc` 有佔位 id**`database_id` / KV `id` 要先用 `db:create` / `kv:create` 產生再貼上,未貼前無法部署。R2 bucket 唔使貼 id,用 `r2:create`(或 `npm run setup`)建立即可,binding 係 `MEDIA`
- **圖片上傳**:圖片存 Cloudflare R2binding `MEDIA`、bucket `yingfung-solar-media`),由同一個 Worker 的 `/media/*` 出圖;上傳經 `/admin/upload`client 端先縮圖轉 WebP。本機靠 `platformProxy` 模擬 R2,雲端要先 `npm run r2:create`。圖片欄位仍可貼 URL;換圖/刪除時 `lib/media.ts` 會清舊 R2 檔。
- **上線前要改 `astro.config.mjs``site`**sitemap / canonical / OG 全部靠它(現為 `https://example.com`)。
- **追蹤分析(GA4 / Meta Pixel**ID 存 `site_settings``ga4_measurement_id` / `meta_pixel_id`**空字串=關閉**),喺 `/admin/settings` 改。`Base.astro` 只會喺用戶同意(`localStorage.cookie_consent`)之後先載入,未同意前唔會有追蹤請求/Cookie;`/privacy` 有說明同「重設 Cookie 偏好」。因為係通用 key-value**冇新 migration**,只需 `scripts/seed.sql` 有 default。
- **後台認證**`src/middleware.ts` 保護所有 `/admin`,用 HMAC-signed cookie`src/lib/auth.ts`);未設 `ADMIN_PASSWORD`(本機在 `.dev.vars`,已 gitignore)會回 503。
- **Chakra v3 + Emotion**:前台 React 元件要用 `Provider` / `SiteChrome`(內含 `<ChakraProvider value={system}>`),theme 在 `src/theme/system.ts`。Astro 以 `client:load` 掛 island。
- **語言**:UI 文案、註解、後台全部都係繁體中文(廣東話)——新增內容請保持一致;網站只做中文,不做雙語。
+27 -8
View File
@@ -50,21 +50,38 @@ npm run dev
## 部署到 Cloudflare
### 首次設定(一鍵
### 1. 先設定網址(部署前一定要做
`sitemap` / `canonical` / `OG` 全部靠 `astro.config.mjs``site`,所以**先改佢**
```js
// astro.config.mjs
site: "https://your-domain.com",
```
想用 Cloudflare 自訂網域(例如 `solar.develop-cat.com`)就順手喺 `wrangler.jsonc` 加:
```jsonc
"routes": [
{ "pattern": "solar.develop-cat.com", "custom_domain": true }
],
```
部署時會自動建立 DNS + TLS,唔使自己開 record。唔加就淨係用 `https://<worker-name>.<account>.workers.dev`;亦可以部署後喺 Dashboard → Worker → Settings → Domains & Routes 手動加。
### 2. 首次設定(一鍵)
```bash
npm run login # 瀏覽器授權
npm run setup # 建 D1 / KV / R2、寫入 wrangler.jsonc、設 secret、 migration + seed
npm run setup # 建 D1 / KV / R2、寫入 wrangler.jsonc 嘅 id、設 secret、可選 migration + seed + build + deploy
```
`npm run setup` 會逐步問你:要唔要設定後台密碼、要唔要套 migration/seed、要唔要 build + deploy`astro.config.mjs``site``https://example.com`,會跳過 build + deploy 並提醒你改。
`npm run setup` 會逐步問你:設定後台密碼套 migration/seedbuild + deploy`site` 仍係 `https://example.com`,會跳過 build + deploy 並提醒你改(改完再 `npm run deploy`
### 之後更新
改完 `astro.config.mjs``site` 做真域名後:
### 3. 之後更新
```bash
npm run deploy
npm run deploy # = npm run build + wrangler deploy
```
### Git push 自動部署(Cloudflare Workers Builds
@@ -76,6 +93,7 @@ npm run deploy
**Migration 唔會喺 CI 自動執行**:改完 `src/db/schema.ts` 要手動跑
`npm run db:generate``npm run db:migrate`(雲端)→ `npm run db:seed`(如有新 seed)。
`seed.sql` 放喺 `scripts/`(唔可以放喺 `migrations/`,否則 `db:migrate` 會連 seed 一齊種)。
## AI 生成 Blog
@@ -110,12 +128,13 @@ src/
├─ layouts/AdminLayout.astro 後台外框
├─ pages/
│ ├─ index.astro SSR 首頁
│ ├─ about.astro 關於(靜態)
│ ├─ blog/… Blog
│ ├─ admin/… 後台(含 admin/ai、admin/upload 上傳 endpoint
│ ├─ media/[...key].ts R2 圖片出圖 endpoint
│ └─ sitemap.xml.ts / robots.txt.ts
├─ components/admin/ImageField.astro 後台圖片欄(URL + 上傳 + 預覽)
├─ lib/{auth,db,env,form,media,ai,markdown}.ts
├─ lib/{auth,db,env,form,media,ai,markdown,schema-org}.ts
migrations/
├─ 0000_init.sql … Drizzle migrations
scripts/
+3 -1
View File
@@ -19,7 +19,9 @@ INSERT OR REPLACE INTO site_settings (key, value, updated_at) VALUES
('hero_image', 'https://images.unsplash.com/photo-1509391366360-2e959784a276?auto=format&fit=crop&w=1600&q=80', strftime('%s','now')*1000),
('seo_default_title', '盈豐太陽能工程有限公司 — 香港村屋太陽能一站式服務', strftime('%s','now')*1000),
('seo_default_description', '專營香港村屋太陽能系統:現場評估、合法支架、代辦申請、專業安裝及驗收認證。透明報價,專人跟進,幫你善用天台賺取發電回報。', strftime('%s','now')*1000),
('og_image', 'https://images.unsplash.com/photo-1509391366360-2e959784a276?auto=format&fit=crop&w=1200&q=80', strftime('%s','now')*1000);
('og_image', 'https://images.unsplash.com/photo-1509391366360-2e959784a276?auto=format&fit=crop&w=1200&q=80', strftime('%s','now')*1000),
('ga4_measurement_id', '', strftime('%s','now')*1000),
('meta_pixel_id', '', strftime('%s','now')*1000);
INSERT OR REPLACE INTO site_settings (key, value, updated_at) VALUES
('ai_enabled', '1', strftime('%s','now')*1000),
+11 -3
View File
@@ -8,7 +8,7 @@
- 擁有 `src/` 全層,亦直接擁有以下冇獨立 child doc 的部分:
- `theme/system.ts` — Chakra systemtokens / semanticTokens / fonts / globalCss),前台顏色同字體的唯一來源。現行設計:editorial 極簡風(暖米白 `#FAF8F4` 底、琥珀金 `brand` accent、墨黑 `ink`、Noto Serif HK 標題),詳見 `docs/superpowers/specs/2026-09-11-editorial-redesign-design.md`
- `layouts/Base.astro` — 前台 SEO headcanonical / OG / Twitter / theme-color / JSON-LD `jsonLd` prop / sitemap link)+ Noto Sans HK(非阻塞載入,`media="print" onload` + `<noscript>` fallback)。
- `layouts/Base.astro` — 前台 SEO headcanonical / OG / Twitter / theme-color / JSON-LD `jsonLd` prop / sitemap link)+ Noto Sans HK(非阻塞載入,`media="print" onload` + `<noscript>` fallback+ 同意後先載入嘅 analytics(`gaId` / `metaPixelId` props,見下面 Analytics & consent
- `layouts/AdminLayout.astro` — 後台外框,純 CSS、與前台一致的色系。
- `config.ts` — build-time 靜態頁用的 `SITE_NAME` / `SITE_TAGLINE`(可管理內容一律放 D1,唔放呢度)。
- `middleware.ts` — 保護 `/admin`:設 `locals.isAdmin`,無 `ADMIN_PASSWORD` 回 503,未登入導向 `/admin/login`
@@ -28,8 +28,8 @@
### Routes & SSR`pages/`
- **預設靜態**`astro.config.mjs``output: "static"`。要讀 D1 或 request-time 資料的頁面/endpoint 必須在檔案內寫 `export const prerender = false`
- 目前 SSR`index.astro``blog/index.astro``blog/[slug].astro``admin/**``media/[...key].ts``sitemap.xml.ts`
- 保持靜態:`about.astro``robots.txt.ts`
- 目前 SSR`index.astro``about.astro``privacy.astro``blog/index.astro``blog/[slug].astro``admin/**``media/[...key].ts``sitemap.xml.ts`
- 保持靜態:`robots.txt.ts`
- **環境變數**:只用 `getEnv()`,且只可喺 `prerender = false` 的頁面/endpoint 用。
- **前台動態頁**:讀 D1 → 傳 props 畀單一 React island`client:idle`,內容仍 SSR 出 HTML),並設邊緣快取 `Cache-Control: public, s-maxage=60, stale-while-revalidate=300`
- **404**`blog/[slug].astro` 找唔到文章 → 設 `Astro.response.status = 404` 並 render noindex 頁。
@@ -38,6 +38,14 @@
- **AI 生成(同步)**`/admin/ai` 的生成喺 admin POST 內同步 `await` 完成先 redirect。如日後要改做背景處理,可用 `Astro.locals.cfContext`Cloudflare `ExecutionContext`)嘅 `waitUntil`,唔使改現有流程。
- **SEO**:用 `Base.astro`canonical / OG / sitemap 全部靠 `astro.config.mjs``site`(現為佔位 `https://example.com`,上線前要改)。所有頁面都要有單一 `h1`section 標題用 `h2`、項目用 `h3`,唔可以跳級(heading 語意一律用 Chakra `as`,唔可以淨靠字級)。JSON-LD 由 `lib/schema-org.ts` 砌,經 `Base.astro``jsonLd` prop 輸出:首頁 `LocalBusiness` + `FAQPage`、文章 `BlogPosting` + `BreadcrumbList`
### Analytics & consent
- **ID 放 D1**GA4 / Meta Pixel 的 ID 存 `site_settings``ga4_measurement_id` / `meta_pixel_id`admin `/admin/settings` 可改);**空字串=唔載入**,冇 `enabled` boolean。因為係通用 key-value**唔使 migration**,只需 `scripts/seed.sql` 有 default。
- **注入方式**`Base.astro` 收 optional props `gaId` / `metaPixelId`;只有 SSR 頁讀到 D1 再傳落去(靜態頁要傳就必須轉 `prerender = false`)。GA4 / Meta script 一律喺 **consent 之後**由 inline JS 動態 append,未同意前唔會有任何追蹤請求或 Cookie(亦**冇** Meta `<noscript>` pixel)。
- **Consent**:單一接受/拒絕,存 `localStorage` key `cookie_consent``granted` / `denied`);banner 同載入邏輯都喺 `Base.astro`(純 HTML/CSS + `is:inline`,唔用 React)。已同意者每次載入即追蹤。
- **`/privacy`**`pages/privacy.astro`SSR)記錄 Cookie / GA4 / Meta 用途,並提供「重設 Cookie 偏好」清除 `localStorage`
- 免維護原則:唔使抄參考專案嘅 singleton 表/APIReact 動態注入(本站係 MPA,每次載入=一次 pageview)。
### Admin`pages/admin/`
- 所有 `/admin` 路由**必須** `export const prerender = false`
+1
View File
@@ -39,6 +39,7 @@ export function Footer({ settings }: { settings: Settings }) {
{ label: "服務流程", href: "#process" },
{ label: "常見問題", href: "#faq" },
{ label: "Blog", href: "/blog" },
{ label: "私隱政策", href: "/privacy" },
].map((l) => (
<Link
key={l.href}
+1 -1
View File
@@ -18,7 +18,7 @@ D1 讀取查詢層(`content.ts`)同後台網站設定欄位定義(`setting
- **Slug 去重**`uniqueSlug(db, base, selfId?)` 撞 slug 就加 `-2``-3`…;`selfId` 用喺更新自己時排除自己。admin 文章同 AI 生成都用。
- **`Db` 型別**`DrizzleD1Database<typeof schema>`
- **連結 helper**`whatsappHref` / `telHref` / `mailHref` / `digits` / `formatDate`;電話/WhatsApp 一律經呢啲 helper,唔好散寫。
- **設定欄位**`settings-fields.ts``SETTINGS_GROUPS``/admin/settings` 表單的唯一來源;新增一個 setting key 之後,要同步加落 `scripts/seed.sql`
- **設定欄位**`settings-fields.ts``SETTINGS_GROUPS``/admin/settings` 表單的唯一來源;新增一個 setting key 之後,要同步加落 `scripts/seed.sql`追蹤分析欄位(`ga4_measurement_id` / `meta_pixel_id`,空字串=關閉)由 `Base.astro` 消費,見父層 `src/AGENTS.md` 的 Analytics & consent。
- **圖片欄位**`FieldType``"image"``hero_image` / `og_image` 用呢個型別,`/admin/settings` 會 render `ImageField`(可上傳去 R2)。
- 顯示用文案繁體中文。
+15
View File
@@ -50,6 +50,21 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
{ key: "og_image", label: "分享圖片 (OG image)", type: "image" },
],
},
{
title: "追蹤分析",
fields: [
{
key: "ga4_measurement_id",
label: "GA4 評估 ID",
placeholder: "G-XXXXXXXXXX",
},
{
key: "meta_pixel_id",
label: "Meta Pixel ID",
placeholder: "123456789012345",
},
],
},
];
export const ALL_SETTING_KEYS = SETTINGS_GROUPS.flatMap((g) => g.fields.map((f) => f.key));
+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>
+14 -1
View File
@@ -1,9 +1,22 @@
---
import Base from "../layouts/Base.astro";
import { SITE_NAME } from "../config";
import { getSettings } from "../data/content";
import { getDb } from "../lib/db";
import { getEnv } from "../lib/env";
export const prerender = false;
const settings = await getSettings(getDb(getEnv().DB));
---
<Base title="關於我哋" description={`${SITE_NAME} — 香港村屋太陽能一站式服務。`} noindex={true}>
<Base
title="關於我哋"
description={`${SITE_NAME} — 香港村屋太陽能一站式服務。`}
noindex={true}
gaId={settings.ga4_measurement_id}
metaPixelId={settings.meta_pixel_id}
>
<div style="max-width:640px;margin:15vh auto;padding:0 24px;text-align:center;font-family:'Noto Sans HK',sans-serif;">
<h1 style="font-size:28px;font-weight:800;color:#1A1917;">我哋嘅服務已整合到主頁</h1>
<p style="color:#6E6862;line-height:1.9;">請返去主頁睇晒我哋嘅村屋太陽能服務、案例同流程。</p>
+2
View File
@@ -41,6 +41,8 @@ const jsonLd = post
publishedTime={post.publishedAt}
image={post.coverImage ?? undefined}
jsonLd={jsonLd}
gaId={settings.ga4_measurement_id}
metaPixelId={settings.meta_pixel_id}
>
<BlogPost
title={post.title}
+2
View File
@@ -25,6 +25,8 @@ Astro.response.headers.set("Cache-Control", "public, s-maxage=60, stale-while-re
title="太陽能知識庫"
description={settings.seo_default_description || "村屋太陽能嘅實用資訊、安裝心得同回本分析。"}
image={settings.og_image}
gaId={settings.ga4_measurement_id}
metaPixelId={settings.meta_pixel_id}
>
<BlogIndex posts={items} settings={settings} client:idle />
</Base>
+2
View File
@@ -26,6 +26,8 @@ Astro.response.headers.set(
description={settings.seo_default_description}
image={settings.og_image}
jsonLd={jsonLd}
gaId={settings.ga4_measurement_id}
metaPixelId={settings.meta_pixel_id}
>
<HomePage data={data} client:idle />
</Base>
+182
View File
@@ -0,0 +1,182 @@
---
import Base from "../layouts/Base.astro";
import { SITE_NAME } from "../config";
import { getSettings } from "../data/content";
import { getDb } from "../lib/db";
import { getEnv } from "../lib/env";
export const prerender = false;
const settings = await getSettings(getDb(getEnv().DB));
const siteName = settings.company_name || SITE_NAME;
const email = settings.email || "";
---
<Base
title="私隱政策"
description={`${siteName} 網站私隱政策:Cookie、分析工具同資料使用方式。`}
gaId={settings.ga4_measurement_id}
metaPixelId={settings.meta_pixel_id}
>
<main class="policy">
<p class="eyebrow">私隱政策</p>
<h1>Cookie 同資料使用</h1>
<p class="lead">
呢頁解釋我哋點樣喺 {siteName} 網站使用 Cookie 同分析工具。我哋只會喺你同意之後先載入分析工具,亦唔會出售你嘅個人資料。
</p>
<section>
<h2>1. 咩係 Cookie</h2>
<p>
Cookie 係網站存放喺你瀏覽器嘅細細檔案,用嚟記住你嘅設定同行為。你嘅瀏覽器亦可能使用類似技術(例如
localStorage)去記住你嘅選擇。
</p>
</section>
<section>
<h2>2. 我哋用嘅工具</h2>
<ul>
<li>
<strong>Google Analytics 4</strong> — 分析網站流量同瀏覽行為(例如瀏覽頁面、停留時間、裝置同來源),
幫我哋了解同改善網站內容。
</li>
<li>
<strong>Meta Pixel</strong> — 量度廣告同推廣活動嘅成效,了解訪客喺網站嘅互動情況。
</li>
</ul>
<p>
呢啲工具由第三方供應商(Google、Meta)提供,佢哋嘅資料處理方式受各自嘅私隱政策約束。我哋唔會透過呢啲工具收集你嘅姓名、電話或電郵等可直接識別身份嘅資料。
</p>
</section>
<section>
<h2>3. 你嘅選擇</h2>
<p>
你入到網站時會見到 Cookie 提示,可以選擇「接受」或「拒絕」。
<strong>如果你拒絕,我哋唔會載入 Google Analytics 或 Meta Pixel,亦唔會寫入相關 Cookie。</strong>
你嘅選擇會存放喺你嘅瀏覽器;你亦可隨時撳下面嘅按鈕清除選擇,下次再重新決定。
</p>
<button id="reset-consent" type="button">重設 Cookie 偏好</button>
</section>
<section>
<h2>4. 資料保留同安全</h2>
<p>
分析資料一般以匿名化或彙總形式保存,並只會保留達到上述目的所需嘅時間。我哋會以合理嘅技術同管理措施保障資料安全。
</p>
</section>
<section>
<h2>5. 查詢</h2>
<p>
如你對我哋嘅 Cookie 使用或私隱做法有任何查詢,歡迎電郵
{email ? <a href={`mailto:${email}`}>{email}</a> : "我哋"} 同我哋聯絡。
</p>
</section>
<p class="back"><a href="/">← 返去主頁</a></p>
</main>
</Base>
<script is:inline>
(function () {
var btn = document.getElementById("reset-consent");
if (!btn) return;
btn.addEventListener("click", function () {
try {
window.localStorage.removeItem("cookie_consent");
} catch (e) {}
window.location.reload();
});
})();
</script>
<style>
.policy {
max-width: 680px;
margin: 0 auto;
padding: 96px 24px 120px;
font-family: "Noto Sans HK", "PingFang HK", "Microsoft JhengHei", system-ui, sans-serif;
color: #2f2b27;
}
.eyebrow {
margin: 0 0 12px;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #b45309;
}
h1 {
margin: 0 0 24px;
font-family: "Noto Serif HK", serif;
font-size: 34px;
font-weight: 700;
color: #1a1917;
}
.lead {
margin: 0 0 40px;
font-size: 16px;
line-height: 1.9;
color: #4a453f;
}
section {
margin: 0 0 36px;
padding-top: 28px;
border-top: 1px solid #e5dfd6;
}
h2 {
margin: 0 0 12px;
font-family: "Noto Serif HK", serif;
font-size: 20px;
font-weight: 700;
color: #1a1917;
}
p,
li {
font-size: 15px;
line-height: 1.9;
color: #4a453f;
}
ul {
margin: 0 0 12px;
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
a {
color: #b45309;
}
button {
margin-top: 8px;
padding: 10px 22px;
font: inherit;
font-size: 14px;
font-weight: 700;
color: #1a1917;
background: transparent;
border: 1px solid #1a1917;
border-radius: 2px;
cursor: pointer;
}
button:hover {
background: #1a1917;
color: #faf8f4;
}
.back {
margin-top: 48px;
font-size: 14px;
}
</style>