chore(site) : remove apex domain routing and Google AdSense
This commit is contained in:
+1
-4
@@ -1,9 +1,6 @@
|
||||
# Public site origin used for SEO metadata, sitemap, and robots.txt.
|
||||
BASE_URL=https://guide.sudloh.com
|
||||
|
||||
# Public AdSense publisher client ID. Leave unset to disable ads locally.
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597
|
||||
|
||||
# PostgreSQL
|
||||
DATABASE_URL=postgresql://buzz_sheet:[email protected]:5432/buzz_sheet?sslmode=require
|
||||
DATABASE_POOL_SIZE=10
|
||||
@@ -12,7 +9,7 @@ DATABASE_POOL_SIZE=10
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
BETTER_AUTH_SECRET=replace-with-at-least-32-random-bytes
|
||||
# Comma-separated site origins; also used for auth callback hosts and API mutations.
|
||||
# Production: https://guide.sudloh.com,https://sudloh.com
|
||||
# Production: https://guide.sudloh.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
|
||||
# Optional for local development; required by the production deployment.
|
||||
GOOGLE_CLIENT_ID=replace-with-google-oauth-client-id
|
||||
|
||||
@@ -7,8 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
BASE_URL: https://guide.sudloh.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: https://guide.sudloh.com,https://sudloh.com
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID: ca-pub-9687404323559597
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: https://guide.sudloh.com
|
||||
REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet
|
||||
DEPLOY_NAMESPACE: buzz-sheet
|
||||
KUBE_API_SERVER: https://100.112.189.33:6443/
|
||||
@@ -69,7 +68,6 @@ jobs:
|
||||
docker build \
|
||||
--target app \
|
||||
--build-arg BASE_URL="$BASE_URL" \
|
||||
--build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID" \
|
||||
--build-arg NEXT_DEPLOYMENT_ID=${{ gitea.sha }} \
|
||||
--build-arg VCS_REF=${{ gitea.sha }} \
|
||||
--tag ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} \
|
||||
|
||||
@@ -26,10 +26,8 @@ FROM node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e97188
|
||||
WORKDIR /app
|
||||
ARG NEXT_DEPLOYMENT_ID=container-build
|
||||
ARG BASE_URL
|
||||
ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID
|
||||
ENV BASE_URL=${BASE_URL}
|
||||
ENV NEXT_DEPLOYMENT_ID=${NEXT_DEPLOYMENT_ID}
|
||||
ENV NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID}
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
A Thai-first Genshin Impact guide CMS for publishing character builds, teams,
|
||||
rotations, and comparisons from structured data.
|
||||
|
||||
**Live site:** [guide.sudloh.com](https://guide.sudloh.com) and [sudloh.com](https://sudloh.com)
|
||||
**Live site:** [guide.sudloh.com](https://guide.sudloh.com)
|
||||
|
||||
Buzz Guide is an independent community project and is not affiliated with or
|
||||
endorsed by HoYoverse. The repository and deployment resources retain the
|
||||
@@ -296,7 +296,7 @@ Kustomize resources live in `k8s/` and define:
|
||||
|
||||
- Namespace `buzz-sheet`.
|
||||
- Two rolling web replicas, one outbox worker, and one Discord catalog worker.
|
||||
- ClusterIP port 3000 and Traefik ingress for `guide.sudloh.com` and `sudloh.com`.
|
||||
- ClusterIP port 3000 and Traefik ingress for `guide.sudloh.com`.
|
||||
- Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route.
|
||||
- Startup, liveness, and dependency-aware readiness probes.
|
||||
- Web requests of 500m CPU/1 GiB and limits of 1 CPU/2 GiB.
|
||||
@@ -352,15 +352,13 @@ ConfigMap or commit them to this repository.
|
||||
Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in the application environment.
|
||||
In Google Cloud, register `{BETTER_AUTH_URL}/api/auth/callback/google` as an
|
||||
authorized redirect URI for each environment (for production,
|
||||
`https://guide.sudloh.com/api/auth/callback/google` and
|
||||
`https://sudloh.com/api/auth/callback/google`).
|
||||
`https://guide.sudloh.com/api/auth/callback/google`).
|
||||
|
||||
Both production domains serve the same application. `BASE_URL` remains
|
||||
`https://guide.sudloh.com` for canonical metadata and generated links.
|
||||
`BETTER_AUTH_TRUSTED_ORIGINS` lists both HTTPS origins so sign-in callbacks and
|
||||
API mutations work on either domain. Sessions use separate host cookies.
|
||||
Point `sudloh.com` at the same Cloudflare/Traefik origin as `guide.sudloh.com`,
|
||||
enable edge TLS, and allow both hostnames in the Cloudflare Turnstile widget.
|
||||
The production application is served only at `guide.sudloh.com`. `BASE_URL` and
|
||||
`BETTER_AUTH_TRUSTED_ORIGINS` use `https://guide.sudloh.com` for canonical metadata,
|
||||
generated links, sign-in callbacks, and API mutations. Point `guide.sudloh.com`
|
||||
at the Cloudflare/Traefik origin, enable edge TLS, and allow that hostname in the
|
||||
Cloudflare Turnstile widget. The bare `sudloh.com` domain is not routed to this app.
|
||||
For an existing cluster, apply the updated CI role once before the next release:
|
||||
`kubectl apply --namespace buzz-sheet --filename k8s/base/ci-rbac.yaml`.
|
||||
CI updates the existing ingress and trusted origins during each rollout.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getCachedPublicGuide, getCachedPublicGuideSummary } from "@/lib/cache/public";
|
||||
|
||||
@@ -26,10 +25,5 @@ export default async function GuideSectionPage({ params }: { params: Params }) {
|
||||
const { character, page } = await params;
|
||||
const guide = await getCachedPublicGuide(character, page);
|
||||
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
|
||||
return (
|
||||
<>
|
||||
<GoogleAdsense />
|
||||
<StructuredGuideShell guide={guide} section={page} />
|
||||
</>
|
||||
);
|
||||
return <StructuredGuideShell guide={guide} section={page} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||
import { StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getCachedPublicGuide, getCachedPublicGuideSummary } from "@/lib/cache/public";
|
||||
|
||||
@@ -32,10 +31,5 @@ export default async function GuideOverviewPage({
|
||||
}) {
|
||||
const guide = await getCachedPublicGuide((await params).character);
|
||||
if (!guide) notFound();
|
||||
return (
|
||||
<>
|
||||
<GoogleAdsense />
|
||||
<StructuredGuideShell guide={guide} />
|
||||
</>
|
||||
);
|
||||
return <StructuredGuideShell guide={guide} />;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export const metadata: Metadata = {
|
||||
},
|
||||
description: "ไกด์ตัวละครจากช่องเกนชินไม่ใช่เกมมือถือ",
|
||||
keywords: ["เกนชิน", "เกนชินไม่ใช่เกมมือถือ", "Buzz", "ไกด์", "สุดหล่อ"],
|
||||
other: { "google-adsense-account": "ca-pub-9687404323559597" },
|
||||
icons: { apple: "/icon-192.png" },
|
||||
appleWebApp: { capable: true, title: "Buzz Guide" },
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Suspense } from "react";
|
||||
|
||||
import { AccountLinkErrorRedirect } from "@/components/auth/account-link-error-redirect";
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||
import { DirectorySkeleton } from "@/components/public/directory-skeleton";
|
||||
import { SiteFooter } from "@/components/public/site-footer";
|
||||
import { StructuredDirectory } from "@/components/public/structured-directory";
|
||||
@@ -19,7 +18,6 @@ async function DirectoryContent() {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col bg-background">
|
||||
<GoogleAdsense />
|
||||
<LiveRefresh endpoint="/api/events/directory" />
|
||||
<Suspense fallback={null}>
|
||||
<AccountLinkErrorRedirect />
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const clientId = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID?.trim();
|
||||
|
||||
export function GoogleAdsense() {
|
||||
if (process.env.NODE_ENV !== "production" || !clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<script
|
||||
async
|
||||
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(clientId)}`}
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
data:
|
||||
BASE_URL: https://guide.sudloh.com
|
||||
BETTER_AUTH_URL: https://guide.sudloh.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: https://guide.sudloh.com,https://sudloh.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: https://guide.sudloh.com
|
||||
# Traefik supplies X-Forwarded-For and does not trust incoming forwarded headers.
|
||||
# The network policy allows only Traefik to reach the app.
|
||||
TRUSTED_CLIENT_IP_HEADER: x-forwarded-for
|
||||
|
||||
@@ -9,16 +9,6 @@ metadata:
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: sudloh.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: buzz-sheet
|
||||
port:
|
||||
number: 3000
|
||||
- host: guide.sudloh.com
|
||||
http:
|
||||
paths:
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
export function securityHeaders(production: boolean, emojiImageCdn: string) {
|
||||
const commentStorageOrigin = process.env.S3_PUBLIC_URL ? new URL(process.env.S3_PUBLIC_URL).origin : "";
|
||||
const ads = "https://*.googlesyndication.com https://*.doubleclick.net https://*.googleadservices.com https://*.googletagservices.com https://www.google.com";
|
||||
const cloudflareAnalytics = "https://static.cloudflareinsights.com";
|
||||
const adConnections = "https://*.adtrafficquality.google";
|
||||
const policy = [
|
||||
"default-src 'self'",
|
||||
`script-src 'self' 'unsafe-inline' ${production ? "" : "'unsafe-eval'"} https://challenges.cloudflare.com ${cloudflareAnalytics} ${adConnections} ${ads}`,
|
||||
`script-src 'self' 'unsafe-inline' ${production ? "" : "'unsafe-eval'"} https://challenges.cloudflare.com ${cloudflareAnalytics}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
`img-src 'self' data: blob: https://buzz-cdn.astrxl.dev ${commentStorageOrigin} ${emojiImageCdn} ${ads}`,
|
||||
`img-src 'self' data: blob: https://buzz-cdn.astrxl.dev ${commentStorageOrigin} ${emojiImageCdn}`,
|
||||
"font-src 'self' data:",
|
||||
`connect-src 'self' https://buzz-cdn.astrxl.dev https://challenges.cloudflare.com ${adConnections} ${ads}${production ? "" : " ws: wss:"}`,
|
||||
`frame-src https://challenges.cloudflare.com https://www.youtube-nocookie.com ${ads}`,
|
||||
`connect-src 'self' https://buzz-cdn.astrxl.dev https://challenges.cloudflare.com${production ? "" : " ws: wss:"}`,
|
||||
"frame-src https://challenges.cloudflare.com https://www.youtube-nocookie.com",
|
||||
"media-src 'self' blob:",
|
||||
"worker-src 'self' blob:",
|
||||
"object-src 'none'",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0
|
||||
@@ -25,10 +25,12 @@ describe("production deployment contract", () => {
|
||||
expect(deployment).toContain('drop: ["ALL"]');
|
||||
});
|
||||
|
||||
it("exposes only the app through both edge-TLS hosts", async () => {
|
||||
const [service, ingress] = await Promise.all([
|
||||
it("exposes the app only through the guide edge-TLS host", async () => {
|
||||
const [service, ingress, config, workflow] = await Promise.all([
|
||||
repositoryFile("k8s/base/service.yaml"),
|
||||
repositoryFile("k8s/base/ingress.yaml"),
|
||||
repositoryFile("k8s/base/configmap.yaml"),
|
||||
repositoryFile(".gitea/workflows/ci.yml"),
|
||||
]);
|
||||
|
||||
expect(service).toContain("type: ClusterIP");
|
||||
@@ -36,7 +38,10 @@ describe("production deployment contract", () => {
|
||||
expect(service).toContain("targetPort: http");
|
||||
expect(ingress).toContain("ingressClassName: traefik");
|
||||
expect(ingress).toContain("host: guide.sudloh.com");
|
||||
expect(ingress).toContain("host: sudloh.com");
|
||||
expect(ingress).not.toContain("host: sudloh.com");
|
||||
for (const configuration of [config, workflow]) {
|
||||
expect(configuration).toMatch(/^ BETTER_AUTH_TRUSTED_ORIGINS: https:\/\/guide\.sudloh\.com$/mu);
|
||||
}
|
||||
expect(ingress).toContain(
|
||||
'traefik.ingress.kubernetes.io/read-timeout: "200"',
|
||||
);
|
||||
@@ -100,10 +105,7 @@ describe("production deployment contract", () => {
|
||||
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
|
||||
expect(dockerfile).toContain("ARG BASE_URL");
|
||||
expect(dockerfile).toContain("ENV BASE_URL=${BASE_URL}");
|
||||
expect(dockerfile).toContain("ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
|
||||
expect(dockerfile).toContain(
|
||||
"ENV NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID}",
|
||||
);
|
||||
expect(dockerfile).not.toContain("NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
|
||||
expect(dockerfile).toContain("backend/discord.ts");
|
||||
expect(dockerfile).toMatch(
|
||||
/bun build scripts\/outbox-worker\.ts[\s\S]*?--conditions=react-server[\s\S]*?--outfile=dist\/outbox-worker\.js/u,
|
||||
@@ -137,9 +139,7 @@ describe("production deployment contract", () => {
|
||||
expect(workflow).toContain("--insecure-skip-tls-verify=true");
|
||||
expect(workflow).toContain("--target app");
|
||||
expect(workflow).toContain('--build-arg BASE_URL="$BASE_URL"');
|
||||
expect(workflow).toContain(
|
||||
'--build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID"',
|
||||
);
|
||||
expect(workflow).not.toContain("NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
|
||||
expect(workflow).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
|
||||
expect(workflow).toContain("--target migration");
|
||||
expect(workflow).not.toContain("platforms: linux/arm64");
|
||||
@@ -228,9 +228,7 @@ describe("environment template contract", () => {
|
||||
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
|
||||
}
|
||||
expect(environmentExample).not.toContain("BUZZ_DEMO_MODE");
|
||||
expect(environmentExample).toContain(
|
||||
"NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597",
|
||||
);
|
||||
expect(environmentExample).not.toContain("NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
|
||||
});
|
||||
|
||||
it("runs Discord catalog sync as an isolated worker", async () => {
|
||||
|
||||
@@ -59,34 +59,25 @@ describe("public rendering performance", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads AdSense for every visitor, including signed-in admins", async () => {
|
||||
const [adsense, home, guide, guideSection, adsTxt] = await Promise.all([
|
||||
repositoryFile("components/public/google-adsense.tsx"),
|
||||
it("keeps public pages free of AdSense", async () => {
|
||||
const pages = await Promise.all([
|
||||
repositoryFile("app/layout.tsx"),
|
||||
repositoryFile("app/page.tsx"),
|
||||
repositoryFile("app/[character]/page.tsx"),
|
||||
repositoryFile("app/[character]/[page]/page.tsx"),
|
||||
repositoryFile("public/ads.txt"),
|
||||
]);
|
||||
|
||||
expect(adsense).not.toContain("authClient.useSession()");
|
||||
expect(adsense).not.toContain('from "@/lib/auth/client"');
|
||||
expect(adsense).toContain('process.env.NODE_ENV !== "production"');
|
||||
expect(adsense).toContain("pagead2.googlesyndication.com");
|
||||
expect(adsense).toContain("<script");
|
||||
expect(adsense).not.toContain('from "next/script"');
|
||||
expect(home).toContain("<GoogleAdsense />");
|
||||
expect(guide).toContain("<GoogleAdsense />");
|
||||
expect(guideSection).toContain("<GoogleAdsense />");
|
||||
expect(adsTxt.trim()).toBe(
|
||||
"google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0",
|
||||
);
|
||||
for (const page of pages) {
|
||||
expect(page).not.toMatch(/adsense|googlesyndication|ca-pub-/iu);
|
||||
}
|
||||
await expect(repositoryFile("public/ads.txt")).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("allows the production analytics and advertising endpoints", async () => {
|
||||
it("allows production analytics without advertising endpoints", async () => {
|
||||
const headers = await repositoryFile("lib/security/headers.ts");
|
||||
|
||||
expect(headers).toContain("https://static.cloudflareinsights.com");
|
||||
expect(headers).toContain("https://*.adtrafficquality.google");
|
||||
expect(headers).not.toMatch(/googlesyndication|doubleclick|googleadservices|googletagservices|adtrafficquality/u);
|
||||
});
|
||||
|
||||
it("accepts Next.js deployment metadata on the document root", async () => {
|
||||
|
||||
Reference in New Issue
Block a user