Skip to content

Commit 2a8d5f1

Browse files
Integrate Partytown for analytics and ads
Add Partytown and wire Google Analytics / AdSense through it, plus related optimizations and headers. Highlights: - Add Partytown static assets and initialize Partytown (config + snippet) in app.html. - Load gtag.js and AdSense via <script type="text/partytown"> in Analytics.svelte and Adsense.svelte; removed previous lazy-mount JS that manually injected scripts. - Add _headers with caching rules for static assets, Partytown, immutable build files, and security headers. - Add dev deps and build tooling updates (@builder.io/partytown, @sveltejs/enhanced-img, terser, vite-plugin-compression2) and update lockfile accordingly. - Use enhanced:img for the header logo and move background.jpg into src lib images. - Add isMobile utility and return isMobile from layout.server load; add a small CSS media query to disable reveal animations on small screens or reduced-motion. - Misc: theme-color and preconnect links in app.html; updates to svelte/vite/unocss configs. These changes aim to offload third-party analytics/ads to a web worker (Partytown) for better main-thread performance, enforce caching and security headers, and adjust build tooling for image/enhancement and compression.
1 parent 250b5a7 commit 2a8d5f1

30 files changed

Lines changed: 5968 additions & 190 deletions

site/htdocs/_headers

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Cache static assets
2+
/favicon.ico
3+
Cache-Control: public, max-age=31536000, immutable
4+
/robots.txt
5+
Cache-Control: public, max-age=86400
6+
/sitemap.xml
7+
Cache-Control: public, max-age=86400
8+
/rss.xml
9+
Cache-Control: public, max-age=86400
10+
11+
# Immutable build artifacts
12+
/_app/immutable/*
13+
Cache-Control: public, max-age=31536000, immutable
14+
15+
# Assets
16+
/~partytown/*
17+
Cache-Control: public, max-age=31536000, immutable
18+
19+
# Security Headers
20+
/*
21+
X-Frame-Options: DENY
22+
X-Content-Type-Options: nosniff
23+
Referrer-Policy: strict-origin-when-cross-origin
24+
Permissions-Policy: geolocation=(), microphone=(), camera=()
25+
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

site/htdocs/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
"prepare": "svelte-kit sync || echo ''"
1616
},
1717
"devDependencies": {
18-
"@fontsource/fira-mono": "^5.2.7",
1918
"@antfu/eslint-config": "^7.7.3",
19+
"@builder.io/partytown": "^0.10.3",
2020
"@eslint/compat": "^2.0.3",
2121
"@eslint/js": "^10.0.1",
22+
"@fontsource/fira-mono": "^5.2.7",
2223
"@iconify-json/lucide": "^1.2.99",
2324
"@sveltejs/adapter-cloudflare": "^7.2.8",
25+
"@sveltejs/enhanced-img": "^0.10.4",
2426
"@sveltejs/kit": "^2.55.0",
2527
"@sveltejs/vite-plugin-svelte": "^7.0.0",
2628
"@unocss/eslint-plugin": "^66.6.7",
@@ -44,9 +46,11 @@
4446
"svelte": "^5.55.0",
4547
"svelte-check": "^4.4.5",
4648
"svelte-eslint-parser": "^1.6.0",
49+
"terser": "^5.46.1",
4750
"unist-util-visit": "^5.1.0",
4851
"unocss": "^66.6.7",
49-
"vite": "^8.0.3"
52+
"vite": "^8.0.3",
53+
"vite-plugin-compression2": "^2.5.3"
5054
},
5155
"dependencies": {
5256
"@sveltia/cms": "^0.150.1"

site/htdocs/pnpm-lock.yaml

Lines changed: 162 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/htdocs/src/app.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ h6 {
209209
animation: reveal-up 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
210210
}
211211

212+
@media (max-width: 767px), (prefers-reduced-motion: reduce) {
213+
.reveal-up {
214+
opacity: 1;
215+
animation: none;
216+
transform: none;
217+
}
218+
}
219+
212220
.reveal-fade {
213221
opacity: 0;
214222
animation: reveal-fade 1s ease-out forwards;

site/htdocs/src/app.html

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,21 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6-
7-
<!-- Theme color for browser UI tinting (light / dark) -->
8-
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
9-
<meta name="theme-color" content="#0b0e11" media="(prefers-color-scheme: dark)" />
6+
<meta name="theme-color" content="#0052FF" />
7+
8+
<link rel="preconnect" href="https://pagead2.googlesyndication.com" />
9+
<link rel="preconnect" href="https://www.googletagmanager.com" />
1010

1111
<!-- Favicon — place favicon.ico and favicon.svg in static/ -->
1212
<link rel="icon" href="%sveltekit.assets%/favicon.ico" sizes="32x32" />
13-
<link rel="icon" href="%sveltekit.assets%/favicon.svg" type="image/svg+xml" />
14-
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
15-
16-
<!-- PWA manifest — place manifest.webmanifest in static/ -->
17-
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
18-
19-
<!-- Dark mode FOUC prevention.
20-
Runs synchronously before first paint so there is no flash of unstyled content.
21-
Wrapped in try/catch for environments where localStorage is blocked (private mode, SSR). -->
2213
<script>
23-
;(function () {
24-
try {
25-
var s = localStorage.getItem('theme')
26-
var d = window.matchMedia('(prefers-color-scheme: dark)').matches
27-
document.documentElement.classList.toggle('dark', s === 'dark' || (!s && d))
28-
} catch (_) {
29-
document.documentElement.classList.toggle(
30-
'dark',
31-
window.matchMedia('(prefers-color-scheme: dark)').matches
32-
)
33-
}
34-
})()
14+
/* Partytown Config */
15+
partytown = {
16+
forward: ["gtag", "adsbygoogle", "dataLayer.push"],
17+
};
18+
/* Partytown Snippet */
19+
!(function(w,p,f,c){if(!window.crossOriginIsolated && !navigator.serviceWorker) return;c=w[p]=w[p]||{};c[f]=(c[f]||[])})(window,'partytown','forward');!function(t,r,o,i,a,s,c,d,l,p,u=t,f){function h(){f||(f=1,"/"==(c=(s.lib||"/~partytown/")+(s.debug?"debug/":""))[0]&&(l=r.querySelectorAll('script[type="text/partytown"]'),i!=t?i.dispatchEvent(new CustomEvent("pt1",{detail:t})):(d=setTimeout(v,999999999),r.addEventListener("pt0",w),a?y(1):o.serviceWorker?o.serviceWorker.register(c+(s.swPath||"partytown-sw.js"),{scope:c}).then((function(t){t.active?y():t.installing&&t.installing.addEventListener("statechange",(function(t){"activated"==t.target.state&&y()}))}),console.error):v())))}function y(e){p=r.createElement(e?"script":"iframe"),t._pttab=Date.now(),e||(p.style.display="block",p.style.width="0",p.style.height="0",p.style.border="0",p.style.visibility="hidden",p.setAttribute("aria-hidden",!0)),p.src=c+"partytown-"+(e?"atomics.js?v=0.10.3":"sandbox-sw.html?"+t._pttab),r.querySelector(s.sandboxParent||"body").appendChild(p)}function v(n,o){for(w(),i==t&&(s.forward||[]).map((function(n){delete t[n.split(".")[0]]})),n=0;n<l.length;n++)o=r.createElement("script"),o.innerHTML=l[n].innerHTML,r.head.appendChild(o);p&&p.parentNode.removeChild(p)}function w(){clearTimeout(d)}s=t.partytown||{},i==t&&(s.forward||[]).map((function(r){u=t,r.split(".").map((function(e,r,o){u=u[o[r]]=r+1<o.length?u[o[r]]||{}:function(){(t._ptf=t._ptf||[]).push(o,arguments)}}))})),"complete"==r.readyState?h():(t.addEventListener("DOMContentLoaded",h),t.addEventListener("load",h))}(window,document,navigator,top,window.crossOriginIsolated);
3520
</script>
36-
3721
%sveltekit.head%
3822
</head>
3923

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script>
22
import { page } from '$app/state';
3-
import { onMount } from 'svelte';
43
54
// Helper: push tất cả ad slots chưa được khởi tạo
65
function pushUninitiatedAds() {
@@ -20,43 +19,16 @@
2019
// Reactive: khi navigate sang trang mới, push ads nếu đã load rồi
2120
$effect(() => {
2221
// eslint-disable-next-line no-unused-vars
23-
const _path = page.url.pathname; // trigger reactive khi path đổi
22+
const _path = page.url.pathname;
2423
pushUninitiatedAds();
2524
});
26-
27-
onMount(() => {
28-
let loaded = false;
29-
30-
const loadAdsense = () => {
31-
if (loaded) return;
32-
loaded = true;
33-
34-
const script = document.createElement('script');
35-
script.async = true;
36-
script.src =
37-
'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3602487920405886';
38-
script.crossOrigin = 'anonymous';
39-
40-
// Sau khi script load xong → push toàn bộ ad slots hiện có trên trang
41-
script.onload = () => pushUninitiatedAds();
42-
43-
document.head.appendChild(script);
44-
45-
// Dọn listeners
46-
window.removeEventListener('scroll', loadAdsense);
47-
window.removeEventListener('mousemove', loadAdsense);
48-
window.removeEventListener('touchstart', loadAdsense);
49-
};
50-
51-
// Timeout dài hơn Analytics (10s) để không tranh CPU lúc page vừa load
52-
if ('requestIdleCallback' in window) {
53-
window.requestIdleCallback(loadAdsense, { timeout: 10000 });
54-
} else {
55-
setTimeout(loadAdsense, 8000);
56-
}
57-
58-
window.addEventListener('scroll', loadAdsense, { passive: true, once: true });
59-
window.addEventListener('mousemove', loadAdsense, { passive: true, once: true });
60-
window.addEventListener('touchstart', loadAdsense, { passive: true, once: true });
61-
});
6225
</script>
26+
27+
<svelte:head>
28+
<!-- Load AdSense via Partytown -->
29+
<script
30+
type="text/partytown"
31+
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3602487920405886"
32+
crossorigin="anonymous"
33+
></script>
34+
</svelte:head>
Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script>
22
import { page } from '$app/state';
3-
import { onMount } from 'svelte';
43
5-
// Track page view on SPA navigation — chỉ chạy sau khi gtag đã load
4+
// Track page view on SPA navigation
65
$effect(() => {
76
const path = page.url.pathname;
8-
// Chỉ track khi gtag đã sẵn sàng (tức là sau khi lazy load xong)
97
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
108
window.gtag('event', 'page_view', {
119
page_title: document.title,
@@ -14,49 +12,15 @@
1412
});
1513
}
1614
});
17-
18-
onMount(() => {
19-
let loaded = false;
20-
21-
const loadAnalytics = () => {
22-
if (loaded) return;
23-
loaded = true;
24-
25-
// Bước 1: Init dataLayer TRƯỚC — không cần script tag riêng
26-
window.dataLayer = window.dataLayer || [];
27-
window.gtag = function () { window.dataLayer.push(arguments); };
28-
window.gtag('js', new Date());
29-
window.gtag('config', 'G-4YCKS1JTX8', { send_page_view: false });
30-
31-
// Bước 2: Load GTM script SAU khi đã init xong
32-
const script = document.createElement('script');
33-
script.async = true;
34-
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-4YCKS1JTX8';
35-
script.onload = () => {
36-
// Track page view đầu tiên sau khi script load thành công
37-
window.gtag('event', 'page_view', {
38-
page_title: document.title,
39-
page_location: location.href,
40-
page_path: page.url.pathname
41-
});
42-
};
43-
document.head.appendChild(script);
44-
45-
// Dọn listeners
46-
window.removeEventListener('scroll', loadAnalytics);
47-
window.removeEventListener('mousemove', loadAnalytics);
48-
window.removeEventListener('touchstart', loadAnalytics);
49-
};
50-
51-
// Chờ idle rồi mới load — ưu tiên page interactive trước
52-
if ('requestIdleCallback' in window) {
53-
window.requestIdleCallback(loadAnalytics, { timeout: 8000 });
54-
} else {
55-
setTimeout(loadAnalytics, 5000);
56-
}
57-
58-
window.addEventListener('scroll', loadAnalytics, { passive: true, once: true });
59-
window.addEventListener('mousemove', loadAnalytics, { passive: true, once: true });
60-
window.addEventListener('touchstart', loadAnalytics, { passive: true, once: true });
61-
});
6215
</script>
16+
17+
<svelte:head>
18+
<!-- Global site tag (gtag.js) - Google Analytics via Partytown -->
19+
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-4YCKS1JTX8"></script>
20+
<script type="text/partytown">
21+
window.dataLayer = window.dataLayer || [];
22+
window.gtag = function() { dataLayer.push(arguments); };
23+
window.gtag('js', new Date());
24+
window.gtag('config', 'G-4YCKS1JTX8', { 'send_page_view': false });
25+
</script>
26+
</svelte:head>

site/htdocs/src/lib/components/Header.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { onMount } from 'svelte';
44
import { browser } from '$app/environment';
55
import { githubLink, discordChat, zeppOSDev } from '$lib/info.js';
6-
import logo from '$lib/images/logo.jpg?w=64&format=webp';
76
87
let expanded = $state(false);
98
let expandedDropdown = $state(false);
@@ -91,7 +90,7 @@
9190
<!-- Logo & Brand -->
9291
<a href="/" class="flex items-center gap-3 group" aria-label="Zepp OS Screen Reader Home">
9392
<div class="relative w-9 h-9 md:w-11 md:h-11 overflow-hidden rounded-xl ring-2 ring-blue-500/10 group-hover:ring-blue-500/30 transition-all duration-500">
94-
<img src={logo} alt="" width="44" height="44" class="w-full h-full object-cover" />
93+
<enhanced:img src="$lib/images/logo.jpg" alt="" class="w-full h-full object-cover" />
9594
</div>
9695
<span class="font-display font-black text-lg md:text-xl tracking-tighter uppercase" aria-hidden="true">
9796
<span class="text-blue-600">ZEPP</span> <span class="text-gray-900 dark:text-white">OS</span>

site/htdocs/src/lib/util.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ export function paginate(data, { page = 1, limit } = {}) {
1212

1313
return data
1414
}
15+
16+
/**
17+
* Checks if the user agent is a mobile device.
18+
* @param {string} ua
19+
* @returns {boolean}
20+
*/
21+
export function isMobile(ua) {
22+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
23+
}

0 commit comments

Comments
 (0)