Skip to content

Commit 1fe46d6

Browse files
authored
Merge pull request #39 from thedevdavid/feature/newsletter
Feature/newsletter
2 parents f08617f + dd7b7fb commit 1fe46d6

File tree

17 files changed

+3446
-2479
lines changed

17 files changed

+3446
-2479
lines changed

.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
#Next
22
NEXT_PUBLIC_BASE_URL=https://localhost:3000
3+
4+
#Analytics
35
NEXT_PUBLIC_UMAMI_SCRIPT_URL=
4-
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
6+
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
7+
8+
#Email
9+
EMAIL_API_BASE=
10+
NEXT_PUBLIC_EMAIL_API_KEY=
11+
NEXT_PUBLIC_EMAIL_GROUP_ID=

.husky/pre-commit

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env sh
22
. "$(dirname -- "$0")/_/husky.sh"
33

4-
pnpm lint
5-
pnpm format
4+
pnpm lint-staged

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Share post component
1515
- Lint staged files
1616
- Tag generation and routing
17+
- Newsletter feature
18+
- MailerLite integration
1719

1820
### Changed
1921

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ If you love this template and/or use it, please give it a star on GitHub. This w
2424
- [Colors](#colors)
2525
- [Metadata](#metadata)
2626
- [Analytics](#analytics)
27+
- [Vercel](#vercel)
28+
- [Umami](#umami)
29+
- [Other analytics providers](#other-analytics-providers)
2730
- [Newsletter subscription](#newsletter-subscription)
31+
- [MailerLite](#mailerlite)
32+
- [Other newsletter providers](#other-newsletter-providers)
2833
- [Hero section](#hero-section)
2934
- [Other tips & tricks](#other-tips--tricks)
3035
- [Image optimization](#image-optimization)
@@ -116,13 +121,22 @@ Umami is a simple, easy to use, web analytics solution with self-hosting option!
116121
Configure:
117122
Set `NEXT_PUBLIC_UMAMI_SCRIPT_URL` & `NEXT_PUBLIC_UMAMI_WEBSITE_ID` environment variables on your `.env.local` file and on Vercel dashboard.
118123

119-
#### Others
124+
#### Other analytics providers
120125

121126
Supporting other analytics providers are in progress. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.
122127

123128
### Newsletter subscription
124129

125-
_WIP_ as I'm still deciding which email tools to support. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.
130+
#### MailerLite
131+
132+
MailerLite is a simple email marketing tool for all types of businesses. You can read more about it on [MailerLite website](https://www.mailerlite.com/).
133+
134+
Configure:
135+
Set `EMAIL_API_BASE`, `EMAIL_API_KEY`, and `EMAIL_GROUP_ID` environment variables on your `.env.local` file and on Vercel dashboard.
136+
137+
#### Other newsletter providers
138+
139+
Supporting other newsletter providers are in progress. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.
126140

127141
### Hero section
128142

@@ -175,11 +189,14 @@ Note: DO NOT overdo it. You can easily make images look bad with lossy compressi
175189
- [x] Social icons component
176190
- [x] Social sharing buttons
177191
- [x] Tags
178-
- [ ] newsletter integration (form, api route, keys, welcome page, previous issues)
192+
- [x] newsletter integration (form, api route, keys, thank you/welcome page, MailerLite provider)
193+
- [ ] Other newsletter providers (Convertkit, Substack, Buttondown, Mailchimp, etc)
179194
- [ ] Other analytics providers (fathom, simplelytics, plausible, etc)
195+
- [ ] CLI and/or recipes
180196
- [ ] Post series page
181197
- [ ] prev/next post links
182198
- [ ] related posts
199+
- [ ] Newsletter previous issues page
183200
- [ ] Layouts/templates system
184201
- [ ] Notion data source
185202
- [ ] Sanity data source

app/(site)/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { ArrowRight } from "lucide-react";
77

88
import { defaultAuthor } from "@/lib/metadata";
99
import { cn } from "@/lib/utils";
10-
import CTA from "@/components/cta";
1110
import { HeroImage } from "@/components/hero-image";
1211
import { HeroMinimal } from "@/components/hero-minimal";
1312
import { HeroSimple } from "@/components/hero-simple";
1413
import { HeroVideo } from "@/components/hero-video";
1514
import { Sidebar } from "@/components/home-sidebar";
1615
import { Mdx } from "@/components/mdx-components";
16+
import NewsletterSubscribe from "@/components/newsletter-subscribe";
1717
import PostPreview from "@/components/post-preview";
1818

1919
async function getAboutPage() {
@@ -61,7 +61,7 @@ export default async function Home() {
6161
</aside>
6262
</div>
6363
</div>
64-
<CTA
64+
<NewsletterSubscribe
6565
title="I also write deep dives in email"
6666
description="I write about coding, design, digital nomad life, and solopreneurship. Join over 1,000 other developers in
6767
getting better in business. Unsubscribe whenever."

app/(site)/thank-you/page.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
5+
import { Button } from "@/components/ui/button";
6+
7+
export default function ThankYou() {
8+
const router = useRouter();
9+
10+
return (
11+
<div className="mb-16 items-center justify-center text-center">
12+
<span className="bg-gradient-to-b from-foreground to-transparent bg-clip-text text-[10rem] font-extrabold leading-none text-transparent">
13+
Thank You!
14+
</span>
15+
<h2 className="my-2 font-heading text-2xl font-bold">Welcome to the newsletter</h2>
16+
<p>You&apos;re now receiving.</p>
17+
<div className="mt-8 flex justify-center gap-2">
18+
<Button onClick={() => router.back()} variant="default" size="lg">
19+
Go back
20+
</Button>
21+
<Button onClick={() => router.push("/")} variant="ghost" size="lg">
22+
Back to Home
23+
</Button>
24+
</div>
25+
</div>
26+
);
27+
}

app/(social)/social/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Mail } from "lucide-react";
44
import { defaultAuthor } from "@/lib/metadata";
55
import { projects } from "@/lib/projects-data";
66
import { CopyButton } from "@/components/copy-button";
7-
import CTA from "@/components/cta";
7+
import NewsletterSubscribe from "@/components/newsletter-subscribe";
88
import { Signature } from "@/components/signature";
99
import { SocialButton } from "@/components/social-button";
1010
import { SpotlightCard } from "@/components/spotlight-card";
@@ -39,7 +39,7 @@ export default async function SocialPage() {
3939
<Signature />
4040
</div>
4141
</div>
42-
<CTA
42+
<NewsletterSubscribe
4343
title="I also write deep dives in email"
4444
description="I write about coding, design, digital nomad life, and solopreneurship. Join over 1,000 other developers in
4545
getting better in business. Unsubscribe whenever."

app/newsletter/route.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextResponse } from "next/server";
2+
3+
const API_URL = `${process.env.EMAIL_API_BASE}api/subscribers`;
4+
5+
export async function POST(request: Request) {
6+
const { email } = await request.json();
7+
8+
if (!email) {
9+
return NextResponse.json({ error: "Email is required" }, { status: 400 });
10+
}
11+
try {
12+
const res = await fetch(API_URL, {
13+
method: "POST",
14+
headers: {
15+
"Content-Type": "application/json",
16+
Accept: "application/json",
17+
Authorization: `Bearer ${process.env.EMAIL_API_KEY || "You didn't set the API key"}`,
18+
},
19+
body: JSON.stringify({ email, groups: [process.env.EMAIL_GROUP_ID || ""] }),
20+
});
21+
22+
if (!res.ok) {
23+
throw new Error(res.statusText);
24+
}
25+
return NextResponse.json({ ok: "ok" }, { status: 200 });
26+
} catch (error: any) {
27+
console.log(error);
28+
return NextResponse.json({ error: error.message || error.toString() }, { status: 500 });
29+
}
30+
}

app/sitemap.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { MetadataRoute } from "next";
22
import { allPages, allPosts } from "@/.contentlayer/generated";
33

4+
import { tagOptions } from "@/lib/content-definitions/post";
45
import { BASE_URL } from "@/lib/metadata";
56

67
export default function sitemap(): MetadataRoute.Sitemap {
7-
const posts = allPosts
8-
.filter((post) => post.status === "published")
9-
.map((post) => ({
10-
url: `${BASE_URL}/posts/${post.slug}`,
11-
lastModified: post.lastUpdatedDate,
12-
}));
8+
const now = new Date();
9+
const loadedPosts = allPosts.filter((post) => post.status === "published");
10+
const tags = tagOptions.map((tag) => ({
11+
url: `${BASE_URL}/tags/${tag}`,
12+
lastModified: now,
13+
}));
14+
const posts = loadedPosts.map((post) => ({
15+
url: `${BASE_URL}/posts/${post.slug}`,
16+
lastModified: post.lastUpdatedDate,
17+
}));
1318
const pages = allPages
1419
.filter((page) => page.status === "published")
1520
.map((page) => ({
@@ -19,29 +24,30 @@ export default function sitemap(): MetadataRoute.Sitemap {
1924
return [
2025
{
2126
url: BASE_URL,
22-
lastModified: new Date(),
23-
},
24-
{
25-
url: `${BASE_URL}/about`,
26-
lastModified: new Date(),
27+
lastModified: now,
2728
},
2829
{
2930
url: `${BASE_URL}/projects`,
30-
lastModified: new Date(),
31+
lastModified: now,
3132
},
3233
{
3334
url: `${BASE_URL}/uses`,
34-
lastModified: new Date(),
35+
lastModified: now,
3536
},
3637
{
3738
url: `${BASE_URL}/social`,
38-
lastModified: new Date(),
39+
lastModified: now,
3940
},
4041
...pages,
4142
{
4243
url: `${BASE_URL}/posts`,
43-
lastModified: new Date(),
44+
lastModified: now,
4445
},
4546
...posts,
47+
{
48+
url: `${BASE_URL}/tags`,
49+
lastModified: now,
50+
},
51+
...tags,
4652
];
4753
}

components/mdx-components.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Code } from "bright";
44
import { useMDXComponent } from "next-contentlayer/hooks";
55

66
import { fileIcons } from "@/lib/bright-config";
7-
import CTA from "@/components/cta";
7+
import NewsletterSubscribe from "@/components/newsletter-subscribe";
88

99
function YouTubeVideo({ id }: { id: string }) {
1010
return (
@@ -44,7 +44,7 @@ Code.lineNumbers = true;
4444

4545
const components = {
4646
Image,
47-
CTA,
47+
NewsletterSubscribe,
4848
YouTubeVideo,
4949
pre: Code,
5050
// a: CustomLink,

0 commit comments

Comments
 (0)