This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
# Public site origin used for SEO metadata, sitemap, and robots.txt.
|
# Public site origin used for SEO metadata, sitemap, and robots.txt.
|
||||||
BASE_URL=https://guide.sudloh.com
|
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
|
||||||
|
|
||||||
# Local-only demo mode serves sanitized fixtures and bypasses external services.
|
# Local-only demo mode serves sanitized fixtures and bypasses external services.
|
||||||
# Keep this false in every deployed environment.
|
# Keep this false in every deployed environment.
|
||||||
BUZZ_DEMO_MODE=false
|
BUZZ_DEMO_MODE=false
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
BASE_URL: https://guide.sudloh.com
|
BASE_URL: https://guide.sudloh.com
|
||||||
|
NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID: ca-pub-9687404323559597
|
||||||
REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet
|
REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet
|
||||||
DEPLOY_NAMESPACE: buzz-sheet
|
DEPLOY_NAMESPACE: buzz-sheet
|
||||||
KUBE_API_SERVER: https://100.112.189.33:6443/
|
KUBE_API_SERVER: https://100.112.189.33:6443/
|
||||||
@@ -32,6 +33,7 @@ jobs:
|
|||||||
docker build \
|
docker build \
|
||||||
--target app \
|
--target app \
|
||||||
--build-arg BASE_URL="$BASE_URL" \
|
--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 NEXT_DEPLOYMENT_ID=${{ gitea.sha }} \
|
||||||
--build-arg VCS_REF=${{ gitea.sha }} \
|
--build-arg VCS_REF=${{ gitea.sha }} \
|
||||||
--tag ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} \
|
--tag ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} \
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ FROM node:22-bookworm-slim AS next-builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NEXT_DEPLOYMENT_ID=container-build
|
ARG NEXT_DEPLOYMENT_ID=container-build
|
||||||
ARG BASE_URL
|
ARG BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID
|
||||||
ENV BASE_URL=${BASE_URL}
|
ENV BASE_URL=${BASE_URL}
|
||||||
ENV NEXT_DEPLOYMENT_ID=${NEXT_DEPLOYMENT_ID}
|
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
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
COPY --from=dependencies /app/node_modules ./node_modules
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||||
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
|
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
|
||||||
import { getCachedPublicGuide } from "@/lib/cache/public";
|
import { getCachedPublicGuide } from "@/lib/cache/public";
|
||||||
|
|
||||||
@@ -25,5 +26,10 @@ export default async function GuideSectionPage({ params }: { params: Params }) {
|
|||||||
const { character, page } = await params;
|
const { character, page } = await params;
|
||||||
const guide = await getCachedPublicGuide(character);
|
const guide = await getCachedPublicGuide(character);
|
||||||
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
|
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
|
||||||
return <StructuredGuideShell guide={guide} section={page} />;
|
return (
|
||||||
|
<>
|
||||||
|
<GoogleAdsense />
|
||||||
|
<StructuredGuideShell guide={guide} section={page} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||||
import { StructuredGuideShell } from "@/components/public/structured-guide";
|
import { StructuredGuideShell } from "@/components/public/structured-guide";
|
||||||
import { getCachedPublicGuide } from "@/lib/cache/public";
|
import { getCachedPublicGuide } from "@/lib/cache/public";
|
||||||
|
|
||||||
@@ -31,5 +32,10 @@ export default async function GuideOverviewPage({
|
|||||||
}) {
|
}) {
|
||||||
const guide = await getCachedPublicGuide((await params).character);
|
const guide = await getCachedPublicGuide((await params).character);
|
||||||
if (!guide) notFound();
|
if (!guide) notFound();
|
||||||
return <StructuredGuideShell guide={guide} />;
|
return (
|
||||||
|
<>
|
||||||
|
<GoogleAdsense />
|
||||||
|
<StructuredGuideShell guide={guide} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { connection } from "next/server";
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { LiveRefresh } from "@/components/live-refresh";
|
import { LiveRefresh } from "@/components/live-refresh";
|
||||||
|
import { GoogleAdsense } from "@/components/public/google-adsense";
|
||||||
import { SiteFooter } from "@/components/public/site-footer";
|
import { SiteFooter } from "@/components/public/site-footer";
|
||||||
import { StructuredDirectory } from "@/components/public/structured-directory";
|
import { StructuredDirectory } from "@/components/public/structured-directory";
|
||||||
import { SiteHeader } from "@/components/public/site-header";
|
import { SiteHeader } from "@/components/public/site-header";
|
||||||
@@ -16,6 +17,7 @@ async function DirectoryContent() {
|
|||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh flex-col bg-background">
|
<div className="flex min-h-svh flex-col bg-background">
|
||||||
|
<GoogleAdsense />
|
||||||
<LiveRefresh endpoint="/api/events/directory" />
|
<LiveRefresh endpoint="/api/events/directory" />
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14">
|
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14">
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
|
|
||||||
|
const clientId = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID?.trim();
|
||||||
|
|
||||||
|
export function GoogleAdsense() {
|
||||||
|
const { data: session, error, isPending } = authClient.useSession();
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV !== "production" ||
|
||||||
|
!clientId ||
|
||||||
|
isPending ||
|
||||||
|
error ||
|
||||||
|
session
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Script
|
||||||
|
id="google-adsense"
|
||||||
|
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(clientId)}`}
|
||||||
|
strategy="afterInteractive"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0
|
||||||
@@ -91,7 +91,12 @@ describe("production deployment contract", () => {
|
|||||||
expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
|
expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
|
||||||
expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]');
|
expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]');
|
||||||
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
|
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
|
||||||
expect(dockerfile).toContain("ARG BASE_URL\nENV BASE_URL=${BASE_URL}");
|
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).toContain("backend/discord.ts");
|
expect(dockerfile).toContain("backend/discord.ts");
|
||||||
expect(dockerfile).toContain("./worker/discord.js");
|
expect(dockerfile).toContain("./worker/discord.js");
|
||||||
});
|
});
|
||||||
@@ -106,6 +111,9 @@ describe("production deployment contract", () => {
|
|||||||
expect(workflow).toContain("kubectl kustomize k8s/");
|
expect(workflow).toContain("kubectl kustomize k8s/");
|
||||||
expect(workflow).toContain("--target app");
|
expect(workflow).toContain("--target app");
|
||||||
expect(workflow).toContain('--build-arg BASE_URL="$BASE_URL"');
|
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).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
|
expect(workflow).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
|
||||||
expect(workflow).toContain("--target migration");
|
expect(workflow).toContain("--target migration");
|
||||||
expect(workflow).not.toContain("platforms: linux/arm64");
|
expect(workflow).not.toContain("platforms: linux/arm64");
|
||||||
@@ -167,6 +175,9 @@ describe("environment template contract", () => {
|
|||||||
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
|
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
|
||||||
}
|
}
|
||||||
expect(environmentExample).toContain("BUZZ_DEMO_MODE=false");
|
expect(environmentExample).toContain("BUZZ_DEMO_MODE=false");
|
||||||
|
expect(environmentExample).toContain(
|
||||||
|
"NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs Discord catalog sync as an isolated worker", async () => {
|
it("runs Discord catalog sync as an isolated worker", async () => {
|
||||||
|
|||||||
@@ -55,4 +55,25 @@ describe("public rendering performance", () => {
|
|||||||
sitemap: "https://guide.example.test/sitemap.xml",
|
sitemap: "https://guide.example.test/sitemap.xml",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads AdSense only after confirming the visitor is signed out", async () => {
|
||||||
|
const [adsense, home, guide, guideSection, adsTxt] = await Promise.all([
|
||||||
|
repositoryFile("components/public/google-adsense.tsx"),
|
||||||
|
repositoryFile("app/page.tsx"),
|
||||||
|
repositoryFile("app/[character]/page.tsx"),
|
||||||
|
repositoryFile("app/[character]/[page]/page.tsx"),
|
||||||
|
repositoryFile("public/ads.txt"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(adsense).toContain("authClient.useSession()");
|
||||||
|
expect(adsense).toContain('process.env.NODE_ENV !== "production"');
|
||||||
|
expect(adsense).toMatch(/isPending \|\|\s+error \|\|\s+session/u);
|
||||||
|
expect(adsense).toContain("pagead2.googlesyndication.com");
|
||||||
|
expect(home).toContain("<GoogleAdsense />");
|
||||||
|
expect(guide).toContain("<GoogleAdsense />");
|
||||||
|
expect(guideSection).toContain("<GoogleAdsense />");
|
||||||
|
expect(adsTxt.trim()).toBe(
|
||||||
|
"google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user