Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vercel
119 changes: 119 additions & 0 deletions packages/docs/api/mailerlite-subscribe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
export default async function handler(req, res) {
const MAILERLITE_API_KEY = process.env.MAILERLITE_API_KEY
const MAILERLITE_GROUP_ID = process.env.MAILERLITE_GROUP_ID || process.env.NEXT_PUBLIC_MAILERLITE_WEBFORM_ID

// Enable CORS
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
)

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, message: 'Method not allowed' })
}

if (!MAILERLITE_API_KEY) {
console.error('MailerLite API key not configured')
return res.status(500).json({
success: false,
message: 'Subscription service is not configured properly'
})
}

try {
const body = req.body

if (!body.email) {
return res.status(400).json({
success: false,
message: 'Email is required'
})
}

// Build the MailerLite API payload
const payload = {
email: body.email,
status: 'active',
fields: body.fields || {}
}

if (body.name) {
payload.fields.name = body.name
}

if (body.resubscribe) {
payload.resubscribe = true
}

// Add to groups if specified
if (body.groups && body.groups.length > 0) {
payload.groups = body.groups
} else if (MAILERLITE_GROUP_ID) {
// Use default group if no specific groups provided
payload.groups = [MAILERLITE_GROUP_ID]
}

// Make request to MailerLite API
const response = await fetch('https://api.mailerlite.com/api/v2/subscribers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-MailerLite-ApiKey': MAILERLITE_API_KEY
},
body: JSON.stringify(payload)
})

const data = await response.json()

if (!response.ok) {
// Check if subscriber already exists (200/201 are success, 400 might be duplicate)
if (response.status === 400 && data.message?.includes('already exists')) {
// Try to update existing subscriber
const updateResponse = await fetch(`https://api.mailerlite.com/api/v2/subscribers/${body.email}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-MailerLite-ApiKey': MAILERLITE_API_KEY
Comment on lines +75 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] URL-encode email before calling MailerLite update endpoint

When handling a duplicate subscriber, the code constructs the update URL with https://api.mailerlite.com/api/v2/subscribers/${body.email}. If the email contains characters that are not legal in a path segment (e.g. +, spaces, non‑ASCII), the request will be interpreted incorrectly by the MailerLite API and the update attempt will fail. This means users who sign up with common aliasing patterns like user+promo@example.com will never get the graceful "already subscribed" path and will continue receiving an error. Wrap the email with encodeURIComponent before interpolating it into the URL.

Useful? React with 👍 / 👎.

},
body: JSON.stringify(payload)
})

if (updateResponse.ok) {
const updateData = await updateResponse.json()
return res.status(200).json({
success: true,
message: 'Successfully updated your subscription',
alreadySubscribed: true,
data: updateData
})
}
}

console.error('MailerLite API error:', data)
return res.status(response.status).json({
success: false,
message: data.message || 'Failed to subscribe. Please try again.'
})
}

return res.status(200).json({
success: true,
message: 'Successfully subscribed!',
alreadySubscribed: false,
data: data
})
} catch (error) {
console.error('Subscription error:', error)
return res.status(500).json({
success: false,
message: 'An error occurred while processing your subscription'
})
}
}
5 changes: 3 additions & 2 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.5.1",
"js-yaml": "^4.1.0",
"dotenv": "^16.6.1",
"js-yaml": "^4.1.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -33,6 +33,7 @@
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/tsconfig": "3.8.1",
"@docusaurus/types": "3.8.1",
"@vercel/node": "^5.3.24",
"typescript": "~5.5.4"
},
"engines": {
Expand All @@ -44,7 +45,7 @@
"build": "pnpm api-docs && docusaurus build && node scripts/fix-sitemap.js",
"clear": "docusaurus clear",
"deploy": "docusaurus deploy",
"dev": "docusaurus start --listen 4242",
"dev": "docusaurus start --port 4242",
"docs": "docusaurus start --port 4242",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
Expand Down
6 changes: 4 additions & 2 deletions packages/docs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"extends": "./node_modules/@docusaurus/tsconfig/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
}
},
"include": ["src", "docs"],
"exclude": ["node_modules", "build", "api"]
}
Loading
Loading