Skip to content

feat(site): add newsletter page#7711

Open
aidankmcalister wants to merge 5 commits intomainfrom
feat/newsletter-page
Open

feat(site): add newsletter page#7711
aidankmcalister wants to merge 5 commits intomainfrom
feat/newsletter-page

Conversation

@aidankmcalister
Copy link
Copy Markdown
Member

@aidankmcalister aidankmcalister commented Mar 26, 2026

Summary by CodeRabbit

  • New Features

    • Added a newsletter signup page with a controlled email subscription form and contextual submission feedback (submitting, success, error, already-subscribed).
    • “Latest from the Blog” section shows recent posts with optional image previews and formatted publication dates.
  • Public API

    • RSS item format now supports an optional image URL for posts.
  • Chores

    • Content-Security-Policy updated to allow additional localhost and Prisma image sources.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Mar 27, 2026 9:10pm
docs Ready Ready Preview, Comment Mar 27, 2026 9:10pm
eclipse Ready Ready Preview, Comment Mar 27, 2026 9:10pm
site Ready Ready Preview, Comment Mar 27, 2026 9:10pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a client-side NewsletterSignup React component, a server-rendered NewsletterPage that shows the signup and up to three parsed RSS blog posts (with images), augments internal RSS generation to include optional image URLs, and extends the Content-Security-Policy to allow additional image hosts.

Changes

Cohort / File(s) Summary
Newsletter Signup
apps/site/src/app/newsletter/newsletter-signup.tsx
New exported client React component NewsletterSignup using useNewsletter({}) to manage email and submission state, renders controlled Input/Button, disables controls while submitting/already subscribed/submitted, awaits async subscribe() on submit, and conditionally shows error/success/duplicate status text.
Newsletter Page & RSS parsing
apps/site/src/app/newsletter/page.tsx
New metadata export and default async NewsletterPage that renders a hero and signup card, fetches/parses https://www.prisma.io/blog/rss.xml (revalidate 3600), extracts up to 3 <item> entries via regex, derives title, link, date (pubDate), stripped/truncated description, optionally extracts an image URL (enclosure or legacy image), formats dates to UTC en-US, and conditionally renders “Latest from the Blog” cards with optional next/image previews.
Blog RSS generation
apps/blog/src/lib/rss.ts
RSS item construction updated to include an image field: prefers page.data.metaImagePath then page.data.heroImagePath, converts via withBlogBasePathForImageSrc, and builds an absolute URL when the path begins with /; preserves existing id, url, title, description, date.
Public RSS types & generator
packages/ui/src/lib/rss.ts
Public RSSItem interface extended with optional image?: string; generateRSS updated to include image in feed.addItem(...) calls when present.
Next.js config — CSP
apps/site/next.config.mjs
Content-Security-Policy img-src updated to allow http://localhost:3002, http://127.0.0.1:3002, https://www.prisma.io, and https://prisma.io in addition to existing image sources.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(site): add newsletter page' accurately describes the primary change—introducing a new newsletter page component to the site application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@argos-ci
Copy link
Copy Markdown

