Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/docs-social-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Match the documentation website's Twitter sharing metadata to its existing Open Graph image and page content.
7 changes: 6 additions & 1 deletion apps/docs/src/components/posthog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
27 changes: 18 additions & 9 deletions apps/docs/src/layouts/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Layout>
<LayoutHead
{...head}
name={globals.name}
image={
head.image || {
src: "https://sitex.full.dev/social.png",
alt: globals.name,
width: 1200,
height: 630,
type: "image/png",
}
}
image={{ ...imageProps, src: socialImageUrl }}
twitterCard={head.twitterCard || "summary_large_image"}
>
<link rel="icon" href="/favicon.png" type="image/png" sizes="96x96" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any" />
<ThemeScript {...theme} />
<meta name="twitter:image" content={socialImageUrl} />
{imageProps.alt && (
<meta name="twitter:image:alt" content={imageProps.alt} />
)}
<meta name="twitter:title" content={head.title} />
<meta name="twitter:description" content={head.description} />
<link rel="alternate" type="text/markdown" href={markdownPath(path)} />
</LayoutHead>
<LayoutBody>
Expand Down
73 changes: 0 additions & 73 deletions apps/docs/src/lib/analytics-interactions.ts

This file was deleted.

63 changes: 21 additions & 42 deletions apps/docs/src/lib/analytics-privacy.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
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<string, unknown>
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
}
57 changes: 19 additions & 38 deletions apps/docs/src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
Loading