From 5c6cea8395253bb8bca21e83409179c11f403747 Mon Sep 17 00:00:00 2001 From: "muslim.ilmiawan" Date: Thu, 2 Jul 2026 07:27:31 +0700 Subject: [PATCH 1/5] [SPARKS] add email newsletter subscription form --- .../[slug]/components/newsletter-section.tsx | 116 ++++++++++++++++++ .../(hyperjump)/services/[slug]/page.tsx | 2 + app/api/subscribe/route.ts | 44 +++++++ locales/en/ai.json | 13 ++ locales/id/ai.json | 13 ++ 5 files changed, 188 insertions(+) create mode 100644 app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx create mode 100644 app/api/subscribe/route.ts diff --git a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx new file mode 100644 index 000000000..2a3a230b3 --- /dev/null +++ b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState } from "react"; + +import type { SupportedLanguage } from "@/locales/.generated/types"; +import { + aiNewsletterCta, + aiNewsletterDisclaimer, + aiNewsletterError, + aiNewsletterHeading, + aiNewsletterLanguageEn, + aiNewsletterLanguageId, + aiNewsletterLanguageLabel, + aiNewsletterLanguageSu, + aiNewsletterPlaceholder, + aiNewsletterSubheading, + aiNewsletterSuccess, +} from "@/locales/.generated/strings"; + +type NewsletterSectionProps = { + lang: SupportedLanguage; +}; + +export function NewsletterSection({ lang }: NewsletterSectionProps) { + const [email, setEmail] = useState(""); + const [digestLang, setDigestLang] = useState<"en" | "id" | "su">( + lang === "id" ? "id" : "en" + ); + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = email.trim(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!trimmed || !emailRegex.test(trimmed)) { + setStatus("error"); + return; + } + + setStatus("loading"); + try { + const res = await fetch("/api/subscribe", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: trimmed, language: digestLang }), + }); + if (res.ok) { + setStatus("success"); + } else { + setStatus("error"); + } + } catch { + setStatus("error"); + } + }; + + return ( +
+
+

+ {aiNewsletterHeading(lang)} +

+

+ {aiNewsletterSubheading(lang)} +

+ + {/* Language dropdown */} +
+ + +
+ +
+ setEmail(e.target.value)} + placeholder={aiNewsletterPlaceholder(lang)} + disabled={status === "loading" || status === "success"} + className="flex-1 rounded-lg border border-gray-200 bg-white px-4 py-3 text-base text-hyperjump-black placeholder-gray-400 shadow-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50" + /> + +
+ + {status === "success" && ( +

+ {aiNewsletterSuccess(lang)} +

+ )} + {status === "error" && ( +

+ {aiNewsletterError(lang)} +

+ )} + +

{aiNewsletterDisclaimer(lang)}

