feat(analytics): activate GA4 (G-KQ1JRV3KN7) + GDPR privacy page + GSC support

Client provided the GA4 Measurement ID and approved the standard policy.

- Activate analytics: NEXT_PUBLIC_GA_ID set to the FLUX property
  G-KQ1JRV3KN7 in the env template, with the same value as the
  docker-compose build-arg fallback so it works out of the box on deploy.
  (GA Measurement IDs are public — they ship in page HTML — safe to commit.)
- New GDPR-compliant Privacy & Cookie Policy page at /[locale]/privacy
  (all 5 locales), linked from the consent banner. Includes a clearly
  marked template disclaimer for legal review and a TODO on the contact
  email. Added to sitemap.
- Consent banner now links via the locale-aware next-intl Link.
- Google Search Console: optional NEXT_PUBLIC_GSC_VERIFICATION env var
  emits the google-site-verification meta tag (Dockerfile arg +
  docker-compose wired). Empty by default.

Verified: build inlines G-KQ1JRV3KN7 into the client bundle; the 5
/privacy routes render; TypeScript clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 12:00:44 -05:00
parent 148aefc68f
commit fbfffb28d9
7 changed files with 220 additions and 6 deletions
+5
View File
@@ -79,12 +79,17 @@ export async function generateMetadata(): Promise<Metadata> {
apple: branding.appleTouchIconUrl,
};
// Google Search Console verification (HTML-tag method). Emits
// <meta name="google-site-verification" content="..."> when set.
const gscToken = process.env.NEXT_PUBLIC_GSC_VERIFICATION;
return {
metadataBase: new URL(APP_BASE_URL),
title: "FLUX | Energy, Directed.",
description: "Advanced Radio Frequency Solutions by Patrizio Grando.",
icons,
manifest: "/manifest.webmanifest",
...(gscToken ? { verification: { google: gscToken } } : {}),
openGraph: {
title: "FLUX | Energy, Directed.",
description: "Advanced Radio Frequency Solutions by Patrizio Grando.",
+198
View File
@@ -0,0 +1,198 @@
import type { Metadata } from "next";
import { setRequestLocale } from "next-intl/server";
import { buildPageMetadata } from "@/lib/seo";
import Breadcrumbs from "@/components/seo/Breadcrumbs";
// Static legal page. Revalidate rarely.
export const revalidate = 86400;
const LAST_UPDATED = "June 2026";
const COMPANY = "FLUX Srl";
const ADDRESS = "Romano d'Ezzelino, Vicenza, Italy";
const CONTACT_EMAIL = "privacy@rf-flux.com"; // TODO: confirm with FLUX legal
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
return buildPageMetadata({
locale,
pathWithoutLocale: "privacy",
title: "Privacy & Cookie Policy | FLUX",
description:
"How FLUX Srl collects, uses and protects personal data on rf-flux.com, in compliance with the EU GDPR.",
});
}
export default async function PrivacyPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const crumbs = [
{ name: "Home", url: `/${locale}` },
{ name: "Privacy & Cookie Policy", url: `/${locale}/privacy` },
];
return (
<main className="relative w-full min-h-screen bg-[#F5F5F7]">
<div className="max-w-3xl mx-auto px-6 pt-28 md:pt-36 pb-24">
<Breadcrumbs items={crumbs} />
<header className="mt-6 mb-10">
<h1 className="text-3xl md:text-5xl font-light text-[#1D1D1F] tracking-tight">
Privacy &amp; Cookie <span className="font-medium">Policy</span>
</h1>
<p className="mt-3 text-sm text-[#86868B]">Last updated: {LAST_UPDATED}</p>
</header>
{/* Template disclaimer — remove once reviewed by legal counsel */}
<div className="mb-10 rounded-2xl border border-amber-300/50 bg-amber-50 p-4 text-sm text-amber-900">
<strong>Template notice:</strong> this is a standard GDPR-compliant
template provided as a starting point. Please have it reviewed and
adapted by your legal counsel before relying on it, and confirm the
contact details below.
</div>
<div className="space-y-8 text-[#1D1D1F]">
<Section title="1. Who we are">
<P>
{COMPANY} (&ldquo;we&rdquo;, &ldquo;us&rdquo;, &ldquo;our&rdquo;)
is the data controller responsible for your personal data
collected through this website, {SITE}. Our registered office is
in {ADDRESS}.
</P>
<P>
For any privacy-related request you can contact us at{" "}
<a href={`mailto:${CONTACT_EMAIL}`} className="text-[#0066CC] underline underline-offset-2">
{CONTACT_EMAIL}
</a>
.
</P>
</Section>
<Section title="2. What data we collect">
<P>We collect personal data only when you actively provide it, or through privacy-respecting analytics:</P>
<ul className="list-disc pl-5 space-y-1.5 text-[#3A3A3C]">
<li>
<strong>Contact &amp; consultation requests:</strong> name, company,
email, phone (optional) and any message you send through our
forms or the FLUX AI assistant.
</li>
<li>
<strong>AI assistant conversations:</strong> the messages you
exchange with the on-site assistant, used to answer your
questions and improve the service. Your IP address is stored
only in pseudonymised (hashed) form.
</li>
<li>
<strong>Analytics:</strong> aggregated, anonymised usage data
via Google Analytics 4 but only after you accept analytics
cookies (see section 4).
</li>
<li>
<strong>Technical logs:</strong> standard server logs (IP,
browser, timestamps) kept for security and troubleshooting.
</li>
</ul>
</Section>
<Section title="3. How and why we use it">
<P>We process your data on the following legal bases (GDPR Art. 6):</P>
<ul className="list-disc pl-5 space-y-1.5 text-[#3A3A3C]">
<li><strong>Consent</strong> analytics cookies; you can withdraw it at any time.</li>
<li><strong>Pre-contractual / legitimate interest</strong> responding to your consultation and quote requests.</li>
<li><strong>Legitimate interest</strong> keeping the site secure and improving our products and content.</li>
</ul>
</Section>
<Section title="4. Cookies & consent">
<P>
We use a strictly necessary set of cookies to run the site and,
optionally, analytics cookies. When you first visit, a banner lets
you accept or decline analytics. We use Google Consent Mode v2:
until you accept, no analytics cookies are set and no personal
data is sent to Google. You can change your choice at any time by
clearing the site cookies in your browser.
</P>
</Section>
<Section title="5. Who we share data with">
<P>We never sell your data. We share it only with trusted processors strictly to operate the site:</P>
<ul className="list-disc pl-5 space-y-1.5 text-[#3A3A3C]">
<li><strong>Google (Analytics)</strong> anonymised usage statistics, only with your consent.</li>
<li><strong>Email / hosting providers</strong> to deliver your requests to our team and host the site.</li>
</ul>
<P>
Some providers may process data outside the EU/EEA; where that
happens, transfers are covered by appropriate safeguards such as
the EU Standard Contractual Clauses.
</P>
</Section>
<Section title="6. How long we keep it">
<P>
We keep consultation and contact data for as long as needed to
handle your request and to comply with legal obligations, then
delete or anonymise it. Analytics data is retained according to
Google Analytics&rsquo; configured retention period.
</P>
</Section>
<Section title="7. Your rights">
<P>Under the GDPR you have the right to:</P>
<ul className="list-disc pl-5 space-y-1.5 text-[#3A3A3C]">
<li>access, rectify or erase your personal data;</li>
<li>restrict or object to processing;</li>
<li>data portability;</li>
<li>withdraw consent at any time;</li>
<li>lodge a complaint with your data protection authority (in Italy, the Garante per la protezione dei dati personali).</li>
</ul>
<P>
To exercise any of these rights, contact us at{" "}
<a href={`mailto:${CONTACT_EMAIL}`} className="text-[#0066CC] underline underline-offset-2">
{CONTACT_EMAIL}
</a>
.
</P>
</Section>
<Section title="8. Data security">
<P>
We apply appropriate technical and organisational measures
(encryption in transit, access controls, pseudonymisation) to
protect your data against unauthorised access, loss or misuse.
</P>
</Section>
<Section title="9. Changes to this policy">
<P>
We may update this policy from time to time. The &ldquo;last
updated&rdquo; date at the top reflects the latest revision.
</P>
</Section>
</div>
</div>
</main>
);
}
const SITE = "rf-flux.com";
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section>
<h2 className="text-lg md:text-xl font-semibold text-[#1D1D1F] mb-3">{title}</h2>
<div className="space-y-3 text-[15px] leading-relaxed">{children}</div>
</section>
);
}
function P({ children }: { children: React.ReactNode }) {
return <p className="text-[#3A3A3C]">{children}</p>;
}
+1
View File
@@ -30,6 +30,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
{ path: "/news", priority: 0.7, changeFrequency: "daily" as const },
{ path: "/heritage", priority: 0.6, changeFrequency: "monthly" as const },
{ path: "/team", priority: 0.6, changeFrequency: "monthly" as const },
{ path: "/privacy", priority: 0.3, changeFrequency: "yearly" as const },
];
for (const locale of LOCALES) {
+3 -2
View File
@@ -14,6 +14,7 @@
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/routing";
import {
analyticsEnabled,
readStoredConsent,
@@ -59,12 +60,12 @@ export default function ConsentBanner() {
<p className="text-sm font-medium text-[#1D1D1F]">{t("title")}</p>
<p className="mt-1 text-xs leading-relaxed text-[#6E6E73]">
{t("body")}{" "}
<a
<Link
href="/privacy"
className="underline decoration-[#00B8CC] underline-offset-2 hover:text-[#1D1D1F]"
>
{t("learnMore")}
</a>
</Link>
</p>
</div>
<div className="flex shrink-0 items-center gap-2">