argos-ci bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Mar 27, 2026, 9:15 PM

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/app/newsletter/newsletter-signup.tsx`:
- Around line 66-69: The statusMessage paragraph is not in an ARIA live region
so screen-readers may not announce updates; update the JSX that renders
statusMessage (the <p> that references statusMessage.text) to be an ARIA live
region (e.g., add role="status" and aria-live="polite" and aria-atomic="true")
and ensure the element remains in the DOM even when statusMessage is empty
(render an empty/visually-hidden element or text node) so updates to the
statusMessage variable are announced to assistive tech.
- Around line 58-64: Replace the nested <input> inside the Button component with
a native button submit by passing type="submit" and the label text as the
Button's children (use the existing isSubmitting conditional to show "..." vs
"Sign me up") so the Button's disabled prop correctly controls the element;
update the Button invocation (the component named Button in this file) to
include type="submit" and remove the inner input. Also make the status message
element (the success/error message rendered after the form) accessible by adding
either aria-live="polite" or role="status" so screen readers announce changes.

In `@apps/site/src/app/newsletter/page.tsx`:
- Around line 27-30: The fetch call that reads the RSS feed (const res = await
fetch(...)) currently parses the response body even for non-2xx responses;
update the code in page.tsx to check res.ok after the fetch and before calling
res.text() (and do the same for the second fetch at the other occurrence), and
if !res.ok log an error including res.status and either throw or return the
fallback path so the parser isn't given partial/HTML bodies; reference the
existing variables res and xml and ensure the fallback/logging is explicit so
failures are visible.
- Around line 78-79: The top-level await in NewsletterPage blocks rendering;
move the call to getLatestBlogPosts into a Suspense-wrapped async child so the
signup form renders immediately. Create a new async component (e.g.,
LatestBlogPosts) that calls getLatestBlogPosts(3) and returns the blog cards,
then replace the direct await in NewsletterPage with <Suspense fallback={/*
lightweight placeholder or spinner */}><LatestBlogPosts /></Suspense>. Keep
NewsletterPage synchronous and ensure the Suspense fallback matches the layout
to avoid layout shift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01c35fa4-e699-4efd-9312-bfb7d189cd75

📥 Commits

Reviewing files that changed from the base of the PR and between edda9c3 and bfbadfe.

📒 Files selected for processing (2)
  • apps/site/src/app/newsletter/newsletter-signup.tsx
  • apps/site/src/app/newsletter/page.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/site/src/app/newsletter/page.tsx (1)

117-117: Consider target="_blank" for external blog links if they're full URLs.

If post.link contains absolute URLs (e.g., https://www.prisma.io/blog/...), the current implementation keeps users in the same tab. This is fine if that's intentional since it's the same domain family, but worth confirming the expected behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/newsletter/page.tsx` at line 117, If post.link can be an
absolute external URL, update the rendering around the Link element (the Link
with key={post.link} and className="group") to open externals in a new tab by
adding target="_blank" and rel="noopener noreferrer" for security; implement
this conditionally by detecting absolute URLs (e.g., post.link
startsWith('http') or using URL parsing) so internal/local links remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/site/src/app/newsletter/page.tsx`:
- Line 117: If post.link can be an absolute external URL, update the rendering
around the Link element (the Link with key={post.link} and className="group") to
open externals in a new tab by adding target="_blank" and rel="noopener
noreferrer" for security; implement this conditionally by detecting absolute
URLs (e.g., post.link startsWith('http') or using URL parsing) so internal/local
links remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c104b88-41e8-45fb-b423-48549c705c4a

📥 Commits

Reviewing files that changed from the base of the PR and between bfbadfe and 27e036c.

📒 Files selected for processing (2)
  • apps/site/src/app/newsletter/newsletter-signup.tsx
  • apps/site/src/app/newsletter/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/site/src/app/newsletter/newsletter-signup.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/site/src/app/newsletter/newsletter-signup.tsx (2)

62-68: ⚠️ Potential issue | 🟠 Major

Use Button as the submit control directly.

Line 62 currently wraps an interactive <input type="submit"> (Lines 63-67) inside Button, which creates nested interactive elements and invalid HTML semantics.

Suggested fix
-        <Button variant="ppg" size="2xl" disabled={disabled}>
-          <input
-            type="submit"
-            value={isSubmitting ? "..." : "Sign me up"}
-            className="cursor-pointer"
-          />
-        </Button>
+        <Button type="submit" variant="ppg" size="2xl" disabled={disabled}>
+          {isSubmitting ? "..." : "Sign me up"}
+        </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/newsletter/newsletter-signup.tsx` around lines 62 - 68,
Replace the nested <input type="submit"> with the Button component as the form
submit control: remove the inner input and render Button with type="submit",
keep disabled={disabled}, preserve variant="ppg" and size="2xl", and use the
isSubmitting ? "..." : "Sign me up" string as the Button label so there are no
nested interactive elements (look for the Button usage around the newsletter
signup form and the isSubmitting/disabled variables).

70-74: ⚠️ Potential issue | 🟠 Major

Announce submit status changes via a live region.

Lines 70-74 render important success/error feedback, but without live-region semantics this update may not be announced to screen-reader users.