+
+
+ ); +} diff --git a/app/[lang]/(hyperjump)/services/[slug]/page.tsx b/app/[lang]/(hyperjump)/services/[slug]/page.tsx index 878a6a5a0..2add8e21b 100644 --- a/app/[lang]/(hyperjump)/services/[slug]/page.tsx +++ b/app/[lang]/(hyperjump)/services/[slug]/page.tsx @@ -35,6 +35,7 @@ import { mainServicesLabel } from "@/locales/.generated/strings"; +import { NewsletterSection } from "./components/newsletter-section"; import { AnimatedLines } from "../../components/animated-lines"; import { CaseStudyCard } from "../../components/case-study-card"; import { SectionReveal } from "../../components/motion-wrappers"; @@ -107,6 +108,7 @@ export default async function ServiceDetail({ params }: ServiceDetailProps) { + {slug === ServiceSlug.InferenceAI && } diff --git a/app/api/subscribe/route.ts b/app/api/subscribe/route.ts new file mode 100644 index 000000000..1f1edf931 --- /dev/null +++ b/app/api/subscribe/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const { email, language } = body; + + // Basic validation server-side + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!email || !emailRegex.test(email.trim())) { + return NextResponse.json({ error: "Invalid email" }, { status: 422 }); + } + + const apiKey = process.env.SUBSCRIBE_API_KEY; + const apiUrl = process.env.FRONTIERNOTES_API_URL ?? "https://tech-monitor.fly.dev"; + + if (!apiKey) { + return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); + } + + const res = await fetch(`${apiUrl}/api/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + email: email.trim().toLowerCase(), + language: language ?? "en", + }), + }); + + let data: { status?: string } = {}; + try { + data = await res.json(); + } catch { + // FrontierNotes returned non-JSON (e.g. plain text error) + } + + if (res.status === 201 || res.status === 200) { + return NextResponse.json({ status: data.status ?? "subscribed" }, { status: 200 }); + } + + return NextResponse.json({ error: "Subscription failed" }, { status: 500 }); +} diff --git a/locales/en/ai.json b/locales/en/ai.json index 2ac7d917b..50b50136d 100644 --- a/locales/en/ai.json +++ b/locales/en/ai.json @@ -103,6 +103,19 @@ "title": "Our Products", "description": "Innovative products tailored to support your goals and scale with your needs." }, + "newsletter": { + "heading": "Stay Ahead of the Curve", + "subheading": "Get practical AI insights, agent architecture breakdowns, and real-world implementation stories — straight to your inbox.", + "placeholder": "Enter your email", + "cta": "Subscribe", + "disclaimer": "No spam. Unsubscribe anytime.", + "success": "You're in! We'll be in touch soon.", + "error": "Something went wrong. Please try again.", + "languageLabel": "Digest language:", + "languageEn": "English", + "languageId": "Bahasa Indonesia", + "languageSu": "Basa Sunda" + }, "faq": { "heading": "Frequently asked questions", "desc": "Everything you need to go from concept to fully deployed Al agent-done for you, end to end.", diff --git a/locales/id/ai.json b/locales/id/ai.json index 266f81935..37a4c79fb 100644 --- a/locales/id/ai.json +++ b/locales/id/ai.json @@ -103,6 +103,19 @@ "title": "Produk Kami", "description": "Produk inovatif yang dirancang untuk mendukung tujuan Anda dan berkembang sesuai kebutuhan Anda." }, + "newsletter": { + "heading": "Selangkah Lebih Maju", + "subheading": "Dapatkan insight AI praktis, breakdown arsitektur agent, dan kisah implementasi nyata — langsung ke kotak masukmu.", + "placeholder": "Masukkan email kamu", + "cta": "Langganan", + "disclaimer": "Tanpa spam. Bisa berhenti berlangganan kapan saja.", + "success": "Berhasil! Kami akan segera menghubungi kamu.", + "error": "Terjadi kesalahan. Silakan coba lagi.", + "languageLabel": "Bahasa digest:", + "languageEn": "English", + "languageId": "Bahasa Indonesia", + "languageSu": "Basa Sunda" + }, "faq": { "heading": "Pertanyaan yang Sering Diajukan", "desc": "Segala hal yang Anda butuhkan untuk mewujudkan agen AI dari konsep hingga implementasi penuh dikerjakan secara menyeluruh oleh kami.", From 5a85bc80ed5dc5bc00782cf30efa710bc759272c Mon Sep 17 00:00:00 2001 From: "muslim.ilmiawan" Date: Thu, 2 Jul 2026 10:37:56 +0700 Subject: [PATCH 2/5] Fix formatting issue in newsletter --- .../[slug]/components/newsletter-section.tsx | 21 ++++++++++++------- app/api/subscribe/route.ts | 14 ++++++++----- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx index 2a3a230b3..5b0f1bd9a 100644 --- a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx +++ b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx @@ -14,7 +14,7 @@ import { aiNewsletterLanguageSu, aiNewsletterPlaceholder, aiNewsletterSubheading, - aiNewsletterSuccess, + aiNewsletterSuccess } from "@/locales/.generated/strings"; type NewsletterSectionProps = { @@ -26,7 +26,9 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) { const [digestLang, setDigestLang] = useState<"en" | "id" | "su">( lang === "id" ? "id" : "en" ); - const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -42,7 +44,7 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) { const res = await fetch("/api/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email: trimmed, language: digestLang }), + body: JSON.stringify({ email: trimmed, language: digestLang }) }); if (res.ok) { setStatus("success"); @@ -70,9 +72,10 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) { - setDigestLang(e.target.value as "en" | "id" | "su") + setDigestLang(e.target.value as "en" | "id" | "su" | "de") } className="rounded border border-gray-300 px-2 py-1 text-sm text-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-200 focus:outline-none"> - - - + + + +
- setEmail(e.target.value)} - placeholder={aiNewsletterPlaceholder(lang)} - disabled={status === "loading" || status === "success"} - className="text-hyperjump-black flex-1 rounded-lg border border-gray-200 bg-white px-4 py-3 text-base placeholder-gray-400 shadow-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50" - /> - + className="flex w-full max-w-xl flex-col gap-3"> +
+ setEmail(e.target.value)} + placeholder={copy.placeholder} + disabled={status === "loading" || status === "success"} + className="text-hyperjump-black flex-1 rounded-lg border border-gray-200 bg-white px-4 py-3 text-base placeholder-gray-400 shadow-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50" + /> + +
+ +
+ setCaptchaToken(token)} + onExpire={() => setCaptchaToken(null)} + onError={() => setCaptchaToken(null)} + /> +
{status === "success" && ( -

- {aiNewsletterSuccess(lang)} -

+

{copy.success}

)} {status === "error" && ( -

- {aiNewsletterError(lang)} +

{copy.error}

+ )} + {status === "captcha-error" && ( +

+ {copy.captchaError}

)} -

