Main tools
- Deco.cx for hosting and CMS.
- Deco Records as an SQLite database, using
Drizzle ORM
- Resend app for sending confirmation emails. Email sending API that offers a
free plan of 3,000 emails per month.
Necessary configurations
- Create a site on deco.cx
- Configure Deco Records
For this tutorial, we created a “newsletter” table with columns id
, email
,
confirmed_at
, confirmation_key
.
- Install the Resend app in the “Apps” menu, configuring the API Key. When
installing the Resend app on deco, you will find instructions on how to do
this.
Creating the sections used
Two sections were used in this tutorial:
-
newsletterSubscribe.tsx
: Newsletter subscription form (email field) +
inline action that performs the
process of including the email in the database and sending the email using
the Resend app. Main components of the section:
export async function action(
props: Props,
req: Request,
ctx: AppContext & RecordsApp & ResendApp,
): Promise<Props> {
const form = await req.formData(); // Gets the data from the database
const email = `${form.get("email") ?? ""}`;
if (!email) {
console.log("Email is empty");
return { ...props, submissionResponse: { email: "" } };
}
const drizzle = await ctx.invoke("records/loaders/drizzle.ts"); // Loads drizzle to interact with the database
try {
const recs = await drizzle // Checks if the email is already registered in the database
.select({ email: newsletter.email })
.from(newsletter)
.where(eq(newsletter.email, email));
if (recs.length) {
return {
...props,
submissionResponse: { error: "Email already exists.", email },
};
}
const confirmationKey = crypto.randomUUID(); // Generates a unique confirmation key for verification
await drizzle.insert(newsletter).values({ // Inserts the new newsletter record into the database
email,
confirmed_at: null,
confirmation_Key: confirmationKey,
});
await ctx.invoke("resend/actions/emails/send.ts", {
subject: `Personal Blog - Confirm your subscription`,
from: no-reply@blog.owner
html: `<h1>Thanks for subscribing!</h1><br/><br/>Click <a href="https://sites-blog.decocdn.com/confirm-newsletter?key=${confirmationKey}">here</a> to confirm your subscription.`,
to: email,
});
return { ...props, submissionResponse: { email: "" } };
} catch (e) {
console.log(e);
ctx.monitoring?.logger?.error(e);
return {
...props, submissionResponse: { error: "System error", email },
};
}
}
export function loader(props: Props) {
return props;
}
And in the form, it contains the use of HTMX to make the request
asynchronously and update only the necessary section.
<form
class="form-control"
hx-post={useComponent(import.meta.url, props)} // URL using the useComponent hook where the request will be sent
hx-target="closest section" // Target element that will be updated with the server response, the idea here is to call the same component to avoid changing the entire page
hx-swap="outerHTML" // Mode of replacing the target content
>
<input
type="email"
value={submissionResponse?.email}
placeholder="Email address"
class="input input-bordered"
name="email"
required
/>
<button class="btn btn-primary" type="submit">
<span class="inline [.htmx-request_&]:hidden">{buttonText}</span>
<span class="hidden [.htmx-request_&]:inline loading loading-spinner" />
{" "}
{/*spinner used while HTMX receives the request*/}
</button>
</form>;
-
newsletterConfirmation.tsx
: Section to be included on the /confirm
page
of the site so that users can confirm the subscription and receive positive
feedback. The main use of this section was the use of the loader to process
the data:
export const loader = async (
props: Props,
req: Request,
ctx: AppContext & RecordsApp,
) => {
const url = new URL(req.url);
const reallyQs = url.searchParams.get("really");
if (!reallyQs) {
return props;
}
const confirmationKey = url.searchParams.get("key");
if (!confirmationKey) {
return { ...props, error: "No confirmation key." };
}
const drizzle = await ctx.invoke("records/loaders/drizzle.ts");
await drizzle
.update(newsletter)
.set({
confirmed_at: new Date().toISOString(),
confirmation_key: null,
})
.where(eq(newsletter.confirmation_key, confirmationKey ?? ""));
return { ...props, really: true };
};
Conclusion
This tutorial demonstrates how to implement a newsletter subscription system
using deco.cx tools, Deco Records, and Resend. The presented solution offers a
complete flow, from capturing the user’s email to confirming the subscription,
ensuring data integrity and user experience. The use of modern technologies such
as HTMX and Drizzle ORM provides an efficient and easy-to-maintain
implementation. This system can be easily adapted and expanded to meet the
specific needs of different web projects.