From 4fc0442eb3f6a607703f0ed51222d12b4bc04295 Mon Sep 17 00:00:00 2001 From: Bunyasit Fang Date: Tue, 5 May 2026 04:53:08 -0700 Subject: [PATCH 01/30] =?UTF-8?q?feat(cdn-url-utility):=20add=20lib/cdn-ur?= =?UTF-8?q?l.ts=20with=20toCdnUrl()=20B2=E2=86=92CDN=20rewriter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrites s3.*.backblazeb2.com and f005.backblazeb2.com URLs to cdn.passionseed.org - Pass-through for already-CDN and non-B2 URLs - Handles null/undefined/empty input gracefully - 17 Jest tests, 100% branch coverage Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- lib/cdn-url.test.ts | 118 ++++++++++++++++++++++++++++++++++++++++++++ lib/cdn-url.ts | 66 +++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 lib/cdn-url.test.ts create mode 100644 lib/cdn-url.ts diff --git a/lib/cdn-url.test.ts b/lib/cdn-url.test.ts new file mode 100644 index 00000000..be5e6ee4 --- /dev/null +++ b/lib/cdn-url.test.ts @@ -0,0 +1,118 @@ +import { toCdnUrl } from "./cdn-url"; + +describe("toCdnUrl", () => { + describe("S3-style B2 endpoint", () => { + it("rewrites us-east-005 B2 URL to CDN URL", () => { + const input = + "https://pseed-dev.s3.us-east-005.backblazeb2.com/images/test.webp"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/images/test.webp"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("rewrites us-west-000 B2 URL to CDN URL", () => { + const input = + "https://pseed-dev.s3.us-west-000.backblazeb2.com/submissions/abc/def.jpg"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/submissions/abc/def.jpg"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("handles different bucket names", () => { + const input = + "https://my-bucket.s3.eu-central-003.backblazeb2.com/docs/report.pdf"; + const expected = + "https://cdn.passionseed.org/file/my-bucket/docs/report.pdf"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("handles paths with query strings", () => { + const input = + "https://pseed-dev.s3.us-east-005.backblazeb2.com/images/test.webp?x-id=GetObject"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/images/test.webp"; + expect(toCdnUrl(input)).toBe(expected); + }); + }); + + describe("friendly B2 endpoint (f005)", () => { + it("rewrites f005 B2 URL to CDN URL", () => { + const input = + "https://f005.backblazeb2.com/file/pseed-dev/guidebook.pdf"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/guidebook.pdf"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("handles other friendly endpoints like f000", () => { + const input = + "https://f000.backblazeb2.com/file/pseed-dev/archive.zip"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/archive.zip"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("preserves nested paths", () => { + const input = + "https://f005.backblazeb2.com/file/pseed-dev/2024/01/report.pdf"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/2024/01/report.pdf"; + expect(toCdnUrl(input)).toBe(expected); + }); + }); + + describe("pass-through cases", () => { + it("returns already-CDN URLs unchanged", () => { + const input = "https://cdn.passionseed.org/file/pseed-dev/x.jpg"; + expect(toCdnUrl(input)).toBe(input); + }); + + it("returns Supabase Storage URLs unchanged", () => { + const input = "https://supabase.co/storage/v1/object/avatar.png"; + expect(toCdnUrl(input)).toBe(input); + }); + + it("returns arbitrary external URLs unchanged", () => { + const input = "https://example.com/image.png"; + expect(toCdnUrl(input)).toBe(input); + }); + + it("returns HTTP URLs unchanged", () => { + const input = "http://old-site.com/image.jpg"; + expect(toCdnUrl(input)).toBe(input); + }); + }); + + describe("edge cases", () => { + it("returns empty string for empty input", () => { + expect(toCdnUrl("")).toBe(""); + }); + + it("returns empty string for null input", () => { + expect(toCdnUrl(null)).toBe(""); + }); + + it("returns empty string for undefined input", () => { + expect(toCdnUrl(undefined)).toBe(""); + }); + + it("returns original string for invalid URL", () => { + const input = "not-a-valid-url"; + expect(toCdnUrl(input)).toBe(input); + }); + + it("handles URL with no path", () => { + const input = "https://pseed-dev.s3.us-east-005.backblazeb2.com/"; + const expected = "https://cdn.passionseed.org/file/pseed-dev/"; + expect(toCdnUrl(input)).toBe(expected); + }); + + it("handles URL with encoded characters in path", () => { + const input = + "https://pseed-dev.s3.us-east-005.backblazeb2.com/images/hello%20world.webp"; + const expected = + "https://cdn.passionseed.org/file/pseed-dev/images/hello%20world.webp"; + expect(toCdnUrl(input)).toBe(expected); + }); + }); +}); diff --git a/lib/cdn-url.ts b/lib/cdn-url.ts new file mode 100644 index 00000000..979cdad0 --- /dev/null +++ b/lib/cdn-url.ts @@ -0,0 +1,66 @@ +/** + * CDN URL utility — rewrites Backblaze B2 URLs to Cloudflare CDN URLs. + * + * Patterns handled: + * 1. S3-style B2: https://{bucket}.s3.{region}.backblazeb2.com/{path} + * → https://cdn.passionseed.org/file/{bucket}/{path} + * + * 2. Friendly B2: https://f005.backblazeb2.com/file/{bucket}/{path} + * → https://cdn.passionseed.org/file/{bucket}/{path} + * + * 3. Already CDN: https://cdn.passionseed.org/... + * → returned as-is (pass-through) + * + * 4. Non-B2 URLs (Supabase Storage, external links, etc.) + * → returned as-is + */ + +const CDN_BASE = "https://cdn.passionseed.org"; + +/** + * Rewrite a Backblaze B2 URL to a Cloudflare CDN URL. + * + * @param b2Url - Raw URL (may be B2, CDN, Supabase, external, empty, or nullish) + * @returns CDN URL, or empty string for null/empty input + */ +export function toCdnUrl(b2Url: string | null | undefined): string { + if (!b2Url) { + return ""; + } + + // Already a CDN URL — pass through + if (b2Url.startsWith(`${CDN_BASE}/`)) { + return b2Url; + } + + try { + const url = new URL(b2Url); + + // Pattern 1: S3-style B2 endpoint + // e.g. https://pseed-dev.s3.us-east-005.backblazeb2.com/images/test.webp + const s3B2Match = url.hostname.match( + /^(?[^.]+)\.s3\.[a-z0-9-]+\.backblazeb2\.com$/i + ); + if (s3B2Match?.groups?.bucket) { + const bucket = s3B2Match.groups.bucket; + const path = url.pathname.replace(/^\//, ""); + return `${CDN_BASE}/file/${bucket}/${path}`; + } + + // Pattern 2: Friendly B2 endpoint (f005, f000, etc.) + // e.g. https://f005.backblazeb2.com/file/pseed-dev/guidebook.pdf + const friendlyB2Match = url.hostname.match( + /^f\d+\.backblazeb2\.com$/i + ); + if (friendlyB2Match) { + // Path is already /file/{bucket}/{rest} + return `${CDN_BASE}${url.pathname}`; + } + } catch { + // Invalid URL — return original string (graceful fallback) + return b2Url; + } + + // Not a recognized B2 URL — return as-is + return b2Url; +} From cd862d8b2b082abb9ae6ef8245e0e9c1a697ff24 Mon Sep 17 00:00:00 2001 From: Bunyasit Fang Date: Tue, 5 May 2026 09:46:51 -0700 Subject: [PATCH 02/30] feat(db-migration): add one-off SQL script to rewrite B2 URLs to CDN URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates all Backblaze B2 image/file URLs across ~20 tables to Cloudflare CDN: - S3-style B2: https://{bucket}.s3.{region}.backblazeb2.com/{path} - Friendly B2: https://f005.backblazeb2.com/file/{bucket}/{path} → https://cdn.passionseed.org/file/{bucket}/{path} Covers: - 22 TEXT columns (single URLs) - 5 TEXT[] array columns (multiple URLs) - 3 JSONB columns with embedded URLs (revisions, certificate_data) Idempotent: filters rows with backblazeb2.com, pass-through for already-CDN URLs. Wrapped in BEGIN/COMMIT with temporary helper functions cleaned up after. Excludes: - Supabase Storage URLs (profiles.avatar_url, hackathon_teams.team_avatar_url) - _key columns (B2 file keys, not URLs) - Non-image URL columns (website_url, linkedin_url, etc.) - reflections.image_url (column does not exist in schema) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../20260505000000_migrate_b2_to_cdn.sql | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 supabase/migrations/20260505000000_migrate_b2_to_cdn.sql diff --git a/supabase/migrations/20260505000000_migrate_b2_to_cdn.sql b/supabase/migrations/20260505000000_migrate_b2_to_cdn.sql new file mode 100644 index 00000000..95f69eb1 --- /dev/null +++ b/supabase/migrations/20260505000000_migrate_b2_to_cdn.sql @@ -0,0 +1,353 @@ +-- ============================================================================ +-- One-off migration: Rewrite all Backblaze B2 URLs to Cloudflare CDN URLs +-- ============================================================================ +-- Date: 2026-05-05 +-- Purpose: Idempotently rewrite B2 URLs in ~20 tables to cdn.passionseed.org +-- +-- Patterns handled: +-- 1. S3-style B2: https://{bucket}.s3.{region}.backblazeb2.com/{path} +-- → https://cdn.passionseed.org/file/{bucket}/{path} +-- 2. Friendly B2: https://f005.backblazeb2.com/file/{bucket}/{path} +-- → https://cdn.passionseed.org/file/{bucket}/{path} +-- +-- Excluded (not B2 / not image URLs): +-- - Supabase Storage URLs (profiles.avatar_url, hackathon_teams.team_avatar_url) +-- - _key columns (B2 file keys, not URLs) +-- - Non-image URL columns (website_url, linkedin_url, etc.) +-- - reflections.image_url (column does not exist in schema) +-- ============================================================================ + +BEGIN; + +-- ---------------------------------------------------------------------------- +-- Helper function: rewrite a single B2 URL to CDN URL +-- ---------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION _b2_to_cdn(url TEXT) +RETURNS TEXT AS $$ +BEGIN + IF url IS NULL OR url = '' THEN + RETURN url; + END IF; + + -- Already a CDN URL — pass through + IF url LIKE 'https://cdn.passionseed.org/%' THEN + RETURN url; + END IF; + + -- Pattern 1: S3-style B2 endpoint + -- e.g. https://pseed-dev.s3.us-east-005.backblazeb2.com/images/test.webp + IF url ~ '^https://[^/]+\.s3\.[a-z0-9-]+\.backblazeb2\.com/' THEN + RETURN regexp_replace( + url, + '^https://([^/]+)\.s3\.[a-z0-9-]+\.backblazeb2\.com/(.*)$', + 'https://cdn.passionseed.org/file/\1/\2' + ); + END IF; + + -- Pattern 2: Friendly B2 endpoint (f005, f000, etc.) + -- e.g. https://f005.backblazeb2.com/file/pseed-dev/guidebook.pdf + IF url ~ '^https://f[0-9]+\.backblazeb2\.com/' THEN + RETURN regexp_replace( + url, + '^https://f[0-9]+\.backblazeb2\.com(/.*)$', + 'https://cdn.passionseed.org\1' + ); + END IF; + + -- Not a recognized B2 URL — return as-is + RETURN url; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- ---------------------------------------------------------------------------- +-- Helper function: rewrite an array of B2 URLs to CDN URLs +-- ---------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION _b2_to_cdn_array(urls TEXT[]) +RETURNS TEXT[] AS $$ +DECLARE + result TEXT[]; + i INT; +BEGIN + IF urls IS NULL THEN + RETURN NULL; + END IF; + + result := ARRAY[]::TEXT[]; + FOR i IN 1..array_length(urls, 1) LOOP + result := array_append(result, _b2_to_cdn(urls[i])); + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- ============================================================================ +-- CATEGORY 1: TEXT columns (single URL per row) +-- ============================================================================ + +-- learning_maps +UPDATE public.learning_maps +SET cover_image_url = _b2_to_cdn(cover_image_url) +WHERE cover_image_url LIKE '%backblazeb2.com%'; + +UPDATE public.learning_maps +SET sprite_url = _b2_to_cdn(sprite_url) +WHERE sprite_url LIKE '%backblazeb2.com%'; + +-- node_content (map_content) +UPDATE public.node_content +SET content_url = _b2_to_cdn(content_url) +WHERE content_url LIKE '%backblazeb2.com%'; + +-- seeds +UPDATE public.seeds +SET cover_image_url = _b2_to_cdn(cover_image_url) +WHERE cover_image_url LIKE '%backblazeb2.com%'; + +-- seed_categories +UPDATE public.seed_categories +SET logo_url = _b2_to_cdn(logo_url) +WHERE logo_url LIKE '%backblazeb2.com%'; + +-- seed_certificates +UPDATE public.seed_certificates +SET signature_image_url = _b2_to_cdn(signature_image_url) +WHERE signature_image_url LIKE '%backblazeb2.com%'; + +UPDATE public.seed_certificates +SET logo_url = _b2_to_cdn(logo_url) +WHERE logo_url LIKE '%backblazeb2.com%'; + +-- issued_certificates +UPDATE public.issued_certificates +SET certificate_url = _b2_to_cdn(certificate_url) +WHERE certificate_url LIKE '%backblazeb2.com%'; + +-- seed_badges +UPDATE public.seed_badges +SET badge_image_url = _b2_to_cdn(badge_image_url) +WHERE badge_image_url LIKE '%backblazeb2.com%'; + +-- assessment_submissions +UPDATE public.assessment_submissions +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- hackathon_phase_activity_submissions +UPDATE public.hackathon_phase_activity_submissions +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- hackathon_phase_activity_team_submissions +UPDATE public.hackathon_phase_activity_team_submissions +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- path_assessment_submissions +UPDATE public.path_assessment_submissions +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- path_content +UPDATE public.path_content +SET content_url = _b2_to_cdn(content_url) +WHERE content_url LIKE '%backblazeb2.com%'; + +UPDATE public.path_content +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- hackathon_phase_activity_content +UPDATE public.hackathon_phase_activity_content +SET content_url = _b2_to_cdn(content_url) +WHERE content_url LIKE '%backblazeb2.com%'; + +-- community_images +UPDATE public.community_images +SET url = _b2_to_cdn(url) +WHERE url LIKE '%backblazeb2.com%'; + +-- post_media +UPDATE public.post_media +SET url = _b2_to_cdn(url) +WHERE url LIKE '%backblazeb2.com%'; + +-- mentor_profiles +UPDATE public.mentor_profiles +SET photo_url = _b2_to_cdn(photo_url) +WHERE photo_url LIKE '%backblazeb2.com%'; + +-- expert_profiles +UPDATE public.expert_profiles +SET photo_url = _b2_to_cdn(photo_url) +WHERE photo_url LIKE '%backblazeb2.com%'; + +-- universities +UPDATE public.universities +SET logo_url = _b2_to_cdn(logo_url) +WHERE logo_url LIKE '%backblazeb2.com%'; + +-- projects +UPDATE public.projects +SET image_url = _b2_to_cdn(image_url) +WHERE image_url LIKE '%backblazeb2.com%'; + +-- ============================================================================ +-- CATEGORY 2: TEXT[] array columns (multiple URLs per row) +-- ============================================================================ + +-- assessment_submissions.file_urls +UPDATE public.assessment_submissions +SET file_urls = _b2_to_cdn_array(file_urls) +WHERE file_urls IS NOT NULL + AND EXISTS ( + SELECT 1 FROM unnest(file_urls) AS u + WHERE u LIKE '%backblazeb2.com%' + ); + +-- hackathon_phase_activity_submissions.file_urls +UPDATE public.hackathon_phase_activity_submissions +SET file_urls = _b2_to_cdn_array(file_urls) +WHERE file_urls IS NOT NULL + AND EXISTS ( + SELECT 1 FROM unnest(file_urls) AS u + WHERE u LIKE '%backblazeb2.com%' + ); + +-- hackathon_phase_activity_team_submissions.file_urls +UPDATE public.hackathon_phase_activity_team_submissions +SET file_urls = _b2_to_cdn_array(file_urls) +WHERE file_urls IS NOT NULL + AND EXISTS ( + SELECT 1 FROM unnest(file_urls) AS u + WHERE u LIKE '%backblazeb2.com%' + ); + +-- path_assessment_submissions.file_urls +UPDATE public.path_assessment_submissions +SET file_urls = _b2_to_cdn_array(file_urls) +WHERE file_urls IS NOT NULL + AND EXISTS ( + SELECT 1 FROM unnest(file_urls) AS u + WHERE u LIKE '%backblazeb2.com%' + ); + +-- path_content.file_urls +UPDATE public.path_content +SET file_urls = _b2_to_cdn_array(file_urls) +WHERE file_urls IS NOT NULL + AND EXISTS ( + SELECT 1 FROM unnest(file_urls) AS u + WHERE u LIKE '%backblazeb2.com%' + ); + +-- ============================================================================ +-- CATEGORY 3: JSONB columns (embedded URLs) +-- ============================================================================ + +-- ---------------------------------------------------------------------------- +-- 3a: hackathon_phase_activity_submissions.revisions +-- Each element: { "image_url": text, "file_urls": text[], ... } +-- ---------------------------------------------------------------------------- +UPDATE public.hackathon_phase_activity_submissions +SET revisions = ( + SELECT jsonb_agg( + CASE + WHEN elem->>'image_url' IS NOT NULL OR elem->>'file_urls' IS NOT NULL THEN + jsonb_set( + jsonb_set( + elem, + '{image_url}', + to_jsonb(_b2_to_cdn(elem->>'image_url')) + ), + '{file_urls}', + CASE + WHEN elem->>'file_urls' IS NOT NULL THEN + to_jsonb(_b2_to_cdn_array( + ARRAY(SELECT jsonb_array_elements_text(elem->'file_urls')) + )) + ELSE 'null'::jsonb + END + ) + ELSE elem + END + ) + FROM jsonb_array_elements(revisions) AS elem +) +WHERE revisions IS NOT NULL + AND revisions != '[]'::jsonb + AND EXISTS ( + SELECT 1 FROM jsonb_array_elements(revisions) AS elem + WHERE (elem->>'image_url' LIKE '%backblazeb2.com%') + OR EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(elem->'file_urls') AS u + WHERE u LIKE '%backblazeb2.com%' + ) + ); + +-- ---------------------------------------------------------------------------- +-- 3b: hackathon_phase_activity_team_submissions.revisions +-- Same structure as individual submissions +-- ---------------------------------------------------------------------------- +UPDATE public.hackathon_phase_activity_team_submissions +SET revisions = ( + SELECT jsonb_agg( + CASE + WHEN elem->>'image_url' IS NOT NULL OR elem->>'file_urls' IS NOT NULL THEN + jsonb_set( + jsonb_set( + elem, + '{image_url}', + to_jsonb(_b2_to_cdn(elem->>'image_url')) + ), + '{file_urls}', + CASE + WHEN elem->>'file_urls' IS NOT NULL THEN + to_jsonb(_b2_to_cdn_array( + ARRAY(SELECT jsonb_array_elements_text(elem->'file_urls')) + )) + ELSE 'null'::jsonb + END + ) + ELSE elem + END + ) + FROM jsonb_array_elements(revisions) AS elem +) +WHERE revisions IS NOT NULL + AND revisions != '[]'::jsonb + AND EXISTS ( + SELECT 1 FROM jsonb_array_elements(revisions) AS elem + WHERE (elem->>'image_url' LIKE '%backblazeb2.com%') + OR EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(elem->'file_urls') AS u + WHERE u LIKE '%backblazeb2.com%' + ) + ); + +-- ---------------------------------------------------------------------------- +-- 3c: issued_certificates.certificate_data +-- Contains: logo_url, signature_image_url (and other non-URL fields) +-- ---------------------------------------------------------------------------- +UPDATE public.issued_certificates +SET certificate_data = jsonb_set( + jsonb_set( + certificate_data, + '{logo_url}', + to_jsonb(_b2_to_cdn(certificate_data->>'logo_url')) + ), + '{signature_image_url}', + to_jsonb(_b2_to_cdn(certificate_data->>'signature_image_url')) +) +WHERE certificate_data IS NOT NULL + AND ( + (certificate_data->>'logo_url' LIKE '%backblazeb2.com%') + OR (certificate_data->>'signature_image_url' LIKE '%backblazeb2.com%') + ); + +-- ============================================================================ +-- Cleanup: drop helper functions +-- ============================================================================ +DROP FUNCTION IF EXISTS _b2_to_cdn(TEXT); +DROP FUNCTION IF EXISTS _b2_to_cdn_array(TEXT[]); + +COMMIT; From 390d2678e5dabec9a42b4d1a240ec3e48efc3e14 Mon Sep 17 00:00:00 2001 From: Bunyasit Fang Date: Tue, 5 May 2026 09:57:04 -0700 Subject: [PATCH 03/30] feat(next-config): disable Vercel image optimization, add CDN remotePattern, optimize lucide-react imports - Set images.unoptimized: true (not conditional on NODE_ENV) - Add cdn.passionseed.org to remotePatterns - Keep *.backblazeb2.com and *.supabase.co for backward compat - Add optimizePackageImports for lucide-react Feature: next-config-image-cdn Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- next.config.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 6b9b8259..a8bbf189 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -26,9 +26,9 @@ const nextConfig = { typescript: { ignoreBuildErrors: true, }, - // Enable image optimization for production + // Disable Vercel image optimization — images served directly from Cloudflare CDN images: { - unoptimized: process.env.NODE_ENV === "development", + unoptimized: true, domains: ["localhost", "127.0.0.1"], formats: ["image/webp", "image/avif"], remotePatterns: [ @@ -40,6 +40,10 @@ const nextConfig = { protocol: "https", hostname: "*.backblazeb2.com", }, + { + protocol: "https", + hostname: "cdn.passionseed.org", + }, ], }, // Production optimizations @@ -50,6 +54,7 @@ const nextConfig = { webpackBuildWorker: true, parallelServerBuildTraces: true, parallelServerCompiles: true, + optimizePackageImports: ["lucide-react"], serverActions: { bodySizeLimit: "50mb", // Allow up to 50MB for server actions and streaming }, From beaab040295db831bbd23e4bd3d13c02aa280bb1 Mon Sep 17 00:00:00 2001 From: Bunyasit Fang Date: Tue, 5 May 2026 09:58:58 -0700 Subject: [PATCH 04/30] feat(analytics): remove @vercel/analytics from app/layout.tsx Remove Vercel Analytics import and component to eliminate paid analytics usage. Part of the cdn-image-config milestone. - Remove: import { Analytics } from '@vercel/analytics/next' - Remove: JSX component Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- app/layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 93654aed..e124bdb8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,6 @@ import { Noto_Sans_Thai, } from "next/font/google"; import "./globals.css"; -import { Analytics } from "@vercel/analytics/next"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster as SonnerToaster } from "@/components/ui/sonner"; import { createClient } from "@/utils/supabase/server"; @@ -169,7 +168,6 @@ export default async function RootLayout({ - ); From 373f7df6005c88b0efd161c0b20a3471b5b113f7 Mon Sep 17 00:00:00 2001 From: Bunyasit Fang Date: Tue, 5 May 2026 10:05:52 -0700 Subject: [PATCH 05/30] feat(analytics): add Cloudflare Web Analytics snippet to app/layout.tsx - Add async defer script tag loading beacon.min.js from cloudflareinsights.com - Include data-cf-beacon attribute with site token placeholder (YOUR_CF_BEACON_TOKEN) - Script placed at end of for non-blocking load - Vercel Analytics already removed in previous commit Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- app/layout.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index e124bdb8..5ed7ab0d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -168,6 +168,13 @@ export default async function RootLayout({ + {/* Cloudflare Web Analytics */} +