From f1d851d8213cce593a73fb06d172cd07763c48f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:54:53 +0000 Subject: [PATCH 1/7] Initial plan From a3341b22766953ab1ae3cbc8d4b5f49aa1963666 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:17:38 +0000 Subject: [PATCH 2/7] Switch newsletter forms from Mailchimp to Brevo (static HTML form approach) Co-authored-by: bobbyiliev <21223421+bobbyiliev@users.noreply.github.com> --- .env.example | 4 +++ .../devops-survival-guide/client-content.tsx | 14 +++------- components/book-promotion-popup.tsx | 27 ++++++++++--------- components/footer/newsletter-form.tsx | 16 +++++------ components/sponsor-sidebar.tsx | 14 +++------- 5 files changed, 32 insertions(+), 43 deletions(-) diff --git a/.env.example b/.env.example index 8c0f6cea0..d2ba9a7aa 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,7 @@ WATCHPACK_POLLING=true # Optional: API Keys (if needed) # OPENAI_API_KEY=your_api_key_here + +# Brevo (newsletter) — set to your Brevo subscription form URL +# e.g. https://sibforms.com/serve/MUIFAAAA... +# NEXT_PUBLIC_BREVO_FORM_URL=https://sibforms.com/serve/YOUR_FORM_ID_HERE diff --git a/app/books/devops-survival-guide/client-content.tsx b/app/books/devops-survival-guide/client-content.tsx index 9559d1f71..1f200eb60 100644 --- a/app/books/devops-survival-guide/client-content.tsx +++ b/app/books/devops-survival-guide/client-content.tsx @@ -525,7 +525,7 @@ export function ClientContent() {
- {/* Honeypot bot field */} + {/* Brevo bot-protection fields */} -
+ setBookEmail(e.target.value)} + required + placeholder="your@email.com" + className="w-full px-5 py-4 border-2 border-border/50 bg-background/50 backdrop-blur-sm rounded-xl text-base focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-all duration-300" + /> + + {bookSubscribeStatus === 'error' && ( +

Something went wrong. Please try again.

+ )} + + + + )}

diff --git a/components/book-promotion-popup.tsx b/components/book-promotion-popup.tsx index c2de98765..fd4bc4932 100644 --- a/components/book-promotion-popup.tsx +++ b/components/book-promotion-popup.tsx @@ -5,7 +5,6 @@ import { motion, AnimatePresence } from 'framer-motion' import { BookOpen, X, Sparkles } from 'lucide-react' import { Button } from '@/components/ui/button' import Confetti from 'react-confetti' -import { BREVO_FORM_URL } from '@/lib/newsletter' export function BookPromotionPopup() { const [isVisible, setIsVisible] = useState(false) @@ -61,29 +60,36 @@ export function BookPromotionPopup() { }, 300) } - const handleSubscribe = (e: React.FormEvent) => { - e.preventDefault() + const handleSubscribe = async (e: React.FormEvent) => { + e.preventDefault(); - if (!email) return + if (!email) return; - // Submit form to Brevo in a new tab - const form = e.currentTarget - form.submit() + try { + await fetch('/api/newsletter', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + } catch (err) { + console.error('[newsletter] Popup subscription error:', err); + // Proceed optimistically – show thank you regardless + } // Show thank you message - setShowThankYou(true) - localStorage.setItem('book-promo-subscribed', 'true') + setShowThankYou(true); + localStorage.setItem('book-promo-subscribed', 'true'); // Show celebration confetti - setShowConfetti(true) + setShowConfetti(true); // Auto-close after 3 seconds setTimeout(() => { - setShowConfetti(false) - setIsLoaded(false) - setTimeout(() => setIsVisible(false), 300) - }, 3000) - } + setShowConfetti(false); + setIsLoaded(false); + setTimeout(() => setIsVisible(false), 300); + }, 3000); + }; if (!isVisible) return null @@ -158,9 +164,6 @@ export function BookPromotionPopup() {

- {/* Brevo bot-protection fields */} - -
diff --git a/components/newsletter-inline-form.tsx b/components/newsletter-inline-form.tsx new file mode 100644 index 000000000..783b5a5ca --- /dev/null +++ b/components/newsletter-inline-form.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { CheckCircle } from 'lucide-react'; +import { useNewsletterSubscribe } from '@/hooks/use-newsletter-subscribe'; + +export function NewsletterInlineForm() { + const [email, setEmail] = useState(''); + const { status, subscribe } = useNewsletterSubscribe(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (email) subscribe(email); + }; + + if (status === 'success') { + return ( +
+ +

You're subscribed! Check your inbox.

+
+ ); + } + + return ( +
+ setEmail(e.target.value)} + required + placeholder="you@example.com" + className="w-full px-3 py-2 border border-border bg-background rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> + + {status === 'error' && ( +

Something went wrong. Please try again.

+ )} + + +
+ ); +} diff --git a/components/sponsor-sidebar.tsx b/components/sponsor-sidebar.tsx index 585b0d416..4190a547a 100644 --- a/components/sponsor-sidebar.tsx +++ b/components/sponsor-sidebar.tsx @@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'; import { Clock, Sparkles, ExternalLink } from 'lucide-react'; import { CarbonAds } from '@/components/carbon-ads'; import { sponsors } from '@/lib/sponsors'; -import { BREVO_FORM_URL } from '@/lib/newsletter'; +import { NewsletterInlineForm } from '@/components/newsletter-inline-form'; interface SponsorSidebarProps { className?: string; @@ -98,34 +98,7 @@ export function SponsorSidebar({ className, relatedPosts = [] }: SponsorSidebarP

