26 | {dayjs(post.date).format("YYYY-MM-DD")} 27 |
28 |37 | You have successfully unsubscribed from the email notifications. 38 |
39 |Email: {email}
40 |41 | If you change your mind, you can re-subscribe at any time. 42 |
43 |{errorMessage}
47 |48 | Please ensure you used the correct unsubscribe link, or contact our 49 | support team for help. 50 |
51 |Thank you for subscribing to the newsletter. You will receive the latest updates and news.
77 |78 | If you wish to unsubscribe, please click here 79 |
80 | `, 81 | headers: { 82 | "List-Unsubscribe": `<${unsubscribeLink}>`, 83 | "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" 84 | } 85 | }); 86 | 87 | return { success: true }; 88 | } catch (error) { 89 | console.error('Newsletter subscription failed:', error); 90 | throw error; 91 | } 92 | } 93 | 94 | export async function unsubscribeFromNewsletter(token: string) { 95 | try { 96 | await checkRateLimit(); 97 | 98 | const email = Buffer.from(token, 'base64').toString(); 99 | const normalizedEmail = normalizeEmail(email); 100 | const { isValid, error } = validateEmail(normalizedEmail); 101 | 102 | if (!isValid) { 103 | throw new Error(error || 'Invalid email address'); 104 | } 105 | 106 | // Check if subscribed 107 | const list = await resend.contacts.list({ audienceId: AUDIENCE_ID }); 108 | const user = list.data?.data.find((item) => item.email === normalizedEmail); 109 | 110 | if (!user) { 111 | throw new Error('This email is not subscribed to our notifications'); 112 | } 113 | 114 | // Remove from audience 115 | await resend.contacts.remove({ 116 | audienceId: AUDIENCE_ID, 117 | email: normalizedEmail, 118 | }); 119 | 120 | return { success: true, email: normalizedEmail }; 121 | } catch (error) { 122 | console.error('Newsletter unsubscribe failed:', error); 123 | throw error; 124 | } 125 | } -------------------------------------------------------------------------------- /app/api/newsletter/route.ts: -------------------------------------------------------------------------------- 1 | import { subscribeToNewsletter } from '@/app/actions/newsletter'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function POST(request: Request) { 5 | try { 6 | const { email } = await request.json(); 7 | 8 | if (!email) { 9 | return NextResponse.json( 10 | { error: 'email is required' }, 11 | { status: 400 } 12 | ); 13 | } 14 | 15 | const result = await subscribeToNewsletter(email); 16 | return NextResponse.json(result); 17 | } catch (error) { 18 | const message = error instanceof Error ? error.message : 'Server processing request failed'; 19 | return NextResponse.json( 20 | { error: message }, 21 | { status: error instanceof Error ? 400 : 500 } 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from '@/config/site' 2 | import type { MetadataRoute } from 'next' 3 | 4 | const siteUrl = siteConfig.url 5 | 6 | export default function robots(): MetadataRoute.Robots { 7 | return { 8 | rules: { 9 | userAgent: '*', 10 | allow: '/', 11 | disallow: ['/private/', '/api/', '/_next/', '/assets/', '*/404', '*/500'], 12 | }, 13 | sitemap: `${siteUrl}/sitemap.xml`, 14 | host: siteUrl, 15 | } 16 | } -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from '@/config/site' 2 | import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing' 3 | import { getPosts } from '@/lib/getBlogs' 4 | import { MetadataRoute } from 'next' 5 | 6 | const siteUrl = siteConfig.url 7 | 8 | type ChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined 9 | 10 | export default async function sitemap(): Promise{t("description")}
63 | 91 |26 | {t("description")} 27 |
28 |47 | {website.name} 48 |
49 | 50 | ))} 51 |
82 | ),
83 | pre: (props) => (
84 |
88 | ),
89 | blockquote: (props) => (
90 |
94 | ),
95 | img: (props) => (
96 |