feat: harden portfolio and add SEO foundations
This commit is contained in:
1 parent
b079afdd73
commit
62bcfbd8fa
121 files changed
+7769
-10469
No files matched your search
@@ -1,33 +1,282 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
const GA_MEASUREMENT_ID = "G-QTT9QT0T3S";
|
||||
const CONSENT_STORAGE_KEY = "evandro.analytics-consent.v1";
|
||||
const GA_DISABLE_KEY = "ga-disable-G-QTT9QT0T3S";
|
||||
const GA_SCRIPT_SRC = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
|
||||
const SITE_DOMAIN = "evandro.dev.br";
|
||||
|
||||
export default function GoogleAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const deniedConsent = {
|
||||
analytics_storage: "denied",
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
} as const;
|
||||
|
||||
type AnalyticsConsent = "granted" | "denied";
|
||||
type Gtag = (...args: unknown[]) => void;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer?: IArguments[];
|
||||
"ga-disable-G-QTT9QT0T3S"?: boolean;
|
||||
gtag?: Gtag;
|
||||
}
|
||||
}
|
||||
|
||||
function readConsent(): AnalyticsConsent | null {
|
||||
try {
|
||||
const value = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||
return value === "granted" || value === "denied" ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persistConsent(value: AnalyticsConsent): boolean {
|
||||
try {
|
||||
localStorage.setItem(CONSENT_STORAGE_KEY, value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getGtag(): Gtag {
|
||||
window.dataLayer ??= [];
|
||||
const dataLayer = window.dataLayer;
|
||||
if (!window.gtag) {
|
||||
window.gtag = function gtag() {
|
||||
// biome-ignore lint/complexity/noArguments: The official gtag bootstrap requires an IArguments entry, not an Array.
|
||||
dataLayer.push(arguments);
|
||||
};
|
||||
}
|
||||
return window.gtag;
|
||||
}
|
||||
|
||||
function removeAnalyticsLoader() {
|
||||
document.getElementById("google-analytics-script")?.remove();
|
||||
}
|
||||
|
||||
function writeCookie(value: string) {
|
||||
// biome-ignore lint/suspicious/noDocumentCookie: Cookie Store is not universal and domain-scoped GA cookies must also be expired.
|
||||
document.cookie = value;
|
||||
}
|
||||
function expireAnalyticsCookies() {
|
||||
const cookieNames = new Set(
|
||||
document.cookie
|
||||
.split(";")
|
||||
.map((cookie) => cookie.split("=")[0]?.trim())
|
||||
.filter(
|
||||
(name): name is string =>
|
||||
name === "_ga" || Boolean(name?.startsWith("_ga_")),
|
||||
),
|
||||
);
|
||||
const { hostname } = window.location;
|
||||
const domains =
|
||||
hostname === SITE_DOMAIN || hostname.endsWith(`.${SITE_DOMAIN}`)
|
||||
? new Set([hostname, SITE_DOMAIN])
|
||||
: new Set<string>();
|
||||
|
||||
for (const name of cookieNames) {
|
||||
const expiration = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; SameSite=Lax`;
|
||||
writeCookie(expiration);
|
||||
for (const domain of domains) {
|
||||
writeCookie(`${expiration}; Domain=${domain}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const panelClassName =
|
||||
"fixed bottom-sp-4 left-sp-4 right-sp-4 z-[9980] ml-auto max-w-[560px] border border-[var(--slash)] bg-[var(--ink)] p-sp-5 text-white shadow-[8px_8px_0_var(--slash)] sm:left-auto";
|
||||
const buttonClassName =
|
||||
"inline-flex min-h-11 min-w-11 items-center justify-center border px-sp-4 py-sp-2 font-mono text-[11px] font-bold uppercase tracking-[0.08em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]";
|
||||
|
||||
function GoogleAnalyticsConsent() {
|
||||
const [consent, setConsent] = useState<AnalyticsConsent | null>();
|
||||
const [preferencesOpen, setPreferencesOpen] = useState(false);
|
||||
const configuredRef = useRef(false);
|
||||
const preferencesButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const enableAnalytics = useCallback(() => {
|
||||
const gtag = getGtag();
|
||||
window[GA_DISABLE_KEY] = false;
|
||||
if (!configuredRef.current) {
|
||||
gtag("consent", "default", deniedConsent);
|
||||
gtag("js", new Date());
|
||||
configuredRef.current = true;
|
||||
}
|
||||
gtag("consent", "update", {
|
||||
...deniedConsent,
|
||||
analytics_storage: "granted",
|
||||
});
|
||||
gtag("config", GA_MEASUREMENT_ID, {
|
||||
allow_ad_personalization_signals: false,
|
||||
anonymize_ip: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const storedConsent = readConsent();
|
||||
if (storedConsent === "granted") enableAnalytics();
|
||||
setConsent(storedConsent);
|
||||
}, [enableAnalytics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (consent !== "granted" && consent !== undefined) {
|
||||
removeAnalyticsLoader();
|
||||
}
|
||||
}, [consent]);
|
||||
|
||||
function closePreferences() {
|
||||
setPreferencesOpen(false);
|
||||
preferencesButtonRef.current?.focus();
|
||||
}
|
||||
|
||||
function acceptAnalytics() {
|
||||
if (!persistConsent("granted")) return;
|
||||
enableAnalytics();
|
||||
setConsent("granted");
|
||||
closePreferences();
|
||||
}
|
||||
|
||||
function denyAnalytics() {
|
||||
persistConsent("denied");
|
||||
setConsent("denied");
|
||||
closePreferences();
|
||||
}
|
||||
|
||||
function revokeAnalytics() {
|
||||
persistConsent("denied");
|
||||
window[GA_DISABLE_KEY] = true;
|
||||
window.gtag?.("consent", "update", deniedConsent);
|
||||
expireAnalyticsCookies();
|
||||
setConsent("denied");
|
||||
closePreferences();
|
||||
}
|
||||
|
||||
if (consent === undefined) return null;
|
||||
|
||||
if (consent === null) {
|
||||
return (
|
||||
<section
|
||||
role="dialog"
|
||||
aria-modal="false"
|
||||
aria-labelledby="analytics-consent-title"
|
||||
aria-describedby="analytics-consent-description"
|
||||
className={panelClassName}
|
||||
>
|
||||
<p
|
||||
id="analytics-consent-title"
|
||||
className="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-[var(--slash)]"
|
||||
>
|
||||
{"Privacidade // Analytics"}
|
||||
</p>
|
||||
<p
|
||||
id="analytics-consent-description"
|
||||
className="mt-sp-2 max-w-[48ch] text-[13px] leading-6 text-white/85"
|
||||
>
|
||||
Você decide se permite métricas anônimas para melhorar este site.
|
||||
Publicidade e personalização continuam desativadas.
|
||||
</p>
|
||||
<div className="mt-sp-4 flex flex-wrap gap-sp-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
||||
onClick={acceptAnalytics}
|
||||
>
|
||||
Aceitar analytics
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${buttonClassName} border-white/40 bg-transparent text-white`}
|
||||
onClick={denyAnalytics}
|
||||
>
|
||||
Recusar analytics
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script src="/sys/libs/vendor-utils.js" strategy="lazyOnload" />
|
||||
<Script id="google-analytics" strategy="lazyOnload">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${GA_MEASUREMENT_ID}', {
|
||||
transport_url: window.location.origin + '/sys/api/vitals',
|
||||
cookie_flags: 'SameSite=None;Secure',
|
||||
allow_ad_personalization_signals: false,
|
||||
anonymize_ip: true,
|
||||
page_location: window.location.href,
|
||||
page_title: document.title
|
||||
});
|
||||
`}
|
||||
</Script>
|
||||
{consent === "granted" && (
|
||||
<Script
|
||||
id="google-analytics-script"
|
||||
data-testid="google-analytics-script"
|
||||
src={GA_SCRIPT_SRC}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
ref={preferencesButtonRef}
|
||||
aria-expanded={preferencesOpen}
|
||||
aria-controls="analytics-preferences"
|
||||
className={`${buttonClassName} fixed bottom-sp-4 right-sp-4 z-[9980] border-white/25 bg-[var(--ink)] text-white shadow-[4px_4px_0_var(--slash)]`}
|
||||
onClick={() => setPreferencesOpen((open) => !open)}
|
||||
>
|
||||
Preferências de analytics
|
||||
</button>
|
||||
{preferencesOpen && (
|
||||
<section
|
||||
id="analytics-preferences"
|
||||
role="dialog"
|
||||
aria-modal="false"
|
||||
aria-labelledby="analytics-preferences-title"
|
||||
aria-describedby="analytics-preferences-description"
|
||||
className={panelClassName}
|
||||
>
|
||||
<p
|
||||
id="analytics-preferences-title"
|
||||
className="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-[var(--slash)]"
|
||||
>
|
||||
{"Preferências // Analytics"}
|
||||
</p>
|
||||
<p
|
||||
id="analytics-preferences-description"
|
||||
className="mt-sp-2 text-[13px] leading-6 text-white/85"
|
||||
>
|
||||
Analytics está {consent === "granted" ? "ativo" : "desativado"}.
|
||||
Dados de publicidade permanecem negados.
|
||||
</p>
|
||||
<div className="mt-sp-4 flex flex-wrap gap-sp-2">
|
||||
{consent === "granted" ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
||||
onClick={revokeAnalytics}
|
||||
>
|
||||
Revogar analytics
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
||||
onClick={acceptAnalytics}
|
||||
>
|
||||
Aceitar analytics
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`${buttonClassName} border-white/40 bg-transparent text-white`}
|
||||
onClick={closePreferences}
|
||||
>
|
||||
Fechar preferências
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GoogleAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") return null;
|
||||
return <GoogleAnalyticsConsent />;
|
||||
}
|
||||
Reference in new issue
Block a user