diff --git a/.changeset/docs-social-metadata.md b/.changeset/docs-social-metadata.md new file mode 100644 index 0000000..7fc7191 --- /dev/null +++ b/.changeset/docs-social-metadata.md @@ -0,0 +1,4 @@ +--- +--- + +Match the documentation website's Twitter sharing metadata to its existing Open Graph image and page content. diff --git a/apps/docs/src/components/posthog.tsx b/apps/docs/src/components/posthog.tsx index 78bee23..73b3444 100644 --- a/apps/docs/src/components/posthog.tsx +++ b/apps/docs/src/components/posthog.tsx @@ -18,7 +18,12 @@ function scheduleAnalytics() { export function Posthog() { useEffect(() => { - if (started || window.location.hostname !== "sitex.full.dev") return + if ( + navigator.webdriver || + started || + window.location.hostname !== "sitex.full.dev" + ) + return started = true if (document.readyState === "complete") scheduleAnalytics() else window.addEventListener("load", scheduleAnalytics, { once: true }) diff --git a/apps/docs/src/layouts/base.tsx b/apps/docs/src/layouts/base.tsx index a6261bd..306b7f1 100644 --- a/apps/docs/src/layouts/base.tsx +++ b/apps/docs/src/layouts/base.tsx @@ -34,24 +34,33 @@ export default function BaseLayout({ url: _url, ...head }: BaseLayoutProps) { + const image = head.image || { + src: "https://sitex.full.dev/social.png", + alt: globals.name, + width: 1200, + height: 630, + type: "image/png", + } + const imageProps = typeof image === "string" ? { src: image } : image + const socialImageUrl = new URL(imageProps.src, "https://sitex.full.dev").href + return ( + + {imageProps.alt && ( + + )} + + diff --git a/apps/docs/src/lib/analytics-interactions.ts b/apps/docs/src/lib/analytics-interactions.ts deleted file mode 100644 index 924f268..0000000 --- a/apps/docs/src/lib/analytics-interactions.ts +++ /dev/null @@ -1,73 +0,0 @@ -let started = false - -/** - * Record intent only, never form values, link text or contact details. - */ -export function trackInteractions(posthog: { - capture: (name: string, properties: Record) => unknown -}) { - if (started) return - started = true - const ignored = - ".ph-no-capture, .ph-no-autocapture, [data-ph-no-capture], [data-ph-no-autocapture]" - const capture = (type: string) => { - posthog.capture("website_interaction", { interaction_type: type }) - } - - document.addEventListener( - "submit", - (event) => { - if ( - event.target instanceof HTMLFormElement && - !event.target.closest(ignored) - ) { - capture("form_submit_attempt") - } - }, - true - ) - - document.addEventListener( - "click", - (event) => { - if (!(event.target instanceof Element) || event.target.closest(ignored)) - return - const link = event.target.closest("a[href]") - if (!(link instanceof HTMLAnchorElement)) return - let url - try { - url = new URL(link.href) - } catch { - return - } - if (url.protocol === "tel:") capture("phone_click") - else if (url.protocol === "mailto:") capture("email_click") - else if (url.protocol === "whatsapp:") capture("whatsapp_click") - else if ( - link.hasAttribute("download") && - (url.protocol === "blob:" || url.protocol === "data:") - ) { - capture("download_click") - } else if (url.protocol === "https:" || url.protocol === "http:") { - if ( - ["wa.me", "api.whatsapp.com", "web.whatsapp.com"].includes( - url.hostname - ) - ) { - capture("whatsapp_click") - } else if ( - link.hasAttribute("download") || - /\.(pdf|docx?|xlsx?|pptx?|csv|zip)$/i.test(url.pathname) - ) { - capture("download_click") - } else if ( - url.hostname.replace(/^www\./, "") !== - window.location.hostname.replace(/^www\./, "") - ) { - capture("outbound_click") - } - } - }, - { capture: true, passive: true } - ) -} diff --git a/apps/docs/src/lib/analytics-privacy.ts b/apps/docs/src/lib/analytics-privacy.ts index 8eee149..606b177 100644 --- a/apps/docs/src/lib/analytics-privacy.ts +++ b/apps/docs/src/lib/analytics-privacy.ts @@ -1,49 +1,28 @@ import type { CaptureResult } from "posthog-js" -/** Remove URL details from exceptions; redact ambiguous text as a whole. */ -export function sanitizeExceptionUrls(event: CaptureResult | null) { - if (event?.event !== "$exception") return event +/** Keep basic page statistics and remove URL query strings and fragments. */ +export function sanitizeEventUrls(event: CaptureResult | null) { + if (!event || !["$pageview", "$pageleave"].includes(event.event)) return null const seen = new WeakSet() - const clean = (value: unknown): unknown => { - if (typeof value === "string") { - if (!/[?#@]/.test(value)) return value - const normalized = value.replace(/\\/g, "/") - // Only preserve an unambiguous standalone URL or relative filename. - if ( - /\s|["'<>]/.test(normalized) || - !/^(?:https?:|\/|\.\.?\/|[\w.-]+[?#])/i.test(normalized) - ) { - return "[redacted-url-details]" - } - try { - const absolute = /^https?:/i.test(normalized) - const parsed = new URL( - normalized, - absolute ? undefined : "https://redacted.invalid" - ) - if (/https?:|\/\/|@/i.test(parsed.pathname)) - return "[redacted-url-details]" - const prefix = absolute - ? parsed.protocol + "//" + parsed.host - : normalized.startsWith("//") - ? "//" + parsed.host - : "" - const pathname = - !absolute && !normalized.startsWith("/") - ? parsed.pathname.slice(1) - : parsed.pathname - return prefix + pathname - } catch { - return "[redacted-url-details]" - } + function scrub(value: unknown) { + if (!value || typeof value !== "object" || seen.has(value)) return + seen.add(value) + const properties = value as Record + for (const [key, nested] of Object.entries(value)) { + if (typeof nested === "string" && /^https?:\/\//i.test(nested)) { + try { + const url = new URL(nested) + if (/referrer/i.test(key)) url.pathname = "/" + url.search = "" + url.hash = "" + properties[key] = url.href + } catch { + properties[key] = "" + } + } else scrub(nested) } - if (value && typeof value === "object" && !seen.has(value)) { - seen.add(value) - const record = value as Record - for (const key of Object.keys(record)) record[key] = clean(record[key]) - } - return value } - clean(event.properties) + for (const values of [event.properties, event.$set, event.$set_once]) + scrub(values) return event } diff --git a/apps/docs/src/lib/analytics.ts b/apps/docs/src/lib/analytics.ts index 013b5bb..cab7e3b 100644 --- a/apps/docs/src/lib/analytics.ts +++ b/apps/docs/src/lib/analytics.ts @@ -1,54 +1,35 @@ -import "posthog-js/dist/web-vitals" -import { sanitizeExceptionUrls } from "./analytics-privacy" -import "posthog-js/dist/exception-autocapture" -import { - ErrorTrackingExtensions, - AnalyticsExtensions, -} from "posthog-js/dist/extension-bundles" -import { trackInteractions } from "./analytics-interactions" import posthog from "posthog-js/dist/module.slim.no-external" +import { sanitizeEventUrls } from "./analytics-privacy" + export function startAnalytics() { + const fetchOptions: RequestInit = { credentials: "omit", cache: "no-store" } posthog.init("phc_txCmZNAK3YueTeNyvtXmnh9JvK2LdbF3hK3kvxRcEJou", { api_host: "https://eu.i.posthog.com", + api_transport: "fetch", + disable_beacon: true, + fetch_options: fetchOptions, defaults: "2025-11-30", cookieless_mode: "always", + persistence: "memory", + persistence_name: "website", person_profiles: "never", - autocapture: false, - __extensionClasses: { - ...ErrorTrackingExtensions, - webVitalsAutocapture: AnalyticsExtensions.webVitalsAutocapture, - }, - capture_exceptions: { - capture_unhandled_errors: true, - capture_unhandled_rejections: true, - capture_console_errors: true, - }, - capture_performance: { web_vitals: true, web_vitals_attribution: false }, - mask_personal_data_properties: true, - custom_personal_data_properties: [ - "email", - "phone", - "name", - "password", - "token", - "access_token", - "refresh_token", - "code", - ], - disable_capture_url_hashes: true, - property_denylist: [ - "$referrer", - "$initial_referrer", - "$session_entry_referrer", - ], - loaded: trackInteractions, - before_send: sanitizeExceptionUrls, + save_campaign_params: false, + property_denylist: ["ph_keyword", "$initial_ph_keyword"], capture_pageview: true, capture_pageleave: true, + disable_scroll_properties: true, + autocapture: false, + capture_dead_clicks: false, + capture_heatmaps: false, + capture_exceptions: false, + capture_performance: false, disable_session_recording: true, disable_surveys: true, + disable_product_tours: true, + disable_conversations: true, advanced_disable_flags: true, disable_external_dependency_loading: true, + before_send: sanitizeEventUrls, }) }