- {aiNewsletterDisclaimer(lang)} -

+

{copy.disclaimer}

); diff --git a/app/api/subscribe/route.ts b/app/api/subscribe/route.ts index ad47b81e4..d693fa4b4 100644 --- a/app/api/subscribe/route.ts +++ b/app/api/subscribe/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; export async function POST(req: NextRequest) { const body = await req.json(); - const { email, language } = body; + const { email, language, turnstileToken } = body; // Basic validation server-side const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -10,6 +10,40 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Invalid email" }, { status: 422 }); } + if (!turnstileToken) { + return NextResponse.json( + { error: "Missing CAPTCHA verification" }, + { status: 422 } + ); + } + + const turnstileVerifyRes = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + secret: process.env.TURNSTILE_SECRET_KEY, + response: turnstileToken, + remoteip: req.headers.get("x-forwarded-for") ?? undefined + }) + } + ); + + let turnstileData: { success?: boolean } = {}; + try { + turnstileData = await turnstileVerifyRes.json(); + } catch { + // Cloudflare returned non-JSON, treat as verification failure below + } + + if (turnstileData.success !== true) { + return NextResponse.json( + { error: "CAPTCHA verification failed" }, + { status: 403 } + ); + } + const apiKey = process.env.SUBSCRIBE_API_KEY; const apiUrl = process.env.FRONTIERNOTES_API_URL ?? "https://tech-monitor.fly.dev"; diff --git a/bun.lock b/bun.lock index c7e1a12c1..75d68fea0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "hyperjump.tech", "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", "@mdx-js/loader": "3.1.1", "@mdx-js/react": "3.1.1", "@n8n/chat": "0.61.0", @@ -213,6 +213,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.3", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-8Dij2jiNGNczq1U4EKpO4do2XepcTPxSMc2ZzvHndO+gcp68tvMULm27z2P99rGkdB89hc3452NZeu2Rti4g6A=="], + "@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], diff --git a/locales/en/ai.json b/locales/en/ai.json index 50b50136d..32980f6f4 100644 --- a/locales/en/ai.json +++ b/locales/en/ai.json @@ -109,12 +109,14 @@ "placeholder": "Enter your email", "cta": "Subscribe", "disclaimer": "No spam. Unsubscribe anytime.", - "success": "You're in! We'll be in touch soon.", + "success": "Almost there! Check your inbox to confirm your subscription.", "error": "Something went wrong. Please try again.", + "captchaError": "Please complete the CAPTCHA verification.", "languageLabel": "Digest language:", "languageEn": "English", "languageId": "Bahasa Indonesia", - "languageSu": "Basa Sunda" + "languageSu": "Basa Sunda", + "languageDe": "Deutsch" }, "faq": { "heading": "Frequently asked questions", diff --git a/locales/id/ai.json b/locales/id/ai.json index 37a4c79fb..f67ddc47f 100644 --- a/locales/id/ai.json +++ b/locales/id/ai.json @@ -109,12 +109,14 @@ "placeholder": "Masukkan email kamu", "cta": "Langganan", "disclaimer": "Tanpa spam. Bisa berhenti berlangganan kapan saja.", - "success": "Berhasil! Kami akan segera menghubungi kamu.", + "success": "Hampir selesai! Cek email kamu untuk konfirmasi langganan.", "error": "Terjadi kesalahan. Silakan coba lagi.", + "captchaError": "Mohon selesaikan verifikasi CAPTCHA terlebih dahulu.", "languageLabel": "Bahasa digest:", "languageEn": "English", "languageId": "Bahasa Indonesia", - "languageSu": "Basa Sunda" + "languageSu": "Basa Sunda", + "languageDe": "Deutsch" }, "faq": { "heading": "Pertanyaan yang Sering Diajukan", diff --git a/package.json b/package.json index 07798ea6d..dbe3fd4fe 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:e2e:report": "playwright show-report" }, "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", "@mdx-js/loader": "3.1.1", "@mdx-js/react": "3.1.1", "@n8n/chat": "0.61.0", From acd7bc9a13fb6038fff26e4074b7bd08c8462279 Mon Sep 17 00:00:00 2001 From: "muslim.ilmiawan" Date: Mon, 6 Jul 2026 13:18:14 +0700 Subject: [PATCH 4/5] fix formatting --- .../[slug]/components/newsletter-section.tsx | 141 +++++++++--------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx index 09f0931a1..1cfd0447a 100644 --- a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx +++ b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx @@ -31,73 +31,72 @@ const newsletterCopyByDigestLang: Record< "en" | "id" | "de" | "su", NewsletterCopy > = { - en: { - heading: "Stay Ahead of the Curve", - subheading: - "Get practical AI insights, agent architecture breakdowns, and real-world implementation stories — straight to your inbox.", - placeholder: "Enter your email", - cta: "Subscribe", - disclaimer: "No spam. Unsubscribe anytime.", - success: - "Almost there! Check your inbox to confirm your subscription.", - error: "Something went wrong. Please try again.", - captchaError: "Please complete the CAPTCHA verification.", - languageLabel: "Digest language:", - languageEn: "English", - languageId: "Bahasa Indonesia", - languageSu: "Basa Sunda", - languageDe: "Deutsch" - }, - id: { - heading: "Selangkah Lebih Maju", - subheading: - "Dapatkan insight AI praktis, breakdown arsitektur agent, dan kisah implementasi nyata — langsung ke kotak masukmu.", - placeholder: "Masukkan email kamu", - cta: "Langganan", - disclaimer: "Tanpa spam. Bisa berhenti berlangganan kapan saja.", - success: "Hampir selesai! Cek email kamu untuk konfirmasi langganan.", - error: "Terjadi kesalahan. Silakan coba lagi.", - captchaError: "Mohon selesaikan verifikasi CAPTCHA terlebih dahulu.", - languageLabel: "Bahasa digest:", - languageEn: "English", - languageId: "Bahasa Indonesia", - languageSu: "Basa Sunda", - languageDe: "Deutsch" - }, - de: { - heading: "Bleiben Sie immer einen Schritt voraus", - subheading: - "Erhalte praxisnahe KI-Einblicke, detaillierte Agent-Architekturen und Berichte aus echten Implementierungen — direkt in dein Postfach.", - placeholder: "Gib deine E-Mail-Adresse ein", - cta: "Abonnieren", - disclaimer: "Kein Spam. Jederzeit abbestellbar.", - success: - "Fast geschafft! Bestätige dein Abonnement über den Link in deinem Postfach.", - error: "Etwas ist schiefgelaufen. Bitte versuche es erneut.", - captchaError: "Bitte schließe die CAPTCHA-Verifizierung ab.", - languageLabel: "Digest-Sprache:", - languageEn: "English", - languageId: "Bahasa Indonesia", - languageSu: "Basa Sunda", - languageDe: "Deutsch" - }, - su: { - heading: "Salangkung Leuwih Maju", - subheading: - "Kenging insight AI anu praktis, bedah arsitektur agent, sareng carita implementasi nyata — langsung ka kotak surel anjeun.", - placeholder: "Lebetkeun email anjeun", - cta: "Langganan", - disclaimer: "Tanpa spam. Tiasa eureun langganan iraha wae.", - success: "Ampir rengse! Pariksa email anjeun kanggo konfirmasi langganan.", - error: "Aya kasalahan. Mangga cobian deui.", - captchaError: "Mangga lengkepan verifikasi CAPTCHA heula.", - languageLabel: "Basa digest:", - languageEn: "English", - languageId: "Bahasa Indonesia", - languageSu: "Basa Sunda", - languageDe: "Deutsch" - } - }; + en: { + heading: "Stay Ahead of the Curve", + subheading: + "Get practical AI insights, agent architecture breakdowns, and real-world implementation stories — straight to your inbox.", + placeholder: "Enter your email", + cta: "Subscribe", + disclaimer: "No spam. Unsubscribe anytime.", + success: "Almost there! Check your inbox to confirm your subscription.", + error: "Something went wrong. Please try again.", + captchaError: "Please complete the CAPTCHA verification.", + languageLabel: "Digest language:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + id: { + heading: "Selangkah Lebih Maju", + subheading: + "Dapatkan insight AI praktis, breakdown arsitektur agent, dan kisah implementasi nyata — langsung ke kotak masukmu.", + placeholder: "Masukkan email kamu", + cta: "Langganan", + disclaimer: "Tanpa spam. Bisa berhenti berlangganan kapan saja.", + success: "Hampir selesai! Cek email kamu untuk konfirmasi langganan.", + error: "Terjadi kesalahan. Silakan coba lagi.", + captchaError: "Mohon selesaikan verifikasi CAPTCHA terlebih dahulu.", + languageLabel: "Bahasa digest:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + de: { + heading: "Bleiben Sie immer einen Schritt voraus", + subheading: + "Erhalte praxisnahe KI-Einblicke, detaillierte Agent-Architekturen und Berichte aus echten Implementierungen — direkt in dein Postfach.", + placeholder: "Gib deine E-Mail-Adresse ein", + cta: "Abonnieren", + disclaimer: "Kein Spam. Jederzeit abbestellbar.", + success: + "Fast geschafft! Bestätige dein Abonnement über den Link in deinem Postfach.", + error: "Etwas ist schiefgelaufen. Bitte versuche es erneut.", + captchaError: "Bitte schließe die CAPTCHA-Verifizierung ab.", + languageLabel: "Digest-Sprache:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + su: { + heading: "Salangkung Leuwih Maju", + subheading: + "Kenging insight AI anu praktis, bedah arsitektur agent, sareng carita implementasi nyata — langsung ka kotak surel anjeun.", + placeholder: "Lebetkeun email anjeun", + cta: "Langganan", + disclaimer: "Tanpa spam. Tiasa eureun langganan iraha wae.", + success: "Ampir rengse! Pariksa email anjeun kanggo konfirmasi langganan.", + error: "Aya kasalahan. Mangga cobian deui.", + captchaError: "Mangga lengkepan verifikasi CAPTCHA heula.", + languageLabel: "Basa digest:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + } +}; export function NewsletterSection({ lang }: NewsletterSectionProps) { const [email, setEmail] = useState(""); @@ -110,8 +109,8 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) { const [captchaToken, setCaptchaToken] = useState(null); const turnstileRef = useRef(null); - const copy = newsletterCopyByDigestLang[digestLang] ?? - newsletterCopyByDigestLang.en; + const copy = + newsletterCopyByDigestLang[digestLang] ?? newsletterCopyByDigestLang.en; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -218,9 +217,7 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) {

{copy.error}

)} {status === "captcha-error" && ( -

- {copy.captchaError} -

+

{copy.captchaError}

)}

