Create a newsletter site for free in 30 minutes

Jul 01 2024

JD

James Dawson

By the end of this article you should have something that looks like this and is a fully functioning newsletter for free!

News letter example

Initial Setup

We need to create a new project.

pnpm dlx nuxi@latest init <project-name>

We will also add typescript.

pnpm add -D typescript

and finally we will add tailwind.

pnpm dlx nuxi@latest module add @nuxtjs/tailwindcss

We now want to add the shadcn nuxt module.

pnpm dlx nuxi@latest module add shadcn-nuxt

Then we open our nuxt.config.ts file. And replace it with the following.

export default defineNuxtConfig({
  modules: ["@nuxtjs/tailwindcss", "shadcn-nuxt"],
  shadcn: {
    /**
     * Prefix for all the imported component
     */
    prefix: "",
    /**
     * Directory that the component lives in.
     * @default "./components/ui"
     */
    componentDir: "./components/ui",
  },
});

Run the setup CLI.

pnpm dlx shadcn-vue@latest init

And finally just follow the default steps. Make sure you select your framework as Nuxt

Would you like to use TypeScript (recommended)?  yes
Which framework are you using? Nuxt
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your tsconfig.json or jsconfig.json file? › ./tsconfig.json
Where is your global CSS file? › › src/index.css
Do you want to use CSS variables for colors? › no / yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utils
Write configuration to components.json. Proceed? > Y/n

That is all the setup done! All we need to do now is add the input and button components.

pnpm dlx npx shadcn-vue@latest add button input

Build a basic app

Amazing, now let’s make ourselves a quick landing page. Replace the following code in your app.vue file.

<template>
  <div class="dark space-x-2 max-w-sm mx-auto my-auto mt-20">
    <p class="text-center">Hey 👋</p>
    <h1 class="text-7xl font-extrabold text-center mb-4">Join my newsletter</h1>
    <p class="text-center mb-8">We talk about <i>amazing things</i>, delivered to your inbox <b>every Saturday</b>.</p>
    <div class="flex gap-2 ">


    <Input type="email" placeholder="Your email" />
    <Button>Join newsletter</Button>
    </div>
 </div>
</template>

This should all do the job! We now need an endpoint to send the signup request to, along with the users actual email address.

Backend with Resend

Now we can implement Resends logic. First we need create an account with Resend and get your API key.

Once you have your API key. You should create a .env file at the root of your directory and add the API key here.

RESEND_API_KEY=you_key_goes_here

Now we need to create an endpoint that will handle adding the email address to our resend audience and sending them a thankyou email.

Create a new file in server/api/newsletter.post.ts and add the following code.

import { Resend } from "resend";

export default defineEventHandler(async (event: any) => {
  const resend = new Resend(process.env.RESNED_API_KEY);
  const { email } = await readBody(event);

  try {
    const { error } = await resend.contacts.create({
      email,
      unsubscribed: false,
      audienceId: YOUR_AUDIENCE_ID,
    });

    if (error) {
      return {
        statusCode: 400,
        body: error.message,
      };
    }

    await resend.emails.send({
      from: "onboarding@resend.dev",
      to: email,
      subject: "Hello",
      html: "<p>Thank you for joining my newsletter</p>",
    });

    return {
      statusCode: 200,
      body: "You have been added to the newsletter",
    };
  } catch (err) {
    return {
      statusCode: 500,
      body: "Internal Server Error",
    };
  }
});

Finally all that is left to do is to hook the frontend up to this api call

const handleSubmitNewsletter = async (email: string) => {
  const response = await $fetch("/api/newsletter", {
    method: "POST",
    body: { email },
  });

  if (response.statusCode === 200) {
    toast(response.body);
  } else {
    toast("Error", {
      description: response.body || "An error occurred",
    });
  }
};

Now just add this function to your current frontend

<script lang="ts" setup>
const email = ref<string | null>(null);
const handleSubmitNewsletter = async (email: string) => {
  const response = await $fetch("/api/newsletter", {
    method: "POST",
    body: { email },
  });

  if (response.statusCode === 200) {
    toast(response.body);
  } else {
    toast("Error", {
      description: response.body || "An error occurred",
    });
  }
};
</script>

<template>
  <div class="space-x-2 max-w-md mx-auto my-auto mt-20 border border-border px-6 py-12  rounded-lg">
    <p class="pl-2">Hey 👋</p>
    <h1 class="text-3xl font-extrabold  mb-2">Join my newsletter!</h1>
    <p class=" mb-16">We talk about <i>amazing things</i>, delivered to your inbox <b>every Saturday</b>.</p>
    <div class="flex gap-2 ">


    <Input type="email" placeholder="Your email" v-model="email"/>
    <Button @click="handleSubmitNewsletter(email)">Join newsletter</Button>
    </div>
 </div>
</template>

And we have a working newsletter!

Become a better builder

Join our newsletter for frontend tip and tricks.

New blog posts every Sunday.