Suggested fix
-      {statusMessage ? (
-        <p className={`m-0 text-sm ${statusMessage.className}`}>
-          {statusMessage.text}
-        </p>
-      ) : null}
+      <p
+        role={error ? "alert" : "status"}
+        aria-live={error ? "assertive" : "polite"}
+        aria-atomic="true"
+        className={`m-0 text-sm ${statusMessage?.className ?? ""}`}
+      >
+        {statusMessage?.text ?? ""}
+      </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/newsletter/newsletter-signup.tsx` around lines 70 - 74, The
statusMessage paragraph isn't exposed to assistive tech; wrap or replace the
conditional rendering so the status text is placed inside an ARIA live region
(e.g., add role="status" and aria-live="polite" and aria-atomic="true") so
screen readers announce updates from the newsletter-signup component; update the
JSX that renders statusMessage (the element using statusMessage.text /
statusMessage.className) to include these attributes or render the text inside a
dedicated <div> with those attributes.
apps/site/src/app/newsletter/page.tsx (1)

26-30: ⚠️ Potential issue | 🟡 Minor

Handle non-2xx RSS responses before parsing, and log failures.

Line 29 parses response text even when the RSS endpoint returns an error status, and Lines 60-62 swallow parse/fetch errors without telemetry.

Suggested fix
     const res = await fetch("https://www.prisma.io/blog/rss.xml", {
       next: { revalidate: 3600 },
     });
+    if (!res.ok) {
+      console.error("Failed to fetch blog RSS", res.status, res.statusText);
+      return [];
+    }
     const xml = await res.text();
@@
-  } catch {
+  } catch (error) {
+    console.error("Failed to parse blog RSS", error);
     return [];
   }

Also applies to: 60-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/newsletter/page.tsx` around lines 26 - 30, The fetch of the
RSS feed currently calls res.text() and proceeds to parsing even when the HTTP
response is non-2xx and later parse/fetch errors are swallowed; update the fetch
logic around the variables res and xml in page.tsx to check res.ok (or status)
immediately after the fetch and handle non-2xx by logging the status and
statusText (using the same logger/telemetry the app uses) and return or throw a
descriptive error before calling res.text(); also ensure the try/catch around
the parsing (the block that currently swallows errors at lines ~60-62) logs the
exception to telemetry and rethrows or returns a safe fallback so failures are
not silently ignored (reference res, xml, and the parse block where errors are
currently caught).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/app/newsletter/page.tsx`:
- Line 10: Replace the typo "Get releases updates" with "Get release updates"
everywhere it appears in this file: search for the exact string "Get releases
updates, tutorials, and more content delivered to your inbox monthly." (and the
duplicate occurrences around lines ~99-100) and update both the metadata export
and any JSX/body text where that string is used so the user-facing copy reads
"Get release updates, tutorials, and more content delivered to your inbox
monthly."

---

Duplicate comments:
In `@apps/site/src/app/newsletter/newsletter-signup.tsx`:
- Around line 62-68: Replace the nested <input type="submit"> with the Button
component as the form submit control: remove the inner input and render Button
with type="submit", keep disabled={disabled}, preserve variant="ppg" and
size="2xl", and use the isSubmitting ? "..." : "Sign me up" string as the Button
label so there are no nested interactive elements (look for the Button usage
around the newsletter signup form and the isSubmitting/disabled variables).
- Around line 70-74: The statusMessage paragraph isn't exposed to assistive
tech; wrap or replace the conditional rendering so the status text is placed
inside an ARIA live region (e.g., add role="status" and aria-live="polite" and
aria-atomic="true") so screen readers announce updates from the
newsletter-signup component; update the JSX that renders statusMessage (the
element using statusMessage.text / statusMessage.className) to include these
attributes or render the text inside a dedicated <div> with those attributes.

In `@apps/site/src/app/newsletter/page.tsx`:
- Around line 26-30: The fetch of the RSS feed currently calls res.text() and
proceeds to parsing even when the HTTP response is non-2xx and later parse/fetch
errors are swallowed; update the fetch logic around the variables res and xml in
page.tsx to check res.ok (or status) immediately after the fetch and handle
non-2xx by logging the status and statusText (using the same logger/telemetry
the app uses) and return or throw a descriptive error before calling res.text();
also ensure the try/catch around the parsing (the block that currently swallows
errors at lines ~60-62) logs the exception to telemetry and rethrows or returns
a safe fallback so failures are not silently ignored (reference res, xml, and
the parse block where errors are currently caught).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba073e1e-58e5-495a-9d43-62c223817193

📥 Commits

Reviewing files that changed from the base of the PR and between 27e036c and 83ba77d.

📒 Files selected for processing (2)
  • apps/site/src/app/newsletter/newsletter-signup.tsx
  • apps/site/src/app/newsletter/page.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blog/src/lib/rss.ts`:
