From 5a8d7106c4fb353af9c1e94346bd5166a5e112fd Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Fri, 2 Jan 2026 13:55:34 +0000 Subject: [PATCH 1/3] feat(newsletter): migrate to Beehiiv API for subscription management and update privacy policy --- app/(app)/(tsandcs)/privacy/page.mdx | 18 ++- app/(app)/(tsandcs)/tou/page.mdx | 101 ++++++++++++ app/(standalone)/newsletter/_form.tsx | 70 --------- app/(standalone)/newsletter/actions.ts | 63 -------- .../newsletter/confirmed/page.tsx | 40 ----- app/(standalone)/newsletter/page.tsx | 110 ------------- .../newsletter/unsubscribed/page.tsx | 45 ------ next.config.js | 14 ++ server/lib/newsletter.ts | 145 ++++++++++++------ 9 files changed, 232 insertions(+), 374 deletions(-) create mode 100644 app/(app)/(tsandcs)/tou/page.mdx delete mode 100644 app/(standalone)/newsletter/_form.tsx delete mode 100644 app/(standalone)/newsletter/actions.ts delete mode 100644 app/(standalone)/newsletter/confirmed/page.tsx delete mode 100644 app/(standalone)/newsletter/page.tsx delete mode 100644 app/(standalone)/newsletter/unsubscribed/page.tsx diff --git a/app/(app)/(tsandcs)/privacy/page.mdx b/app/(app)/(tsandcs)/privacy/page.mdx index 17630f97..169eb617 100644 --- a/app/(app)/(tsandcs)/privacy/page.mdx +++ b/app/(app)/(tsandcs)/privacy/page.mdx @@ -1,13 +1,15 @@ # Privacy Policy -Last updated March 01, 2024 +Last updated January 02, 2025 This privacy notice for Codú Limited (doing business as Codú) ("**we**," "**us**," or "**our**"), describes how and why we might collect, store, use, and/or share ("**process**") your information when you use our services ("**Services**"), such as when you: -This Policy supplements and is governed by our [Terms of Service](http://www.codu.co/terms) (“Terms”). +This Policy supplements and is governed by our [Terms of Service](http://www.codu.co/terms) ("Terms") and [Newsletter Terms of Use](http://www.codu.co/tou). - Visit our website at [http://www.codu.co](http://www.codu.co), or any website of ours that links to this privacy notice +- Subscribe to or read our newsletter at [https://newsletter.codu.co](https://newsletter.codu.co) + - Engage with us in other related ways, including any sales, marketing, or events **Questions or concerns?** Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you still have any questions or concerns, please contact us at niall@codu.co. @@ -90,6 +92,18 @@ We collect personal information that you voluntarily provide to us when you regi **Sensitive Information.** We do not process sensitive information. +**Newsletter Subscriber Data.** When you subscribe to our newsletter, we collect and process the following information: + +- Email address (required for subscription) +- Subscription date and source +- Email engagement data (whether you open emails, which links you click) +- Geographic location at country/region level (derived from IP address) +- Device and email client information +- Survey responses (if you participate in newsletter surveys) +- Referral information (if you were referred by another subscriber) + +This data helps us deliver relevant content, understand our audience, and improve our newsletter. We use Beehiiv as our newsletter platform, and your data is processed in accordance with their privacy practices. You can unsubscribe at any time using the link in any newsletter email. + **Social Media Login Data.** We may provide you with the option to register with us using your existing social media account details, like your Facebook, Twitter, or other social media account. If you choose to register in this way, we will collect the information described in the section called "HOW DO WE HANDLE YOUR SOCIAL LOGINS?" below. All personal information that you provide to us must be true, complete, and accurate, and you must notify us of any changes to such personal information. diff --git a/app/(app)/(tsandcs)/tou/page.mdx b/app/(app)/(tsandcs)/tou/page.mdx new file mode 100644 index 00000000..a7adbc16 --- /dev/null +++ b/app/(app)/(tsandcs)/tou/page.mdx @@ -0,0 +1,101 @@ +# Newsletter Terms of Use + +Last updated January 02, 2025 + +Welcome to the Codú Newsletter. By subscribing to or reading our newsletter, you accept and agree to be bound by these Terms of Use. If you do not agree to these terms, please do not subscribe to or use our newsletter. + +## 1. Acceptance and Eligibility + +By subscribing to the Codú Newsletter, you confirm that you are at least 16 years of age and have the legal capacity to enter into these terms. If you are subscribing on behalf of an organization, you represent that you have the authority to bind that organization to these terms. + +## 2. Newsletter Service + +The Codú Newsletter provides content related to web development, programming, and technology. We aim to deliver valuable insights, tutorials, and industry news to help you grow as a developer. + +**Service includes:** +- Weekly email newsletters +- Curated content and resources +- Community updates and announcements + +## 3. Subscription and Unsubscription + +- You may subscribe to our newsletter by providing your email address through our website or partner platforms +- You may unsubscribe at any time by clicking the unsubscribe link in any newsletter email +- We will process your unsubscription request promptly + +## 4. Content and Intellectual Property + +All content in our newsletters, including text, graphics, logos, and images, is owned by Codú Limited or its content suppliers and is protected by intellectual property laws. + +**You may:** +- Read and share newsletter content for personal, non-commercial purposes +- Share links to our content with proper attribution + +**You may not:** +- Reproduce, distribute, or republish newsletter content without permission +- Use our content for commercial purposes without prior written consent +- Remove any copyright or proprietary notices from our content + +## 5. User Conduct + +As a subscriber, you agree not to: + +- Provide false or misleading information when subscribing +- Use automated systems to subscribe multiple email addresses +- Attempt to interfere with our newsletter delivery systems +- Use our newsletter content to spam or harass others + +## 6. Third-Party Links and Content + +Our newsletters may contain links to third-party websites or resources. We are not responsible for: + +- The availability or accuracy of such external sites or resources +- Any content, advertising, products, or services on or available from such sites +- Any damage or loss caused by your use of such content or resources + +## 7. Disclaimer of Warranties + +Our newsletter is provided on an "AS IS" and "AS AVAILABLE" basis without warranties of any kind, either express or implied. + +We do not warrant that: +- The newsletter will be uninterrupted or error-free +- The content will be accurate, complete, or current +- Any errors will be corrected + +## 8. Limitation of Liability + +To the maximum extent permitted by law, Codú Limited shall not be liable for any indirect, incidental, special, consequential, or punitive damages arising from your use of or inability to use our newsletter. + +## 9. Privacy + +Your privacy is important to us. Please review our [Privacy Policy](/privacy) to understand how we collect, use, and protect your information when you subscribe to our newsletter. + +**Newsletter-specific data we collect:** +- Email address +- Subscription preferences +- Email engagement metrics (opens, clicks) +- Geographic location (country/region level) + +## 10. Modifications to Terms + +We reserve the right to modify these terms at any time. Changes will be effective immediately upon posting. Your continued subscription after changes constitutes acceptance of the modified terms. + +## 11. Termination + +We may terminate or suspend your subscription at any time, without prior notice, for conduct that we believe: + +- Violates these terms +- Is harmful to other subscribers or third parties +- Is fraudulent or illegal + +## 12. Governing Law + +These terms shall be governed by and construed in accordance with the laws of Ireland, without regard to its conflict of law provisions. + +## 13. Contact Information + +If you have any questions about these Terms of Use, please contact us: + +**Codú Limited** +Email: niall@codu.co +Website: [https://www.codu.co](https://www.codu.co) diff --git a/app/(standalone)/newsletter/_form.tsx b/app/(standalone)/newsletter/_form.tsx deleted file mode 100644 index 3552afe3..00000000 --- a/app/(standalone)/newsletter/_form.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { useFormState, useFormStatus } from "react-dom"; -import { subscribeToNewsletter } from "./actions"; -import { toast } from "sonner"; -import { useEffect } from "react"; - -const initialState = { - message: "", -}; - -function SubmitButton({ disabled }: { disabled: boolean }) { - const { pending } = useFormStatus(); - - return ( - - ); -} - -export function SignupForm() { - const [state, formAction] = useFormState(subscribeToNewsletter, initialState); - - useEffect(() => { - if (state.message === "success") { - toast.success( - "Nearly there! Check your inbox to confirm your subscription.", - { duration: 5000, position: "bottom-center" }, - ); - } else if (state.message === "error") { - toast.error("Something went wrong. Please try again.", { - position: "bottom-center", - }); - } - }, [state.message]); - - return ( -
- - - -