Get the latest DevOps tips and tutorials delivered to your inbox.

-
- - - {/* Brevo bot-protection fields */} - - - -
+
{/* Related Posts Section */} diff --git a/hooks/use-newsletter-subscribe.ts b/hooks/use-newsletter-subscribe.ts new file mode 100644 index 000000000..072109b27 --- /dev/null +++ b/hooks/use-newsletter-subscribe.ts @@ -0,0 +1,31 @@ +'use client'; + +import { useState } from 'react'; + +export type SubscribeStatus = 'idle' | 'loading' | 'success' | 'error'; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function useNewsletterSubscribe() { + const [status, setStatus] = useState('idle'); + + const subscribe = async (email: string) => { + if (!EMAIL_RE.test(email)) { + setStatus('error'); + return; + } + setStatus('loading'); + try { + const res = await fetch('/api/newsletter', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + setStatus(res.ok ? 'success' : 'error'); + } catch { + setStatus('error'); + } + }; + + return { status, subscribe }; +} From 1704c6fa4ffde71387258bffbe601bb81c4b4768 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:57:24 +0000 Subject: [PATCH 6/7] fix: submit to Brevo directly via fetch, remove API route Co-authored-by: bobbyiliev <21223421+bobbyiliev@users.noreply.github.com> --- app/api/newsletter/route.ts | 36 ----------------------------- components/book-promotion-popup.tsx | 9 +++----- hooks/use-newsletter-subscribe.ts | 11 +++------ lib/newsletter.ts | 21 +++++++++++++++++ 4 files changed, 27 insertions(+), 50 deletions(-) delete mode 100644 app/api/newsletter/route.ts diff --git a/app/api/newsletter/route.ts b/app/api/newsletter/route.ts deleted file mode 100644 index 39b6ce2d1..000000000 --- a/app/api/newsletter/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { BREVO_FORM_URL } from '@/lib/newsletter'; - -const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -export async function POST(request: NextRequest) { - try { - const { email } = await request.json(); - - if (!email || typeof email !== 'string' || !EMAIL_RE.test(email)) { - return NextResponse.json({ error: 'A valid email is required.' }, { status: 400 }); - } - - const body = new URLSearchParams({ - EMAIL: email.trim(), - email_address_check: '', - locale: 'en', - }); - - const brevoRes = await fetch(BREVO_FORM_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: body.toString(), - redirect: 'follow', - }); - - if (!brevoRes.ok) { - console.error('[newsletter] Brevo returned', brevoRes.status); - } - - return NextResponse.json({ success: true }); - } catch (err) { - console.error('[newsletter] Subscription error:', err); - return NextResponse.json({ error: 'Failed to subscribe. Please try again.' }, { status: 500 }); - } -} diff --git a/components/book-promotion-popup.tsx b/components/book-promotion-popup.tsx index fd4bc4932..584858897 100644 --- a/components/book-promotion-popup.tsx +++ b/components/book-promotion-popup.tsx @@ -5,6 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { BookOpen, X, Sparkles } from 'lucide-react' import { Button } from '@/components/ui/button' import Confetti from 'react-confetti' +import { EMAIL_RE, submitToBrevo } from '@/lib/newsletter' export function BookPromotionPopup() { const [isVisible, setIsVisible] = useState(false) @@ -63,14 +64,10 @@ export function BookPromotionPopup() { const handleSubscribe = async (e: React.FormEvent) => { e.preventDefault(); - if (!email) return; + if (!email || !EMAIL_RE.test(email)) return; try { - await fetch('/api/newsletter', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }), - }); + await submitToBrevo(email); } catch (err) { console.error('[newsletter] Popup subscription error:', err); // Proceed optimistically – show thank you regardless diff --git a/hooks/use-newsletter-subscribe.ts b/hooks/use-newsletter-subscribe.ts index 072109b27..c292fc262 100644 --- a/hooks/use-newsletter-subscribe.ts +++ b/hooks/use-newsletter-subscribe.ts @@ -1,11 +1,10 @@ 'use client'; import { useState } from 'react'; +import { EMAIL_RE, submitToBrevo } from '@/lib/newsletter'; export type SubscribeStatus = 'idle' | 'loading' | 'success' | 'error'; -const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - export function useNewsletterSubscribe() { const [status, setStatus] = useState('idle'); @@ -16,12 +15,8 @@ export function useNewsletterSubscribe() { } setStatus('loading'); try { - const res = await fetch('/api/newsletter', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }), - }); - setStatus(res.ok ? 'success' : 'error'); + await submitToBrevo(email); + setStatus('success'); } catch { setStatus('error'); } diff --git a/lib/newsletter.ts b/lib/newsletter.ts index 8e73ed2e4..54f286fcd 100644 --- a/lib/newsletter.ts +++ b/lib/newsletter.ts @@ -1,3 +1,24 @@ /** Brevo subscription form URL. Public — safe to commit, no env var needed. */ export const BREVO_FORM_URL = 'https://66ce6dcc.sibforms.com/serve/MUIFABg_VUzhY-5kln8REbgjz0epYq6FtPckqwqsIG_s4FKBiVUqR9Q5SakKep9c2cHa2NEC1J02ps4tMUbaxssoB7MvwSggRvWktJJ7-LM9oWVRG3h0KhFHXsNOgoCSEo9OTB_CIp8JyRlALoSmEQOGpRoVYIYEq2LD0ikQ6T56zXVF8ZNc3tFBykMZGOxtLEfD7tap75LwptWPxg=='; + +export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +/** + * Submit an email to the Brevo hosted subscription form. + * Uses mode:'no-cors' because Brevo's endpoint doesn't emit CORS headers. + * The response is opaque, so any non-throwing fetch is treated as success. + */ +export async function submitToBrevo(email: string): Promise { + const body = new URLSearchParams({ + EMAIL: email.trim(), + email_address_check: '', + locale: 'en', + }); + await fetch(BREVO_FORM_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + mode: 'no-cors', + }); +} From 55217ad7d7fc1f8d66e023f4fc38d3fe7cabedb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:27:22 +0000 Subject: [PATCH 7/7] feat: open Brevo URL in new tab as fallback when fetch fails Co-authored-by: bobbyiliev <21223421+bobbyiliev@users.noreply.github.com> --- components/book-promotion-popup.tsx | 7 +++++-- hooks/use-newsletter-subscribe.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/components/book-promotion-popup.tsx b/components/book-promotion-popup.tsx index 584858897..6827b84e4 100644 --- a/components/book-promotion-popup.tsx +++ b/components/book-promotion-popup.tsx @@ -5,7 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { BookOpen, X, Sparkles } from 'lucide-react' import { Button } from '@/components/ui/button' import Confetti from 'react-confetti' -import { EMAIL_RE, submitToBrevo } from '@/lib/newsletter' +import { EMAIL_RE, BREVO_FORM_URL, submitToBrevo } from '@/lib/newsletter' export function BookPromotionPopup() { const [isVisible, setIsVisible] = useState(false) @@ -70,7 +70,10 @@ export function BookPromotionPopup() { await submitToBrevo(email); } catch (err) { console.error('[newsletter] Popup subscription error:', err); - // Proceed optimistically – show thank you regardless + const w = window.open(BREVO_FORM_URL, '_blank'); + if (!w) { + console.warn('[newsletter] Popup blocked. Direct user to:', BREVO_FORM_URL); + } } // Show thank you message diff --git a/hooks/use-newsletter-subscribe.ts b/hooks/use-newsletter-subscribe.ts index c292fc262..c6c159b9f 100644 --- a/hooks/use-newsletter-subscribe.ts +++ b/hooks/use-newsletter-subscribe.ts @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { EMAIL_RE, submitToBrevo } from '@/lib/newsletter'; +import { BREVO_FORM_URL, EMAIL_RE, submitToBrevo } from '@/lib/newsletter'; export type SubscribeStatus = 'idle' | 'loading' | 'success' | 'error'; @@ -18,6 +18,10 @@ export function useNewsletterSubscribe() { await submitToBrevo(email); setStatus('success'); } catch { + const w = window.open(BREVO_FORM_URL, '_blank'); + if (!w) { + console.warn('[newsletter] Popup blocked. Direct user to:', BREVO_FORM_URL); + } setStatus('error'); } };