+
+ BADSEO.DEV / PRIVACY
+ Privacy policy
+ Policy version July 10, 2026
+ This policy explains what badseo.dev processes when you visit, what remains off until you choose otherwise, and how to change that choice.
+
+
+
+
The short version
+
If optional analytics is enabled, Google Analytics stays completely off unless you accept it. Rejecting does not change the site, and you can later change your choice through Cookie settings. When analytics is disabled at the site level, no consent controls appear because no Google Analytics code can run.
+
+
+
+ Who operates this site
+ badseo.dev is operated by Every App, Inc. It is a public test site maintained by the team behind OpenSEO. It contains no accounts, forms, purchases, or user-submitted content.
+ Privacy questions and requests can be sent to ben@openseo.so.
+
+
+
+ What is necessary to run the site
+ Cloudflare serves badseo.dev and therefore receives ordinary request information such as your IP address, request headers, requested URL, and time of access. We use Cloudflare to deliver and secure the site, store consent records, and rate-limit the consent endpoint. Cloudflare uses the request IP transiently for that rate limit; we do not write it to the consent ledger described below.
+ badseo.dev does not create a separate visitor access-log database. Cloudflare may retain platform request and security data under our service configuration and its legal requirements. We use that processing only for delivery, security, abuse prevention, and diagnosing operational failures.
+
+
+
+ Optional Google Analytics
+ When the optional analytics feature is enabled, badseo.dev uses Google Analytics 4, provided by Google LLC, only after you select Accept analytics. Before that choice, all four Google Consent Mode signals are denied and the Google tag is not loaded, so badseo.dev sends no analytics request or cookieless consent-mode ping to Google. When the feature is disabled, the tag and Consent Mode bootstrap are both absent.
+ When enabled, Google Analytics receives the page URL with its query string and fragment removed, a similarly limited referring URL, browser and device information, timestamps, approximate location derived from your IP address, and randomly generated client and session identifiers. If outbound-click measurement is enabled, it also receives the full destination URL and related link metadata. We use this information to understand aggregate traffic, which technical-SEO examples people use, and which outbound links they follow.
+ Google Analytics uses first-party cookies named _ga and _ga_<container-id> to distinguish visitors and sessions. Google documents a default cookie expiration of up to two years, subject to browser limits and deletion by you.
+ The Worker remains analytics-off until we have verified that the Google Analytics property uses fourteen-month user-level and event-level data retention, does not use User-ID, is not linked to Google Ads, and has the documented sharing and measurement settings. Google's retention setting does not apply to aggregated standard reports, which may remain available longer.
+ The site tag itself disables Google Signals and advertising-personalization signals and keeps all advertising consent signals denied.
+
+
+
+ How your choice is recorded
+ We store your choice, the applicable notice and policy versions, the relevant measurement identifier, the time of the choice, and—after a grant—a randomly generated receipt identifier in necessary local browser storage. We use that record only to remember and apply your preference. We treat it as expired after 400 days and remove it on your next visit. Separate necessary retry markers may be retained for failed withdrawal events until they are successfully written or reach the same 400-day limit.
+ When you accept analytics, a same-origin Cloudflare Worker must save an operator-accessible consent event before Google Analytics can load. That append-only event contains the pseudonymous receipt identifier, server-recorded date and time, grant state, measurement identifier, and an exact copy and version of the notice and choices shown. A later withdrawal creates a separate event. We retain each event in Cloudflare Workers KV for up to 830 days so evidence of the choice remains available throughout the possible 400-day grant and the following fourteen-month user-level and event-level retention window. This evidence period does not determine how long aggregated Google Analytics reports remain available.
+ We do not include your raw IP address or browser user-agent string in this ledger, and we do not keep separate exports of it.
+
+
+
+ Rejecting or withdrawing
+ When optional analytics is enabled, you can reject it without losing access to any page. You can later grant or withdraw consent through Cookie settings on badseo.dev pages.
+ Withdrawal immediately denies analytics in active badseo.dev tabs, takes effect in suspended tabs when they resume, removes the Google Analytics cookies accessible to this site, and prevents the Google tag from loading on later page views. It does not wait for the withdrawal event to be written. If that write fails and necessary browser storage is available, the browser retains a pending marker for each affected receipt and retries on a later page view. If preference storage is unavailable, the page still attempts the write immediately but cannot retain a retry marker; analytics remains off either way. Withdrawal does not affect processing that lawfully occurred before it.
+
+
+
+ Service providers and international processing
+ Cloudflare provides hosting, request security, rate limiting, and the consent-event store. Google provides Google Analytics only after consent. These providers may process information in the United States and other countries. Where required, their applicable terms use recognized transfer mechanisms such as adequacy decisions or standard contractual clauses. Contact us for more information about the safeguards relevant to your request.
+ Learn more about how Google uses information from sites or apps that use its services, Google's privacy practices, and Cloudflare's privacy practices.
+
+
+
+ Why we process this information
+ Where data-protection law requires a legal basis, we rely on your consent for optional Google Analytics. We process the limited information needed to deliver and secure the site, prevent abuse, remember your preference, and maintain consent evidence because those activities are necessary to provide the site and support our legitimate interests in operating it securely and demonstrating your choice.
+
+
+
+ Your privacy rights
+ Depending on where you live, you may have rights to ask about, access, correct, delete, restrict, or object to certain processing of your information. Contact ben@openseo.so to make a request. For a request concerning a pseudonymous consent record, we may provide instructions for retrieving the receipt identifier from your browser so we can locate the matching events. You may also complain to the data-protection or privacy authority where you live.
+
+
+
+ Changes to this policy
+ We will update the version date above when this policy changes. If the analytics vendor, measurement destination, purpose, choices, or material notice wording changes, badseo.dev will disregard any affected browser grant and ask again before enabling analytics.
+
+`,
+ });
+}
diff --git a/badseo/src/styles.ts b/badseo/src/styles.ts
index 4575e468..ef9240bc 100644
--- a/badseo/src/styles.ts
+++ b/badseo/src/styles.ts
@@ -1,7 +1,7 @@
// Served verbatim at /styles.css. Kept as a module string so the Worker stays
-// dependency-free. Palette + type mirror the OpenSEO marketing site (web/):
-// a warm "cream" canvas, ink text, one orange accent, Inter, hairline borders,
-// small radii, no shadows.
+// dependency-free. The warm canvas, ink text, orange accent, hairline borders,
+// and compact editorial typography mirror the OpenSEO marketing site without
+// making any pre-consent font request to a third party.
export const STYLESHEET = `
:root {
--canvas: #f5f1ec;
@@ -13,12 +13,13 @@ export const STYLESHEET = `
--hairline: #d8d1c8;
--hairline-soft: #ebe7e1;
--orange: #ff5600;
+ --orange-ink: #a63a00;
--footer-bg: #eee8de;
--critical: #d23b1f;
--warning: #b26a00;
--info: #6b7280;
- --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
- --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+ --sans: "Avenir Next", Avenir, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
@@ -91,6 +92,39 @@ a:hover { text-decoration: underline; text-underline-offset: 3px; }
.next { margin-top: 8px; }
.next a { font-weight: 500; }
+/* ── privacy policy: an editorial, plain-language legal memo ── */
+.legal-page { max-width: 760px; }
+.legal-head { padding: 20px 0 12px; }
+.legal-kicker {
+ display: block; color: var(--orange-ink); font-family: var(--mono);
+ font-size: 11px; font-weight: 600; letter-spacing: 0.11em;
+}
+.legal-version {
+ display: flex; gap: 12px; align-items: baseline;
+ padding-bottom: 18px; border-bottom: 1px solid var(--hairline);
+ color: var(--ink-muted); font-family: var(--mono); font-size: 12px;
+}
+.legal-version span {
+ color: var(--ink-subtle); font-size: 10px; letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.legal-summary {
+ margin: 28px 0 42px; padding: 17px 19px 18px;
+ border: 1px solid var(--hairline); border-top: 4px solid var(--orange);
+ border-radius: 8px; background: var(--surface);
+}
+.legal-summary strong {
+ display: block; margin-bottom: 5px; font-family: var(--mono);
+ font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
+}
+.legal-summary p { margin: 0; }
+.legal-page section {
+ padding-top: 34px; border-top: 1px solid var(--hairline);
+}
+.legal-page section + section { margin-top: 34px; }
+.legal-page section h2 { margin-top: 0; }
+.legal-page section:last-child { padding-bottom: 12px; }
+
/* ── test panel ── */
.panel {
margin-top: 8px;
@@ -191,7 +225,113 @@ a:hover { text-decoration: underline; text-underline-offset: 3px; }
}
.foot-inner a { color: var(--ink-muted); }
.foot-inner a:hover { color: var(--ink); }
-.foot-links { display: flex; gap: 18px; }
+.foot-links { display: flex; gap: 18px; flex-wrap: wrap; align-items: baseline; }
+
+.analytics-settings {
+ appearance: none; border: 0; padding: 0; background: transparent;
+ color: var(--ink-muted); font: inherit; line-height: inherit; cursor: pointer;
+ text-decoration: none;
+}
+.analytics-settings:hover {
+ color: var(--ink); text-decoration: underline; text-underline-offset: 3px;
+}
+.analytics-settings--floating {
+ position: fixed; z-index: 60; left: 20px; bottom: 20px;
+ padding: 8px 10px; border: 1px solid var(--hairline); border-radius: 6px;
+ background: var(--surface); box-shadow: 3px 3px 0 rgba(17, 17, 17, 0.12);
+}
+.analytics-settings--floating:hover { background: var(--surface-2); }
+.analytics-settings:focus-visible,
+.analytics-consent a:focus-visible,
+.analytics-consent__choice:focus-visible {
+ outline: 3px solid var(--orange); outline-offset: 3px;
+}
+
+/* ── optional analytics consent ── */
+.analytics-consent {
+ position: fixed; z-index: 100; left: 50%; bottom: 18px;
+ width: min(760px, calc(100vw - 36px));
+ transform: translateX(-50%);
+ display: grid; grid-template-columns: minmax(0, 1fr) auto;
+ gap: 24px; align-items: end;
+ padding: 20px 20px 20px 22px;
+ border: 1px solid var(--ink); border-top: 4px solid var(--orange);
+ border-radius: 10px; background: var(--surface); color: var(--ink);
+ box-shadow: 8px 8px 0 rgba(17, 17, 17, 0.18);
+ animation: analytics-consent-enter 180ms ease-out both;
+ max-height: calc(100dvh - 20px); overflow-y: auto;
+ overscroll-behavior: contain;
+}
+.analytics-consent__copy { min-width: 0; }
+.analytics-consent__eyebrow {
+ display: block; margin-bottom: 6px; color: var(--orange-ink);
+ font-family: var(--mono); font-size: 10.5px; font-weight: 500;
+ letter-spacing: 0.1em;
+}
+.analytics-consent__title {
+ display: block; font-size: 17px; line-height: 1.3; font-weight: 600;
+ letter-spacing: -0.012em;
+}
+.analytics-consent__description {
+ max-width: 560px; margin: 6px 0 0; color: var(--ink-muted);
+ font-size: 13.5px; line-height: 1.45;
+}
+.analytics-consent__description a {
+ color: var(--ink); text-decoration: underline;
+ text-decoration-color: var(--hairline); text-underline-offset: 3px;
+}
+.analytics-consent__description a:hover { text-decoration-color: var(--ink); }
+.analytics-consent__status {
+ margin: 9px 0 0; color: #9e2f18;
+ font-size: 12.5px; line-height: 1.4; font-weight: 600;
+}
+.analytics-consent__choices {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
+ min-width: 270px;
+}
+.analytics-consent__choice {
+ appearance: none; min-height: 42px; margin: 0; padding: 9px 13px;
+ border: 1px solid var(--ink); border-radius: 7px;
+ font: 600 13px/1.2 var(--sans); cursor: pointer;
+ transition: transform 120ms ease, background-color 120ms ease, color 120ms ease;
+}
+.analytics-consent__choice:hover { transform: translateY(-1px); }
+.analytics-consent__choice:disabled {
+ cursor: wait; opacity: 0.58; transform: none;
+}
+.analytics-consent__choice--reject,
+.analytics-consent__choice--accept {
+ background: var(--surface); color: var(--ink);
+}
+.analytics-consent__choice--reject:hover,
+.analytics-consent__choice--accept:hover { background: var(--surface-2); }
+.analytics-consent-open .openseo-badge {
+ visibility: hidden;
+}
+.analytics-consent-open .analytics-settings--floating {
+ visibility: hidden;
+}
+@keyframes analytics-consent-enter {
+ from { opacity: 0; transform: translate(-50%, 10px); }
+ to { opacity: 1; transform: translate(-50%, 0); }
+}
+
+@media (max-width: 720px) {
+ .analytics-consent {
+ grid-template-columns: 1fr; gap: 16px; align-items: stretch;
+ padding: 18px;
+ }
+ .analytics-consent__choices { min-width: 0; }
+}
+@media (max-width: 420px) {
+ .analytics-consent { bottom: 10px; width: calc(100vw - 20px); }
+ .analytics-consent__choices { grid-template-columns: 1fr; }
+ .analytics-settings--floating { left: 10px; bottom: 10px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .analytics-consent { animation: none; }
+ .analytics-consent__choice { transition: none; }
+}
.openseo-badge {
position: fixed; right: 20px; bottom: 20px; z-index: 50;
diff --git a/badseo/src/worker-env.d.ts b/badseo/src/worker-env.d.ts
new file mode 100644
index 00000000..290914dc
--- /dev/null
+++ b/badseo/src/worker-env.d.ts
@@ -0,0 +1,15 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types --config=badseo/wrangler.jsonc --env=production --include-runtime=false badseo/src/worker-env.d.ts` (hash: a86cf968e75c0ed194ddfb87c6855162)
+interface __BaseEnv_Env {
+ CONSENT_LOG: KVNamespace;
+ CONSENT_RATE_LIMITER: RateLimit;
+ GA4_MEASUREMENT_ID: "G-7MXV9FH7SS";
+ GA4_ADMIN_VERIFIED: "false";
+}
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./index");
+ }
+ interface Env extends __BaseEnv_Env {}
+}
+interface Env extends __BaseEnv_Env {}
diff --git a/badseo/wrangler.jsonc b/badseo/wrangler.jsonc
index 427d0b1d..aab337e8 100644
--- a/badseo/wrangler.jsonc
+++ b/badseo/wrangler.jsonc
@@ -1,10 +1,25 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
- "name": "badseo",
+ // Keep the root/preview Worker distinct from the production custom-domain
+ // Worker so a plain `wrangler deploy` cannot replace production bindings.
+ "name": "badseo-dev",
"main": "src/index.ts",
- "compatibility_date": "2025-06-01",
+ // Latest compatibility date supported by this repo's pinned workerd.
+ "compatibility_date": "2026-07-02",
"workers_dev": true,
"preview_urls": true,
+ // Wrangler provisions separate local and production namespaces. Consent
+ // events are append-only and expire after the retention period in code.
+ "kv_namespaces": [{ "binding": "CONSENT_LOG" }],
+ "ratelimits": [
+ {
+ "name": "CONSENT_RATE_LIMITER",
+ "namespace_id": "172901",
+ // Local browser tests share one loopback address and exercise many
+ // consent transitions in a minute. Production keeps the strict limit.
+ "simple": { "limit": 100, "period": 60 },
+ },
+ ],
"observability": {
"enabled": true,
},
@@ -14,8 +29,25 @@
// Deploy with: wrangler deploy --env production
"env": {
"production": {
- "name": "badseo",
- "workers_dev": true,
+ // Preserve the Worker identity created by the original implicit
+ // `