- {state.message === "success" - ? "You're subscribed! " - : state.message === "error" - ? "Something went wrong. Please try again." - : null} -

- - ); -} diff --git a/app/(standalone)/newsletter/actions.ts b/app/(standalone)/newsletter/actions.ts deleted file mode 100644 index 2c980af3..00000000 --- a/app/(standalone)/newsletter/actions.ts +++ /dev/null @@ -1,63 +0,0 @@ -"use server"; - -import { z } from "zod"; - -const FormDataSchema = z.object({ - email: z - .string({ error: "Email is required" }) - .email({ error: "Must be a valid email address" }), -}); - -//@TODO - Add sentry to eat errors - -const errorResponse = { message: "error" }; - -export async function subscribeToNewsletter( - prevState: { message: string }, - formData: FormData, -): Promise<{ message: string }> { - try { - const result = FormDataSchema.parse({ - email: formData.get("email"), - }); - - const { email } = result; - - const EMAIL_API_ENDPOINT = process.env.EMAIL_API_ENDPOINT; - const EMAIL_API_KEY = process.env.EMAIL_API_KEY; - const EMAIL_NEWSLETTER_ID = process.env.EMAIL_NEWSLETTER_ID; - - if (!EMAIL_API_ENDPOINT || !EMAIL_API_KEY || !EMAIL_NEWSLETTER_ID) { - console.log("Email API not configured"); - return errorResponse; - } - - const payload = new URLSearchParams({ - email, - api_key: EMAIL_API_KEY, - list: EMAIL_NEWSLETTER_ID, - boolean: "true", - }).toString(); - - const response = await fetch(`${EMAIL_API_ENDPOINT}/subscribe`, { - method: "POST", - headers: { - "Content-type": "application/x-www-form-urlencoded", - }, - body: payload, - }); - - if (response.ok) { - // Send confirmation email to user - return { - message: "success", - }; - } else { - console.log("Error:", response.status); - return errorResponse; - } - } catch (error) { - console.log(error); - return errorResponse; - } -} diff --git a/app/(standalone)/newsletter/confirmed/page.tsx b/app/(standalone)/newsletter/confirmed/page.tsx deleted file mode 100644 index f0944d49..00000000 --- a/app/(standalone)/newsletter/confirmed/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Your email has been confirmed! Welcome to Codú Weekly.", - robots: { - follow: false, - index: false, - }, -}; - -export default function NewsletterConfirmationPage() { - return ( -
-
-
-
-