- Around line 28-33: The image URL logic discards already-absolute cover URLs
because it only handles rel starting with "/" — change the conditional in the
image resolution code (variables cover, rel, image and helper
withBlogBasePathForImageSrc) so that: if rel starts with "/" then prefix baseUrl
(baseUrl + rel), else if rel already is an absolute URL (starts with "http://",
"https://" or protocol-relative "//") keep rel as-is, otherwise fall back to
undefined; update the expression that computes image to implement this three-way
check so absolute URLs are preserved.

In `@apps/site/next.config.mjs`:
- Around line 53-54: The CSP currently includes loopback image origins
("http://localhost:3002" and "http://127.0.0.1:3002") unconditionally; update
the headers generation (the exported headers function / contentSecurityPolicy
variable in next.config.mjs) to append those two origins to the img-src only in
development (e.g. when process.env.NODE_ENV === 'development' or a DEV flag),
and ensure the production branch/build path constructs the img-src without those
loopback origins so deployed pages do not permit localhost or 127.0.0.1.

In `@apps/site/src/app/newsletter/page.tsx`:
- Around line 32-35: The fetch in page.tsx (used by NewsletterPage) hardcodes
"http://localhost:3002/blog/rss.xml" which breaks in preview/prod; change it to
resolve the feed URL from the deployment/config instead of localhost — e.g.
derive the origin from a runtime-config/env var (NEXT_PUBLIC_SITE_URL or
NEXT_PUBLIC_FEED_URL) or build the URL with new URL('/blog/rss.xml',
process.env.NEXT_PUBLIC_SITE_URL) and use that in the fetch call (keep next: {
revalidate: 3600 } intact); update any server-component code that calls fetch to
use this resolved URL so the feed works in preview/production.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 801d789a-70af-49d4-b5cf-198360ee1d9a

📥 Commits

Reviewing files that changed from the base of the PR and between 83ba77d and e00d25b.

📒 Files selected for processing (4)
  • apps/blog/src/lib/rss.ts
  • apps/site/next.config.mjs
  • apps/site/src/app/newsletter/page.tsx
  • packages/ui/src/lib/rss.ts

Comment on lines +28 to +33
const cover = page.data.metaImagePath ?? page.data.heroImagePath;
const rel = cover ? withBlogBasePathForImageSrc(cover) : "";
const image =
rel.startsWith("/")
? `${baseUrl.replace(/\/$/, "")}${rel}`
: undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve already-absolute cover URLs.

withBlogBasePathForImageSrc() leaves non-root URLs unchanged. The current rel.startsWith("/") check turns those values into undefined, so any post whose metaImagePath/heroImagePath is already absolute loses its RSS image and the newsletter card falls back to text-only.

Suggested fix
       const cover = page.data.metaImagePath ?? page.data.heroImagePath;
       const rel = cover ? withBlogBasePathForImageSrc(cover) : "";
       const image =
         rel.startsWith("/")
           ? `${baseUrl.replace(/\/$/, "")}${rel}`
-          : undefined;
+          : /^https?:\/\//i.test(rel)
+            ? rel
+            : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blog/src/lib/rss.ts` around lines 28 - 33, The image URL logic discards
already-absolute cover URLs because it only handles rel starting with "/" —
change the conditional in the image resolution code (variables cover, rel, image
and helper withBlogBasePathForImageSrc) so that: if rel starts with "/" then
prefix baseUrl (baseUrl + rel), else if rel already is an absolute URL (starts
with "http://", "https://" or protocol-relative "//") keep rel as-is, otherwise
fall back to undefined; update the expression that computes image to implement
this three-way check so absolute URLs are preserved.

Comment on lines +53 to +54
http://localhost:3002 http://127.0.0.1:3002
https://www.prisma.io https://prisma.io
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep loopback image origins out of the production CSP.

These headers are applied to /:path*, so every deployed page now permits localhost:3002 and 127.0.0.1:3002 in img-src. That broadens the site-wide CSP for no production benefit and weakens loopback isolation. Please gate these origins behind a development-only branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/next.config.mjs` around lines 53 - 54, The CSP currently includes
loopback image origins ("http://localhost:3002" and "http://127.0.0.1:3002")
unconditionally; update the headers generation (the exported headers function /
contentSecurityPolicy variable in next.config.mjs) to append those two origins
to the img-src only in development (e.g. when process.env.NODE_ENV ===
'development' or a DEV flag), and ensure the production branch/build path
constructs the img-src without those loopback origins so deployed pages do not
permit localhost or 127.0.0.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants