Add Plausible and consent-gated GA4 to badseo.dev (#380)
* Add consent-gated GA4 to badseo.dev * Refine analytics consent banner * Add Plausible analytics to badseo.dev * Tighten analytics consent prompt * Update analytics consent copy
This commit is contained in:
parent
61c0b0c657
commit
987e5494a2
@ -6,6 +6,10 @@ This is not a completed-work log or a bug tracker. Never include secrets, creden
|
||||
|
||||
## Open
|
||||
|
||||
- [ ] `2026-07-10T22:32:40Z` — `codex` — While running the badseo audit from the workspace, sandboxed TSX failed with `listen EPERM` when creating its IPC socket under the temporary directory. Rerunning the same audit with local IPC permission succeeded; provide a sandbox-compatible TSX invocation for validation scripts.
|
||||
- [ ] `2026-07-10T22:23:50Z` — `codex` — While pushing the consent-banner refinement to PR #380, the SSH push hung without output and a separate remote verification hung as well; retrying the same push succeeded immediately. Surface an actionable SSH timeout or connection error instead of waiting indefinitely.
|
||||
- [ ] `2026-07-10T21:36:27Z` — `codex` — While using the repository's `webapp-testing` skill, its required Python Playwright import was unavailable in both system and bundled Python. The bundled Node Playwright runtime completed the check; document or provide that fallback, including Cloudflare Vite's default port 8787 rather than Vite's usual 5173.
|
||||
- [ ] `2026-07-10T21:32:10Z` — `codex` — While formatting the standalone `badseo` workspace, `pnpm exec prettier` failed because Prettier is only available from the repository root. Document the root-only formatter command or expose a workspace-local formatting script.
|
||||
- [ ] `2026-07-10T21:09:27Z` — `codex` — While building the TanStack/Cloudflare badseo app, Wrangler reported an EPERM writing its debug log under the user preferences directory even though the build succeeded. Set `WRANGLER_LOG_PATH` to a writable temporary path in sandboxed build commands or make the logging failure non-fatal and quiet.
|
||||
- [ ] `2026-07-10T17:53:20Z` — `codex` — While validating `.greptile/`, both `pnpm exec prettier --check` and the existing `pnpm format:check` attempted to reconcile `node_modules` and aborted because no TTY was available. Calling `node_modules/.bin/prettier` performed the non-installing check successfully; the agent/CI path needs a stable way to run package scripts without an interactive modules purge.
|
||||
- [ ] `2026-07-10T18:12:35Z` — `codex` — While validating referenced files in zsh, using `path` as a loop variable overwrote zsh's special `path` array and made commands such as `git`, `jq`, and `sed` appear missing later in the same shell. Use a neutral name such as `file_path` in shell loops.
|
||||
|
||||
@ -39,7 +39,7 @@ Browse them all at `/catalog`.
|
||||
badseo.dev is a TanStack Start app deployed to a Cloudflare Worker, following
|
||||
the same Vite and Cloudflare setup as the repository's `web/` app.
|
||||
|
||||
TanStack React routes render the healthy homepage and catalog.
|
||||
TanStack React routes render the healthy homepage, catalog, and privacy policy.
|
||||
A TanStack catch-all server route keeps the deliberate fixtures as raw
|
||||
responses with byte-level control over status codes, redirects, headers
|
||||
(`X-Robots-Tag`, `Link: …; rel=canonical`), timing, and the malformed `<head>`
|
||||
@ -52,6 +52,18 @@ states the audit needs to observe.
|
||||
**SEO-neutral**: it emits no `<h1>`–`<h6>` and no `<img>`.
|
||||
- `src/fixtures/*.ts` — the fixtures, one file per category.
|
||||
|
||||
## Analytics
|
||||
|
||||
Plausible Analytics loads on every page using the site-specific script supplied
|
||||
for badseo.dev. It provides the cookieless aggregate baseline without changing
|
||||
the Google Analytics consent choice.
|
||||
|
||||
Google Analytics uses measurement ID `G-7MXV9FH7SS`. The small consent script in
|
||||
`public/analytics.js` is shared by TanStack pages and raw fixture documents. It
|
||||
does not request Google's tag or set analytics cookies until a visitor accepts.
|
||||
The visitor can reject analytics or revisit the choice from **Cookie settings**
|
||||
in the footer. The choice is stored only in the visitor's browser.
|
||||
|
||||
## Run it locally
|
||||
|
||||
```bash
|
||||
@ -64,7 +76,7 @@ npm run dev # serves on http://localhost:8787
|
||||
The harness drives the **real** OpenSEO crawl + issue-detection functions
|
||||
(imported straight from `../src`) against a running badseo.dev, then asserts every
|
||||
fixture triggers exactly the issues it declares — and that the homepage,
|
||||
catalog, and support pages come back clean.
|
||||
catalog, privacy policy, and support pages come back clean.
|
||||
|
||||
```bash
|
||||
# with `npm run dev` running in another terminal:
|
||||
|
||||
116
badseo/public/analytics.js
Normal file
116
badseo/public/analytics.js
Normal file
@ -0,0 +1,116 @@
|
||||
/* oxlint-disable typescript-eslint/no-unsafe-argument, typescript-eslint/no-unsafe-assignment, typescript-eslint/no-unsafe-call, typescript-eslint/no-unsafe-member-access -- This standalone browser script defines Google's dynamic dataLayer globals. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var measurementId = "G-7MXV9FH7SS";
|
||||
var storageKey = "badseo.analyticsConsent";
|
||||
var granted = "granted";
|
||||
var denied = "denied";
|
||||
|
||||
function readConsent() {
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeConsent(value) {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, value);
|
||||
} catch {
|
||||
// The choice still applies to this page when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function loadAnalytics() {
|
||||
if (document.getElementById("ga4-script")) return;
|
||||
|
||||
window["ga-disable-" + measurementId] = false;
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.gtag = function () {
|
||||
window.dataLayer.push(arguments);
|
||||
};
|
||||
window.gtag("consent", "default", {
|
||||
analytics_storage: "granted",
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
});
|
||||
window.gtag("js", new Date());
|
||||
window.gtag("config", measurementId);
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.id = "ga4-script";
|
||||
script.async = true;
|
||||
script.src =
|
||||
"https://www.googletagmanager.com/gtag/js?id=" +
|
||||
encodeURIComponent(measurementId);
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function deleteAnalyticsCookies() {
|
||||
document.cookie.split(";").forEach(function (cookie) {
|
||||
var name = cookie.split("=")[0].trim();
|
||||
if (name !== "_ga" && !name.startsWith("_ga_")) return;
|
||||
|
||||
document.cookie = name + "=; Max-Age=0; path=/; SameSite=Lax";
|
||||
if (window.location.hostname.endsWith("badseo.dev")) {
|
||||
document.cookie =
|
||||
name + "=; Max-Age=0; path=/; domain=.badseo.dev; SameSite=Lax";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createBanner() {
|
||||
var banner = document.createElement("div");
|
||||
banner.className = "consent-banner";
|
||||
banner.setAttribute("role", "dialog");
|
||||
banner.setAttribute("aria-modal", "true");
|
||||
banner.setAttribute("aria-labelledby", "consent-title");
|
||||
banner.hidden = true;
|
||||
banner.innerHTML =
|
||||
'<div class="consent-copy">' +
|
||||
'<strong id="consent-title">This website uses cookies</strong>' +
|
||||
"<p>We use cookies to ensure you get the best experience.</p>" +
|
||||
'<a href="/privacy">Privacy policy</a>' +
|
||||
"</div>" +
|
||||
'<div class="consent-actions">' +
|
||||
'<button type="button" data-consent="denied">Reject</button>' +
|
||||
'<button type="button" class="consent-accept" data-consent="granted">Accept</button>' +
|
||||
"</div>";
|
||||
document.body.appendChild(banner);
|
||||
|
||||
banner.addEventListener("click", function (event) {
|
||||
var button = event.target.closest("[data-consent]");
|
||||
if (!button) return;
|
||||
|
||||
var previous = readConsent();
|
||||
var choice = button.getAttribute("data-consent");
|
||||
writeConsent(choice);
|
||||
banner.hidden = true;
|
||||
|
||||
if (choice === granted) {
|
||||
loadAnalytics();
|
||||
return;
|
||||
}
|
||||
|
||||
window["ga-disable-" + measurementId] = true;
|
||||
deleteAnalyticsCookies();
|
||||
if (previous === granted) window.location.reload();
|
||||
});
|
||||
|
||||
return banner;
|
||||
}
|
||||
|
||||
var banner = createBanner();
|
||||
var consent = readConsent();
|
||||
if (consent === granted) loadAnalytics();
|
||||
else if (consent !== denied) banner.hidden = false;
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (!event.target.closest("[data-cookie-settings]")) return;
|
||||
banner.hidden = false;
|
||||
banner.querySelector("button").focus();
|
||||
});
|
||||
})();
|
||||
@ -393,6 +393,115 @@ a:hover {
|
||||
.foot-links {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.footer-button {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--ink-muted);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.footer-button:hover {
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ── analytics consent ── */
|
||||
.consent-banner {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
width: min(350px, calc(100vw - 48px));
|
||||
padding: 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 20px 50px rgb(17 17 17 / 14%),
|
||||
0 2px 8px rgb(17 17 17 / 7%);
|
||||
animation: consent-in 180ms ease-out;
|
||||
}
|
||||
.consent-banner[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.consent-copy strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.consent-copy p {
|
||||
margin: 0 0 7px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.consent-copy a {
|
||||
color: var(--ink-muted);
|
||||
font-size: 12.5px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--hairline);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.consent-copy a:hover {
|
||||
color: var(--ink);
|
||||
text-decoration-color: var(--ink);
|
||||
}
|
||||
.consent-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.consent-actions button {
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
min-height: 36px;
|
||||
padding: 7px 12px;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.consent-actions button:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.consent-actions .consent-accept {
|
||||
border-color: var(--ink);
|
||||
background: var(--ink);
|
||||
color: #ffffff;
|
||||
}
|
||||
.consent-actions .consent-accept:hover {
|
||||
background: #000000;
|
||||
}
|
||||
@keyframes consent-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.consent-banner {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.consent-banner {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.openseo-badge {
|
||||
|
||||
@ -46,7 +46,7 @@ interface CrawlLink {
|
||||
|
||||
async function warmup(): Promise<void> {
|
||||
// Prime the dev server so healthy pages don't read as slow on a cold start.
|
||||
const paths = new Set<string>(["/", "/catalog"]);
|
||||
const paths = new Set<string>(["/", "/catalog", "/privacy"]);
|
||||
for (const f of allFixtures) for (const p of fixturePaths(f)) paths.add(p);
|
||||
await Promise.all(
|
||||
[...paths].map((p) =>
|
||||
@ -335,6 +335,7 @@ async function main() {
|
||||
// Non-fixture content pages must be clean.
|
||||
check("Homepage", "/", [], false);
|
||||
check("Catalog", "/catalog", [], false);
|
||||
check("Privacy policy", "/privacy", [], false);
|
||||
|
||||
const byCategory = new Map<string, Fixture[]>();
|
||||
for (const f of allFixtures) {
|
||||
|
||||
@ -22,6 +22,14 @@ export function SiteLayout({ children }: { children: ReactNode }) {
|
||||
<a href="/catalog">Catalog</a>
|
||||
<a href="https://github.com/every-app/open-seo">GitHub</a>
|
||||
<a href="https://openseo.so">OpenSEO</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
<button
|
||||
className="footer-button"
|
||||
type="button"
|
||||
data-cookie-settings
|
||||
>
|
||||
Cookie settings
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
// — not accidental noise from the layout.
|
||||
import { AUDIT_ISSUE_TYPES } from "../../src/shared/audit-issues";
|
||||
import type { Fixture, IssueId } from "./fixtures/types";
|
||||
import { PLAUSIBLE_INIT_SCRIPT, PLAUSIBLE_SCRIPT_SRC } from "./plausible";
|
||||
|
||||
export function escapeHtml(input: string): string {
|
||||
return input
|
||||
@ -44,7 +45,13 @@ interface DocumentOptions {
|
||||
|
||||
/** Build a complete HTML document string with exact <head> control. */
|
||||
export function renderDocument(opts: DocumentOptions): string {
|
||||
const head: string[] = ['<meta charset="utf-8">'];
|
||||
const head: string[] = [
|
||||
"<!-- Privacy-friendly analytics by Plausible -->",
|
||||
`<script async src="${PLAUSIBLE_SCRIPT_SRC}"></script>`,
|
||||
`<script>${PLAUSIBLE_INIT_SCRIPT}</script>`,
|
||||
'<script defer src="/analytics.js"></script>',
|
||||
'<meta charset="utf-8">',
|
||||
];
|
||||
head.push(
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
||||
);
|
||||
@ -102,6 +109,8 @@ function footerHtml(): string {
|
||||
<a href="/catalog">Catalog</a>
|
||||
<a href="https://github.com/every-app/open-seo">GitHub</a>
|
||||
<a href="https://openseo.so">OpenSEO</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
<button class="footer-button" type="button" data-cookie-settings>Cookie settings</button>
|
||||
</span>
|
||||
</div></footer>`;
|
||||
}
|
||||
|
||||
5
badseo/src/plausible.ts
Normal file
5
badseo/src/plausible.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const PLAUSIBLE_SCRIPT_SRC =
|
||||
"https://plausible.io/js/pa-Cr442m-9lp6sEF3_GhWNS.js";
|
||||
|
||||
export const PLAUSIBLE_INIT_SCRIPT =
|
||||
"window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init();";
|
||||
@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
|
||||
import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
import { Route as CatalogRouteImport } from './routes/catalog'
|
||||
import { Route as SplatRouteImport } from './routes/$'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
@ -25,6 +26,11 @@ const RobotsDottxtRoute = RobotsDottxtRouteImport.update({
|
||||
path: '/robots.txt',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PrivacyRoute = PrivacyRouteImport.update({
|
||||
id: '/privacy',
|
||||
path: '/privacy',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const CatalogRoute = CatalogRouteImport.update({
|
||||
id: '/catalog',
|
||||
path: '/catalog',
|
||||
@ -45,6 +51,7 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/$': typeof SplatRoute
|
||||
'/catalog': typeof CatalogRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/robots.txt': typeof RobotsDottxtRoute
|
||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||
}
|
||||
@ -52,6 +59,7 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/$': typeof SplatRoute
|
||||
'/catalog': typeof CatalogRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/robots.txt': typeof RobotsDottxtRoute
|
||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||
}
|
||||
@ -60,21 +68,31 @@ export interface FileRoutesById {
|
||||
'/': typeof IndexRoute
|
||||
'/$': typeof SplatRoute
|
||||
'/catalog': typeof CatalogRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/robots.txt': typeof RobotsDottxtRoute
|
||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/$' | '/catalog' | '/robots.txt' | '/sitemap.xml'
|
||||
fullPaths:
|
||||
'/' | '/$' | '/catalog' | '/privacy' | '/robots.txt' | '/sitemap.xml'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/$' | '/catalog' | '/robots.txt' | '/sitemap.xml'
|
||||
id: '__root__' | '/' | '/$' | '/catalog' | '/robots.txt' | '/sitemap.xml'
|
||||
to: '/' | '/$' | '/catalog' | '/privacy' | '/robots.txt' | '/sitemap.xml'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/$'
|
||||
| '/catalog'
|
||||
| '/privacy'
|
||||
| '/robots.txt'
|
||||
| '/sitemap.xml'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SplatRoute: typeof SplatRoute
|
||||
CatalogRoute: typeof CatalogRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
RobotsDottxtRoute: typeof RobotsDottxtRoute
|
||||
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
|
||||
}
|
||||
@ -95,6 +113,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof RobotsDottxtRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/privacy': {
|
||||
id: '/privacy'
|
||||
path: '/privacy'
|
||||
fullPath: '/privacy'
|
||||
preLoaderRoute: typeof PrivacyRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/catalog': {
|
||||
id: '/catalog'
|
||||
path: '/catalog'
|
||||
@ -123,6 +148,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
SplatRoute: SplatRoute,
|
||||
CatalogRoute: CatalogRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
RobotsDottxtRoute: RobotsDottxtRoute,
|
||||
SitemapDotxmlRoute: SitemapDotxmlRoute,
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
Outlet,
|
||||
Scripts,
|
||||
} from "@tanstack/react-router";
|
||||
import { PLAUSIBLE_INIT_SCRIPT, PLAUSIBLE_SCRIPT_SRC } from "../plausible";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
@ -27,6 +28,10 @@ export const Route = createRootRoute({
|
||||
},
|
||||
{ rel: "stylesheet", href: "/styles.css" },
|
||||
],
|
||||
scripts: [
|
||||
{ async: true, src: PLAUSIBLE_SCRIPT_SRC },
|
||||
{ defer: true, src: "/analytics.js" },
|
||||
],
|
||||
}),
|
||||
component: RootComponent,
|
||||
});
|
||||
@ -36,6 +41,7 @@ function RootComponent() {
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
<script dangerouslySetInnerHTML={{ __html: PLAUSIBLE_INIT_SCRIPT }} />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
|
||||
120
badseo/src/routes/privacy.tsx
Normal file
120
badseo/src/routes/privacy.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SiteLayout } from "../components/site-layout";
|
||||
|
||||
export const Route = createFileRoute("/privacy")({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: "Privacy policy | badseo.dev" },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"How badseo.dev uses Plausible, Google Analytics, and Cloudflare and how visitors control analytics cookies.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: PrivacyPage,
|
||||
});
|
||||
|
||||
function PrivacyPage() {
|
||||
return (
|
||||
<SiteLayout>
|
||||
<main className="main">
|
||||
<h1>Privacy policy</h1>
|
||||
<p className="lede">
|
||||
This policy explains the limited information processed when you visit
|
||||
badseo.dev. Last updated July 10, 2026.
|
||||
</p>
|
||||
|
||||
<h2>Who operates this site</h2>
|
||||
<p>
|
||||
badseo.dev is operated by Every App, Inc. as a public test site for
|
||||
OpenSEO. The site has no accounts, forms, purchases, or user-submitted
|
||||
content. Privacy questions and requests can be sent to{" "}
|
||||
<a href="mailto:ben@openseo.so">ben@openseo.so</a>.
|
||||
</p>
|
||||
|
||||
<h2>Plausible Analytics</h2>
|
||||
<p>
|
||||
We use Plausible Analytics on every page to understand aggregate
|
||||
traffic and which technical SEO examples people use. Plausible does
|
||||
not set cookies or create a persistent identifier for you. It provides
|
||||
aggregate measurements such as page views, referring sites, browser
|
||||
and device categories, and country-level location.
|
||||
</p>
|
||||
<p>
|
||||
Plausible is provided by Plausible Analytics OÜ. Learn more in the{" "}
|
||||
<a href="https://plausible.io/data-policy">
|
||||
Plausible Analytics data policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<h2>Google Analytics</h2>
|
||||
<p>
|
||||
Separately, with your permission, we use Google Analytics 4 to
|
||||
understand traffic in the analytics product many OpenSEO users use.
|
||||
The Google tag does not load until you select <strong>Accept</strong>
|
||||
in the analytics banner.
|
||||
</p>
|
||||
<p>
|
||||
Google Analytics may process the page address and title, referring
|
||||
page, interactions such as page views, scrolls, and outbound clicks,
|
||||
browser and device information, approximate location derived from your
|
||||
IP address, and randomly generated identifiers. It may set first-party
|
||||
cookies including <code>_ga</code> and{" "}
|
||||
<code>_ga_<container-id></code> to distinguish visitors and
|
||||
sessions.
|
||||
</p>
|
||||
<p>
|
||||
If you reject analytics, no Google Analytics request is made. Your
|
||||
choice is stored in your browser's local storage so the site can
|
||||
remember it. You can change your choice at any time using{" "}
|
||||
<strong>Cookie settings</strong> in the footer. Rejecting after a
|
||||
previous acceptance disables analytics and removes accessible Google
|
||||
Analytics cookies from this site. This choice controls Google
|
||||
Analytics; the cookieless Plausible measurement described above
|
||||
remains active.
|
||||
</p>
|
||||
<p>
|
||||
Learn more about{" "}
|
||||
<a href="https://policies.google.com/technologies/partner-sites">
|
||||
how Google uses information from sites that use its services
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="https://policies.google.com/privacy">
|
||||
Google's privacy practices
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<h2>Cloudflare</h2>
|
||||
<p>
|
||||
Cloudflare hosts, delivers, and protects badseo.dev. It receives
|
||||
ordinary request information such as your IP address, request headers,
|
||||
requested URL, and time of access to provide the site, prevent abuse,
|
||||
and diagnose failures. We do not create a separate visitor access-log
|
||||
database. Learn more in{" "}
|
||||
<a href="https://www.cloudflare.com/privacypolicy/">
|
||||
Cloudflare's privacy policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<h2>International processing and your rights</h2>
|
||||
<p>
|
||||
Google, Plausible, and Cloudflare may process information in the
|
||||
United States, the European Economic Area, and other countries.
|
||||
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 us to make a request. You may also complain to
|
||||
the privacy or data-protection authority where you live.
|
||||
</p>
|
||||
|
||||
<h2>Changes</h2>
|
||||
<p>
|
||||
We will update the date above when this policy changes materially.
|
||||
</p>
|
||||
</main>
|
||||
</SiteLayout>
|
||||
);
|
||||
}
|
||||
@ -79,7 +79,7 @@ Sitemap: ${origin}/sitemap.xml
|
||||
export function sitemapResponse(request: Request): Response {
|
||||
const url = new URL(request.url);
|
||||
const origin = requestOrigin(request, url);
|
||||
const paths = new Set<string>(["/"]);
|
||||
const paths = new Set<string>(["/", "/privacy"]);
|
||||
for (const fixture of sitemapFixtures) {
|
||||
for (const path of fixturePaths(fixture)) paths.add(path);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user