- Your email is confirmed! -

-

- {`Now, you just have to wait for the next newsletter to arrive. Thanks for joining!`} -

-
-
-
- - ); -} diff --git a/app/(standalone)/newsletter/page.tsx b/app/(standalone)/newsletter/page.tsx deleted file mode 100644 index cce1b92b..00000000 --- a/app/(standalone)/newsletter/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { Metadata } from "next"; -import { CalendarDaysIcon, HandRaisedIcon } from "@heroicons/react/24/outline"; -import { SignupForm } from "./_form"; -import Link from "next/link"; - -export const metadata: Metadata = { - title: "Codú Weekly - The Newsletter for Web Developers", - description: - "The newsletter for Web Developers delivered weekly and 100% free. Get weekly articles delivered to your inbox.", - keywords: [ - "programming", - "frontend", - "community", - "learn", - "programmer", - "article", - "Python", - "JavaScript", - "AWS", - "HTML", - "CSS", - "Tailwind", - "React", - "email", - "backend", - "newsletter", - ], - metadataBase: new URL("https://www.codu.co"), - openGraph: { - images: "/images/og/newsletter.png", - }, -}; - -export default function NewsletterPage() { - return ( -
-
-
-
-

- Subscribe to Codú Weekly. -

-

- Join our free newsletter!{" "} - - One newsletter - {" "} - per week with the latest articles, tips, and insights on web - development. Don't miss out on valuable content delivered - straight to your inbox! -

- -
-
-
-
-
-
- Weekly newsletter -
-
- Learn something new every Tuesday. We cover a wide range of - topics in web development - frontend, backend, and full stack. -
-
-
-
-
-
No spam
-
- Just quality content. We respect your privacy. Unsubscribe at - any time. -
-
-
-
-
- - ); -} diff --git a/app/(standalone)/newsletter/unsubscribed/page.tsx b/app/(standalone)/newsletter/unsubscribed/page.tsx deleted file mode 100644 index ef20b571..00000000 --- a/app/(standalone)/newsletter/unsubscribed/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; - -export const metadata: Metadata = { - title: "You are now unsubscribed. We are sad to see you go.", - robots: { - follow: false, - index: false, - }, -}; - -export default function NewsletterUnsubscribedPage() { - return ( -
-
-
-
-

- You are now unsubscribed. -

-

- We are sad to see you go. If you change your mind, you can always{" "} - - resubscribe - - . -

-
-
-
- - ); -} diff --git a/next.config.js b/next.config.js index 0b1d983b..b2e9d730 100644 --- a/next.config.js +++ b/next.config.js @@ -20,6 +20,20 @@ const REMOTE_PATTERNS = [ })); const config = { + async redirects() { + return [ + { + source: "/newsletter", + destination: "https://newsletter.codu.co", + permanent: true, + }, + { + source: "/newsletter/:path*", + destination: "https://newsletter.codu.co", + permanent: true, + }, + ]; + }, // Turbopack configuration for SVGR (replaces webpack config) turbopack: { rules: { diff --git a/server/lib/newsletter.ts b/server/lib/newsletter.ts index 9fdfe156..8c97b297 100644 --- a/server/lib/newsletter.ts +++ b/server/lib/newsletter.ts @@ -1,69 +1,126 @@ import * as Sentry from "@sentry/nextjs"; +const BEEHIIV_API_BASE = "https://api.beehiiv.com/v2"; + +interface BeehiivSubscription { + id: string; + email: string; + status: "validating" | "active" | "inactive" | "pending"; + created_at: number; +} + +interface BeehiivResponse { + data: BeehiivSubscription; +} + +function getBeehiivConfig() { + const BEEHIIV_API_KEY = process.env.BEEHIIV_API_KEY; + const BEEHIIV_PUBLICATION_ID = process.env.BEEHIIV_PUBLICATION_ID; + + if (!BEEHIIV_API_KEY || !BEEHIIV_PUBLICATION_ID) { + throw new Error("Beehiiv API not configured"); + } + + return { BEEHIIV_API_KEY, BEEHIIV_PUBLICATION_ID }; +} + export async function manageNewsletterSubscription( email: string, action: "subscribe" | "unsubscribe", ): Promise<{ message: string } | undefined> { - const EMAIL_API_ENDPOINT = process.env.EMAIL_API_ENDPOINT; - const EMAIL_API_KEY = process.env.EMAIL_API_KEY; - const EMAIL_NEWSLETTER_ID = process.env.EMAIL_NEWSLETTER_ID; - - if (!EMAIL_API_ENDPOINT || !EMAIL_API_KEY || !EMAIL_NEWSLETTER_ID) { - throw new Error("Email API not configured"); - } + const { BEEHIIV_API_KEY, BEEHIIV_PUBLICATION_ID } = getBeehiivConfig(); - const payload = new URLSearchParams({ - email, - api_key: EMAIL_API_KEY, - list: EMAIL_NEWSLETTER_ID, - boolean: "true", - silent: "true", // Don't send a confirmation email (using this option for users signed up to platform, not newsletter only option) - }).toString(); - - const response = await fetch(`${EMAIL_API_ENDPOINT}/${action}`, { - method: "POST", - headers: { - "Content-type": "application/x-www-form-urlencoded", - }, - body: payload, - }); + if (action === "subscribe") { + const response = await fetch( + `${BEEHIIV_API_BASE}/publications/${BEEHIIV_PUBLICATION_ID}/subscriptions`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${BEEHIIV_API_KEY}`, + }, + body: JSON.stringify({ + email, + reactivate_existing: true, + send_welcome_email: false, + }), + }, + ); - if (response.ok) { - return { message: `Successfully ${action}d to the newsletter.` }; + if (response.ok) { + return { message: "Successfully subscribed to the newsletter." }; + } else { + const errorData = await response.text(); + Sentry.captureMessage(`Beehiiv subscribe failed: ${errorData}`); + throw new Error("Failed to subscribe to the newsletter"); + } } else { - throw new Error(`Failed to ${action} to the newsletter`); - } -} + // Unsubscribe: First get subscription by email, then update it + const encodedEmail = encodeURIComponent(email); + const getResponse = await fetch( + `${BEEHIIV_API_BASE}/publications/${BEEHIIV_PUBLICATION_ID}/subscriptions/by_email/${encodedEmail}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${BEEHIIV_API_KEY}`, + }, + }, + ); -export async function isUserSubscribedToNewsletter(email: string) { - const EMAIL_API_ENDPOINT = process.env.EMAIL_API_ENDPOINT; - const EMAIL_API_KEY = process.env.EMAIL_API_KEY; - const EMAIL_NEWSLETTER_ID = process.env.EMAIL_NEWSLETTER_ID; + if (!getResponse.ok) { + if (getResponse.status === 404) { + return { message: "Successfully unsubscribed from the newsletter." }; + } + throw new Error("Failed to find subscription"); + } - if (!EMAIL_API_ENDPOINT || !EMAIL_API_KEY || !EMAIL_NEWSLETTER_ID) { - throw new Error("Email API not configured"); + const subscriptionData: BeehiivResponse = await getResponse.json(); + const subscriptionId = subscriptionData.data.id; + + const updateResponse = await fetch( + `${BEEHIIV_API_BASE}/publications/${BEEHIIV_PUBLICATION_ID}/subscriptions/${subscriptionId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${BEEHIIV_API_KEY}`, + }, + body: JSON.stringify({ + unsubscribe: true, + }), + }, + ); + + if (updateResponse.ok) { + return { message: "Successfully unsubscribed from the newsletter." }; + } else { + throw new Error("Failed to unsubscribe from the newsletter"); + } } +} + +export async function isUserSubscribedToNewsletter( + email: string, +): Promise { + const { BEEHIIV_API_KEY, BEEHIIV_PUBLICATION_ID } = getBeehiivConfig(); - const payload = new URLSearchParams({ - email, - api_key: EMAIL_API_KEY, - list_id: EMAIL_NEWSLETTER_ID, - }).toString(); + const encodedEmail = encodeURIComponent(email); const response = await fetch( - `${EMAIL_API_ENDPOINT}/api/subscribers/subscription-status.php`, + `${BEEHIIV_API_BASE}/publications/${BEEHIIV_PUBLICATION_ID}/subscriptions/by_email/${encodedEmail}`, { - method: "POST", + method: "GET", headers: { - "Content-type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${BEEHIIV_API_KEY}`, }, - body: payload, }, ); if (response.ok) { - const status = await response.text(); - return status === "Subscribed"; + const data: BeehiivResponse = await response.json(); + return data.data.status === "active"; + } else if (response.status === 404) { + return false; } else { throw new Error("Failed to check newsletter subscription"); } From 2662a3d1d54372653afffe7f7a43c88cdf208b08 Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Fri, 2 Jan 2026 13:59:03 +0000 Subject: [PATCH 2/3] fix(tou): add missing line breaks for improved readability in Terms of Use --- app/(app)/(tsandcs)/tou/page.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/(app)/(tsandcs)/tou/page.mdx b/app/(app)/(tsandcs)/tou/page.mdx index a7adbc16..b23f8c01 100644 --- a/app/(app)/(tsandcs)/tou/page.mdx +++ b/app/(app)/(tsandcs)/tou/page.mdx @@ -13,6 +13,7 @@ By subscribing to the Codú Newsletter, you confirm that you are at least 16 yea The Codú Newsletter provides content related to web development, programming, and technology. We aim to deliver valuable insights, tutorials, and industry news to help you grow as a developer. **Service includes:** + - Weekly email newsletters - Curated content and resources - Community updates and announcements @@ -28,10 +29,12 @@ The Codú Newsletter provides content related to web development, programming, a All content in our newsletters, including text, graphics, logos, and images, is owned by Codú Limited or its content suppliers and is protected by intellectual property laws. **You may:** + - Read and share newsletter content for personal, non-commercial purposes - Share links to our content with proper attribution **You may not:** + - Reproduce, distribute, or republish newsletter content without permission - Use our content for commercial purposes without prior written consent - Remove any copyright or proprietary notices from our content @@ -58,6 +61,7 @@ Our newsletters may contain links to third-party websites or resources. We are n Our newsletter is provided on an "AS IS" and "AS AVAILABLE" basis without warranties of any kind, either express or implied. We do not warrant that: + - The newsletter will be uninterrupted or error-free - The content will be accurate, complete, or current - Any errors will be corrected @@ -71,6 +75,7 @@ To the maximum extent permitted by law, Codú Limited shall not be liable for an Your privacy is important to us. Please review our [Privacy Policy](/privacy) to understand how we collect, use, and protect your information when you subscribe to our newsletter. **Newsletter-specific data we collect:** + - Email address - Subscription preferences - Email engagement metrics (opens, clicks) From 57e8819fe90171129e9c8b9733337bb443c93bda Mon Sep 17 00:00:00 2001 From: NiallJoeMaher Date: Fri, 2 Jan 2026 14:07:14 +0000 Subject: [PATCH 3/3] fix: update last updated date to January 02, 2026 in Privacy Policy and Newsletter Terms of Use --- app/(app)/(tsandcs)/privacy/page.mdx | 2 +- app/(app)/(tsandcs)/tou/page.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(app)/(tsandcs)/privacy/page.mdx b/app/(app)/(tsandcs)/privacy/page.mdx index 169eb617..5235a5d7 100644 --- a/app/(app)/(tsandcs)/privacy/page.mdx +++ b/app/(app)/(tsandcs)/privacy/page.mdx @@ -1,6 +1,6 @@ # Privacy Policy -Last updated January 02, 2025 +Last updated January 02, 2026 This privacy notice for Codú Limited (doing business as Codú) ("**we**," "**us**," or "**our**"), describes how and why we might collect, store, use, and/or share ("**process**") your information when you use our services ("**Services**"), such as when you: diff --git a/app/(app)/(tsandcs)/tou/page.mdx b/app/(app)/(tsandcs)/tou/page.mdx index b23f8c01..d6ee270a 100644 --- a/app/(app)/(tsandcs)/tou/page.mdx +++ b/app/(app)/(tsandcs)/tou/page.mdx @@ -1,6 +1,6 @@ # Newsletter Terms of Use -Last updated January 02, 2025 +Last updated January 02, 2026 Welcome to the Codú Newsletter. By subscribing to or reading our newsletter, you accept and agree to be bound by these Terms of Use. If you do not agree to these terms, please do not subscribe to or use our newsletter.