{copy.disclaimer}

From 0f40298c97d065911cd316c11444642d782bd35e Mon Sep 17 00:00:00 2001 From: "muslim.ilmiawan" Date: Mon, 6 Jul 2026 14:46:30 +0700 Subject: [PATCH 5/5] Move newsletter subscribe to client-side direct fetch to FrontierNotes hyperjump.tech deploys as a full static export (output: "export") to GitHub Pages, so the Next.js API route added for Turnstile verification never actually ran in production. Turnstile siteverify moves to FrontierNotes; the frontend now fetches NEXT_PUBLIC_FRONTIERNOTES_API_URL directly without a server-side proxy or Authorization header. Co-Authored-By: Claude Sonnet 5 --- .env.example | 6 +- .github/workflows/deploy.yml | 4 + .../[slug]/components/newsletter-section.tsx | 5 +- app/api/subscribe/route.ts | 82 ------------------- 4 files changed, 10 insertions(+), 87 deletions(-) delete mode 100644 app/api/subscribe/route.ts diff --git a/.env.example b/.env.example index ae3d4b9f0..db53dbfd5 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,5 @@ NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK= NEXT_PUBLIC_INFERENCEAI_POST_CHATS_WEBHOOK= NEXT_PUBLIC_INFERENCEAI_GET_CHATS_WEBHOOK= NEXT_PUBLIC_GA_TRACKING_ID= -FRONTIERNOTES_API_URL= -SUBSCRIBE_API_KEY= -NEXT_PUBLIC_TURNSTILE_SITE_KEY= -TURNSTILE_SECRET_KEY= \ No newline at end of file +NEXT_PUBLIC_FRONTIERNOTES_API_URL= +NEXT_PUBLIC_TURNSTILE_SITE_KEY= \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6302faecb..e3d37a8a2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,6 +13,8 @@ env: NEXT_PUBLIC_LANDING_GET_CHATS_WEBHOOK: ${{ vars.NEXT_PUBLIC_LANDING_GET_CHATS_WEBHOOK }} NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK: ${{ vars.NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK }} NEXT_PUBLIC_GA_TRACKING_ID: ${{ vars.NEXT_PUBLIC_GA_TRACKING_ID }} + NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${{ vars.NEXT_PUBLIC_TURNSTILE_SITE_KEY }} + NEXT_PUBLIC_FRONTIERNOTES_API_URL: ${{ vars.NEXT_PUBLIC_FRONTIERNOTES_API_URL }} jobs: deploy: @@ -37,6 +39,8 @@ jobs: NEXT_PUBLIC_LANDING_GET_CHATS_WEBHOOK: ${{ vars.NEXT_PUBLIC_LANDING_GET_CHATS_WEBHOOK }} NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK: ${{ vars.NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK }} NEXT_PUBLIC_GA_TRACKING_ID: ${{ vars.NEXT_PUBLIC_GA_TRACKING_ID }} + NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${{ vars.NEXT_PUBLIC_TURNSTILE_SITE_KEY }} + NEXT_PUBLIC_FRONTIERNOTES_API_URL: ${{ vars.NEXT_PUBLIC_FRONTIERNOTES_API_URL }} - name: Create CNAME file run: echo "hyperjump.tech" > out/CNAME diff --git a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx index 1cfd0447a..15ab794ab 100644 --- a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx +++ b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx @@ -128,7 +128,10 @@ export function NewsletterSection({ lang }: NewsletterSectionProps) { setStatus("loading"); try { - const res = await fetch("/api/subscribe", { + const apiUrl = + process.env.NEXT_PUBLIC_FRONTIERNOTES_API_URL ?? + "https://tech-monitor.fly.dev"; + const res = await fetch(`${apiUrl}/api/subscribe`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/app/api/subscribe/route.ts b/app/api/subscribe/route.ts deleted file mode 100644 index d693fa4b4..000000000 --- a/app/api/subscribe/route.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const body = await req.json(); - const { email, language, turnstileToken } = body; - - // Basic validation server-side - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!email || !emailRegex.test(email.trim())) { - return NextResponse.json({ error: "Invalid email" }, { status: 422 }); - } - - if (!turnstileToken) { - return NextResponse.json( - { error: "Missing CAPTCHA verification" }, - { status: 422 } - ); - } - - const turnstileVerifyRes = await fetch( - "https://challenges.cloudflare.com/turnstile/v0/siteverify", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - secret: process.env.TURNSTILE_SECRET_KEY, - response: turnstileToken, - remoteip: req.headers.get("x-forwarded-for") ?? undefined - }) - } - ); - - let turnstileData: { success?: boolean } = {}; - try { - turnstileData = await turnstileVerifyRes.json(); - } catch { - // Cloudflare returned non-JSON, treat as verification failure below - } - - if (turnstileData.success !== true) { - return NextResponse.json( - { error: "CAPTCHA verification failed" }, - { status: 403 } - ); - } - - const apiKey = process.env.SUBSCRIBE_API_KEY; - const apiUrl = - process.env.FRONTIERNOTES_API_URL ?? "https://tech-monitor.fly.dev"; - - if (!apiKey) { - return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); - } - - const res = await fetch(`${apiUrl}/api/subscribe`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}` - }, - body: JSON.stringify({ - email: email.trim().toLowerCase(), - language: language ?? "en" - }) - }); - - let data: { status?: string } = {}; - try { - data = await res.json(); - } catch { - // FrontierNotes returned non-JSON (e.g. plain text error) - } - - if (res.status === 201 || res.status === 200) { - return NextResponse.json( - { status: data.status ?? "subscribed" }, - { status: 200 } - ); - } - - return NextResponse.json({ error: "Subscription failed" }, { status: 500 }); -}