diff --git a/src/actions/newsletter/@types/index.d.ts b/src/actions/newsletter/@types/index.d.ts
index 369408bb..969db425 100644
--- a/src/actions/newsletter/@types/index.d.ts
+++ b/src/actions/newsletter/@types/index.d.ts
@@ -1,6 +1,7 @@
export type NewsletterFormData = {
email: string
firstName?: string
+ website_url?: string
consentGiven?: boolean
DataSubjectId?: string
}
@@ -22,6 +23,7 @@ export interface PendingSubscription {
export type NewsletterSubscribeInput = {
email: string
firstName?: string
+ website_url?: string
consentGiven?: boolean
DataSubjectId?: string
}
diff --git a/src/actions/newsletter/__tests__/action.spec.ts b/src/actions/newsletter/__tests__/action.spec.ts
index bf57f7ca..5d7d764e 100644
--- a/src/actions/newsletter/__tests__/action.spec.ts
+++ b/src/actions/newsletter/__tests__/action.spec.ts
@@ -138,6 +138,42 @@ beforeEach(() => {
})
describe('newsletter.subscribe.handler', () => {
+ it('silently drops submissions that fill the honeypot field', async () => {
+ const { newsletter } = await import('../action')
+ const { createConsentRecord } = await import('@actions/gdpr/entities/consent')
+ const { createPendingSubscription } = await import('@actions/newsletter/domain')
+ const { sendConfirmationEmail } = await import('@actions/newsletter/entities/email')
+
+ const context = {
+ request: new Request('https://example.com/_actions/newsletter/subscribe', {
+ method: 'POST',
+ headers: { 'user-agent': 'ua-bot' },
+ }),
+ cookies: {} as unknown,
+ clientAddress: '203.0.113.9',
+ }
+
+ const response = await getMockedHandler(
+ newsletter.subscribe
+ )(
+ {
+ email: 'test@example.com',
+ consentGiven: true,
+ website_url: 'https://spam.example',
+ },
+ context
+ )
+
+ expect(response).toEqual({
+ success: true,
+ message: 'Please check your email to confirm your subscription.',
+ requiresConfirmation: true,
+ })
+ expect(createConsentRecord).not.toHaveBeenCalled()
+ expect(createPendingSubscription).not.toHaveBeenCalled()
+ expect(sendConfirmationEmail).not.toHaveBeenCalled()
+ })
+
it('rejects when consent is missing', async () => {
const { newsletter } = await import('../action')
diff --git a/src/actions/newsletter/action.ts b/src/actions/newsletter/action.ts
index 7d674c34..89ef70d0 100644
--- a/src/actions/newsletter/action.ts
+++ b/src/actions/newsletter/action.ts
@@ -22,6 +22,7 @@ import {
const subscribeSchema = z.object({
email: z.string(),
firstName: z.string().optional(),
+ website_url: z.string().trim().max(200).optional(),
consentGiven: z.boolean().optional(),
DataSubjectId: z.string().optional(),
})
@@ -94,6 +95,11 @@ export const newsletter = {
context
): Promise<{ success: true; message: string; requiresConfirmation: true }> => {
const route = '/_actions/newsletter/subscribe'
+ const successResponse = {
+ success: true as const,
+ message: 'Please check your email to confirm your subscription.',
+ requiresConfirmation: true as const,
+ }
let stage: NewsletterSubscribeStage = 'buildRequestFingerprint'
let fingerprint: string | undefined
let consentFunctional = false
@@ -122,6 +128,10 @@ export const newsletter = {
throw new ActionsFunctionError(`Try again in ${retryAfterSeconds}s`, { status: 429 })
}
+ if (body.website_url) {
+ return successResponse
+ }
+
stage = 'validateEmail'
const validatedEmail = validateEmail(body.email)
@@ -175,11 +185,7 @@ export const newsletter = {
stage = 'sendConfirmationEmail'
await sendConfirmationEmail(validatedEmail, token, body.firstName)
- return {
- success: true,
- message: 'Please check your email to confirm your subscription.',
- requiresConfirmation: true,
- }
+ return successResponse
} catch (error) {
const errorContext = {
route,
diff --git a/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts b/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts
index 5eab3952..8ce25127 100644
--- a/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts
+++ b/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts
@@ -38,7 +38,7 @@ const defaultNewsletterProps: NewsletterProps = {
variant: 'article',
}
-const newsletterVariants: NewsletterProps['variant'][] = ['article', 'home']
+const newsletterVariants: NewsletterProps['variant'][] = ['article', 'home', 'page']
const getElements = (root: NewsletterFormElement) => {
const selectElement = (selector: string): T => {
@@ -55,6 +55,7 @@ const getElements = (root: NewsletterFormElement) => {
form: selectElement('#newsletter-form'),
emailLabel: selectElement('#newsletter-email-label'),
emailInput: selectElement('#newsletter-email'),
+ websiteUrlInput: selectElement('#newsletter-website_url'),
consentCheckbox: selectElement('#newsletter-gdpr-consent'),
submitButton: selectElement('#newsletter-submit'),
buttonText: selectElement('#button-text'),
@@ -113,6 +114,7 @@ describe.each(newsletterVariants)('NewsletterFormElement web component (%s)', va
expect(elements.description.id).toBe('newsletter-cta-' + variant + '-description')
expect(elements.form.id).toBe('newsletter-form')
expect(elements.emailInput.id).toBe('newsletter-email')
+ expect(elements.websiteUrlInput.name).toBe('website_url')
expect(elements.consentCheckbox.id).toBe('newsletter-gdpr-consent')
expect(elements.title.textContent).toContain(defaultNewsletterProps.title)
@@ -194,6 +196,27 @@ describe.each(newsletterVariants)('NewsletterFormElement web component (%s)', va
})
})
+ test('forwards the honeypot field when it is filled', async () => {
+ newsletterSubscribeMock.mockResolvedValueOnce({
+ data: { success: true, message: 'Please check your email to confirm your subscription.' },
+ })
+
+ await renderNewsletter(async ({ elements }) => {
+ elements.emailInput.value = 'test@example.com'
+ elements.websiteUrlInput.value = 'https://spam.example'
+ elements.consentCheckbox.checked = true
+
+ elements.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
+ await flushPromises()
+
+ expect(newsletterSubscribeMock).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ 'website_url': 'https://spam.example',
+ consentGiven: true,
+ })
+ })
+ })
+
test('handles API error responses gracefully', async () => {
newsletterSubscribeMock.mockResolvedValueOnce({
error: { message: 'Subscription failed' },
diff --git a/src/components/CallToAction/Newsletter/client/index.ts b/src/components/CallToAction/Newsletter/client/index.ts
index 62cdd088..c22e0425 100644
--- a/src/components/CallToAction/Newsletter/client/index.ts
+++ b/src/components/CallToAction/Newsletter/client/index.ts
@@ -227,6 +227,8 @@ export class NewsletterFormElement extends LitElement {
const email = this.emailInput.value.trim()
const formData = this.form ? new FormData(this.form) : null
const consentGiven = formData?.get('consent') === 'true'
+ const websiteUrlRaw = formData?.get('website_url')
+ const websiteUrl = typeof websiteUrlRaw === 'string' ? websiteUrlRaw.trim() : ''
const dataSubjectIdRaw = formData?.get('DataSubjectId')
const dataSubjectId = typeof dataSubjectIdRaw === 'string' ? dataSubjectIdRaw.trim() : ''
@@ -263,6 +265,7 @@ export class NewsletterFormElement extends LitElement {
try {
result = await actions.newsletter.subscribe({
email,
+ ...(websiteUrl ? { 'website_url': websiteUrl } : {}),
consentGiven,
...(DataSubjectId ? { DataSubjectId } : {}),
})
diff --git a/src/components/CallToAction/Newsletter/layouts/article.astro b/src/components/CallToAction/Newsletter/layouts/article.astro
index cb536a47..6a1c0a00 100644
--- a/src/components/CallToAction/Newsletter/layouts/article.astro
+++ b/src/components/CallToAction/Newsletter/layouts/article.astro
@@ -42,6 +42,21 @@ const {