seo/analytics: real GA attribution, tight robots.txt, no build-date lastmod, GA only on velxio.dev

- trackClickCTA sent the originating page as the event parameter `source`,
  a reserved traffic-source name in GA4: every landing CTA click rewrote the
  session's acquisition to landing / arduino-simulator / rpi-simulator with
  medium (not set), which put 16% of sessions in the Unassigned channel.
  Renamed to cta_source (register it as a custom dimension in GA4 if the
  click_cta report should keep breaking down by page).
- robots.txt is one group with no blank lines (some parsers end a group at
  a blank line, orphaning the Disallow rules); /login, /register and /admin
  are no longer disallowed — they carry noindex, and a page must be
  crawlable for that tag to take effect. /project/ and /api/ stay out.
- The static sitemap no longer stamps every URL with the build date as
  <lastmod>; an all-identical, ever-changing lastmod is discarded as
  unreliable, so omitting it is the honest option.
- The GA4 tag loads only on velxio.dev / www.velxio.dev / tauri.localhost:
  self-hosted copies of the app (there are >180 hostnames reporting into
  the property) no longer pollute velxio.dev's analytics.
This commit is contained in:
David Montero 2026-08-18 16:04:48 +02:00
parent 189ba355a9
commit 1b735c9c30
5 changed files with 42 additions and 215 deletions

View File

@ -20,15 +20,26 @@
<!-- ═══════════════════════════════════════════════
GOOGLE ANALYTICS 4
Replace G-XXXXXXXXXX with your real Measurement ID
(analytics.google.com → Admin → Data Streams → Measurement ID)
The Measurement ID below is velxio.dev's, so the tag only loads on
velxio.dev hostnames (plus the Velxio Desktop shell). Self-hosted
copies of the app send nothing — swap in your own ID AND hostname
(analytics.google.com → Admin → Data Streams → Measurement ID) if you
want analytics on your deployment.
═══════════════════════════════════════════════ -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HPD8HTV3TG"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-HPD8HTV3TG');
(function () {
var h = location.hostname;
if (h !== 'velxio.dev' && h !== 'www.velxio.dev' && h !== 'tauri.localhost') return;
var s = document.createElement('script');
s.async = true;
s.src = 'https://www.googletagmanager.com/gtag/js?id=G-HPD8HTV3TG';
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
window.gtag = gtag;
gtag('js', new Date());
gtag('config', 'G-HPD8HTV3TG');
})();
</script>
<!-- ═══════════════════════════════════════════════

View File

@ -1,17 +1,15 @@
# One group, no blank lines inside it: some crawlers treat a blank line as
# the end of a group, which left the Disallow rules below orphaned.
User-agent: *
Allow: /
# Auth pages — no useful content for search engines
Disallow: /login
Disallow: /register
# UUID-based duplicate of /:username/:slug — keep crawlers on the canonical
# user-friendly URL instead of the opaque /project/<uuid> alternate.
# UUID-based duplicate of /:username/:slug (the canonical URL); crawling it
# is wasted budget.
Disallow: /project/
Disallow: /admin
# Prevent crawling of API endpoints
# API endpoints are not pages.
Disallow: /api/
# Auth and admin pages are deliberately NOT disallowed here: they carry a
# noindex meta tag, and a page has to be crawlable for that tag to be seen —
# a disallowed URL can still be indexed from links, title-less.
# Sitemaps (static build-time routes + dynamic public projects + the docs
# portal, which is a separate Astro build shipping its own sitemap index)

File diff suppressed because it is too large Load Diff

View File

@ -76,11 +76,16 @@ if (process.env.VITE_PRO_BUILD && process.env.PRO_OVERLAY_PATH) {
const exampleUrls = exampleIds.map((id) => ({
loc: `${DOMAIN}/examples/${id}`,
lastmod: TODAY,
changefreq: 'monthly',
priority: 0.6,
}));
// No <lastmod>: this runs at build time with no per-page history to draw
// on, so every URL used to carry the build date. A lastmod that changes on
// every deploy for every page is provably unreliable and search engines
// discard the whole signal; omitting it is the honest option (the projects
// sitemap, which has real updated_at timestamps, keeps its lastmod).
// Every <loc> carries a trailing slash. nginx serves the prerendered route
// files as `<route>/index.html` and 301-redirects the slash-less form to add
// the slash, so a slash-less sitemap URL is fetched as a redirect and filed
@ -94,7 +99,6 @@ ${indexable
(r) => `
<url>
<loc>${DOMAIN}${r.path}${r.path.endsWith('/') ? '' : '/'}</loc>
<lastmod>${TODAY}</lastmod>
<changefreq>${r.changefreq ?? 'monthly'}</changefreq>
<priority>${r.priority ?? 0.5}</priority>
</url>`
@ -105,7 +109,6 @@ ${exampleUrls
(u) => `
<url>
<loc>${u.loc}/</loc>
<lastmod>${u.lastmod}</lastmod>
<changefreq>${u.changefreq}</changefreq>
<priority>${u.priority}</priority>
</url>`

View File

@ -130,9 +130,17 @@ export function trackProductPageClick(componentId: string, brand?: string, href?
// ── CTA / Conversion ────────────────────────────────────────────────────────
/** Fired when a user clicks a CTA button on a landing/SEO page. */
/**
* Fired when a user clicks a CTA button on a landing/SEO page.
*
* The originating page is sent as `cta_source`, NOT `source`: `source`
* (like `medium` and `campaign`) is a reserved traffic-source parameter in
* GA4, so every CTA click was rewriting the session's acquisition source to
* "landing" / "arduino-simulator" / "rpi-simulator" with medium "(not set)"
* — 16% of sessions landed in the Unassigned channel with no real origin.
*/
export function trackClickCTA(source: string, destination: string): void {
fireEvent('click_cta', { event_category: 'conversion', source, destination });
fireEvent('click_cta', { event_category: 'conversion', cta_source: source, destination });
}
// ── Library Manager ─────────────────────────────────────────────────────────