"Luc's Wedding Website πŸ’"

Overview

Big news... I created my own personal wedding website 😍! Here it is below, in all its glory:

Website - Home Page

It comes equipped with the following pages:

The RSVP Page πŸ’Œ

The RSVP page, in particular, is where most of the magic happens ✨. Website - Find Invite

This page queries an authenticated database in real-time, using guest information provided by the user.

You can search for an RSVP for any of the following guests below:

First NameLast Name
JohnDoe
JaneSmith
BarackObama
ChuckNorris
LucMarrie

Project Goals

Initially when I started this project, I wanted to continue exploring Next.js (a React-based framework for building UIs) and to get my hands dirty with some kind of database... even if for a very lightweight use case.

To date, all websites I had built thus far:

  1. Were fully static in nature (page content is already set in stone)
  2. Or, when dynamic data is required, relied on simple GET calls to previously-existing APIs

As such, I wanted to pick a simple website use case that would require me to create my own database.

So... Why a Wedding Website?

You may have noticed the title of my website is "TBD and Luc". That's because I'm not actually getting married... (nor do I have a gf/fiancΓ©e lol).

I'm still a single man, traversing life's winding roads in solitude. However, I fret not because surely my unwavering pursuit of web dev knowledge will lead me to true love.

Single man halfgif

Anyway, prior to the πŸ‚ fall of 2023, I had yet to attend my first wedding. When it rains, it pours I guess... because suddenly three back-to-back weddings popped up on my calendar 😨.

In terms of the wedding sites, they were created by the following service providers:

  1. Zola
  2. The Knot
  3. Wix

Given that I literally help companies make websites for a living, it was interesting to examines websites for a totally different use case from my line of work... and to inspect the visual layout and feature differences between each provider.

In terms of similarities, features included:

  • Several pages of static content (FAQs, photos, "how we met", etc.)
  • Usually a registry
  • In all cases, an RSVP page (where a user provides some sort of unique identifier, granting them read/write access to their RSVP info)

In any case, based on the described functionality, I decided a wedding website was a perfect use case for my project.

Chef's Kiss halfgif

Website Development

Alright, now for the meat and potatoes πŸ₯© πŸ₯”... how was this thing made?

Features

For an MVP, I decided to prioritize the following features:

FeatureDescriptionMVP
Static PagesPages with static content (e.g. home, wedding schedule, FAQs, etc.).βœ…
RSVP (Read)Let's user check on the status of their RSVP using first/last name.βœ…
RSVP (Write)Let's user submit/update their RSVP information.❌
RegistryRegistry page for that new Espresso machine I've had my eyes on. Doing this from scratch would require me to either build an e-commerce site essentially... so, I'm gonna pass for now. If I were to seriously pursue this, I'd probably use a URL 200 rewrite (or iframe) to a page hosted on Zola/The Knot.❌
Site Password ProtectionSecure site with basic password protection. I will revisit this in the near future.❌

Architecture

To support the above feature set, I loosely adhered to this tacky architecture diagram:

Website Architecture

Website Ingredients

Building the Static Pages

To kick things off, I scaffolded my project and created a blank canvas for each page using Next.js's quickstart flow, which now uses App Router by default (as opposed to Pages Router).

npx create-next-app@latest

Note that because I'm new to Next.js, everything described below is written exclusively with knowledge of app router only.

Pages

In Next.js, each folder is used to define a "route" (path at which a page is available in the browser). Creating a page file under each route is what makes the UI at that path publicly accessible. As such, I was able to create each page of my site using the directory structure below:

app β”œβ”€β”€ accomodations <-- defines URL β”‚ └── page.tsx <-- UI here is exposed at folder name β”œβ”€β”€ faqs β”‚ └── page.tsx β”œβ”€β”€ rsvp β”‚ └── page.tsx β”œβ”€β”€ tbd-and-luc β”‚ └── page.tsx β”œβ”€β”€ travel β”‚ └── page.tsx β”œβ”€β”€ wedding-weekend β”‚ └── page.tsx β”œβ”€β”€ globals.css β”œβ”€β”€ layout.tsx <-- automatically shared layout └── page.tsx <-- home page

Shared Layout

In Next.js projects, layout is a special file. As Next.js defines it:

A layout is UI that is shared between multiple pages. On navigation, layouts preserve state, remain interactive, and do not re-render.

I was excited to see this feature. Each page in my app shares the same structure... so there's no need to re-render a fresh layout each time a page loads.

When a layout is defined at the app/ root, it's known as the Root Layout. This is basically a "global" layout; Next.js applies it to all pages in the app automatically.

I tossed a Navbar and Footer component into that bad boy straight away:

import Navbar from './components/navbar' import Footer from './components/footer' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body className=""> <Navbar /> <-- πŸ˜‰ {children} <Footer /> <-- πŸ˜‰ </body> </html> ) }

Beyond that, the rest of my initial work was just plain old React development (components, styling, etc.).

The RSVP Page and Database

Next was the RSVP page. Here's how I decided it should work:

  1. A guest can enter their first/last name on the page
  2. If the guest exists in my database, return their RSVP information (i.e. the events they're currently RSVPed to)
  3. If the guest doesn't exist, display an error

Ladies and gentlemen... enter the database:

database normalgif

Picking a Database

For this segment of the project, I had two important questions to answer:

  1. What type of database should I use? πŸ€” (e.g. relational, NoSQL, key-value store, graph... etc.)
  2. And from which database provider? 😨

I've played around with Supabase before, which was a great experience. Unfortunately they "pause" your DBs after a rather short period of inactivity... so I skipped out on them for now. (I will probably use them for my next project though).

Before I got much further in my search, I came across Vercel's Storage product; front-and-center on their website:

Vercel offers a suite of managed, serverless storage products that integrate with your frontend framework.

Given I was already using Next.js and Vercel, it was compelling to see they also had a storage product. But wait, it gets better... the product page had an entire paragraph dedicated to CHOOSING a database type 😱:

Choosing the correct storage solution depends on your needs for latency, durability, and consistency, among many other considerations.

To help you choose, we've created a table below to summarize the benefits of each storage option in relation to each other:

ProductReadsWritesUse CaseLimits
KVFastMillisecondsKey/value and JSON dataLearn more
PostgresFastMillisecondsStructured, relational dataLearn more
BlobFastMillisecondsLarge, content-addressable files ("blobs")Learn more
Edge ConfigUltra-fastMillisecondsRuntime configuration (e.g., feature flags)Learn more

After a literal 5-second skim of the table, the obvious way to go was Postgres (thank you Vercel for your pristine documentation).

Creating the Database

First, I needed to create a database in the Vercel platform. To do so, I leveraged their (somewhat bare bones) UI for running queries:

Vercel Query - Create Tables

Because it's 2023, I asked ChatGPT to construct me a relational table schema for guests, events, and RSVPs πŸ₯΄. Based on its response, I created the following tables below:

CREATE TABLE guests ( id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255), email VARCHAR(255), num_of_guests INT, rsvp_message VARCHAR(255) ); CREATE TABLE events ( id SERIAL PRIMARY KEY, name VARCHAR(255), description TEXT, datetime TIMESTAMP WITH TIME ZONE, location VARCHAR(255) ); CREATE TABLE rsvps ( id SERIAL PRIMARY KEY, guest_id INT REFERENCES guests(id), event_id INT REFERENCES events(id), response BOOLEAN );

I inserted some guest, event, and rsvp records:

INSERT INTO guests (first_name, last_name, email, num_of_guests, rsvp_message) VALUES ('Luc', 'Marrie', 'luc@marrie.com', 2, 'I''m getting married?'), -- Guest 1 ('John', 'Doe', 'john.doe@example.com', 2, 'So excited!'), -- Guest 2 ('Jane', 'Smith', 'jane.smith@example.com', 2, 'Congrats!') -- Guest 3 ; INSERT INTO events (name, description, datetime, location) VALUES ('Welcome Dinner', null, '2030-09-20 17:00:00', null), -- Event 1 ('Wedding', null, '2030-09-21 16:30:00', null), -- Event 2 ('Farewell Brunch', null, '2030-09-22 10:00:00', null) -- Event 3 ; INSERT INTO rsvps (guest_id, event_id, response) VALUES (1, 1, true), -- Luc Marrie RSVPs yes to Welcome Dinner (1, 2, true), -- Luc Marrie RSVPs yes to Wedding (1, 3, true), -- Luc Marrie RSVPs yes to Farewell Brunch (2, 1, true), -- John Doe RSVPs yes to Welcome Dinner (2, 2, true), -- John Doe RSVPs yes to Wedding (2, 3, false), -- John Doe RSVPs no to Farewell Brunch (3, 1, false), -- Jane Smith RSVPs no to Welcome Dinner (3, 2, false), -- Jane Smith RSVPs yes to Wedding (3, 3, true) -- Jane Smith RSVPs yes to Farewell Brunch ;

VoilΓ  - we were ready to go.

Vercel Query - Select Guests

Local Development

Now that the tables were set up, I could hook up the database to the frontend.

I used Vercel's Postgres quickstart guide to get going. They weren't lying when they said "quick" start... considering how much of a noob I am, it was genuinely very easy for me to get started.

With a single command, I could fetch all necessary environment variables from Vercel and save them to a file called .env.development.local.:

vercel env pull .env.development.local

From there, I could instantly interact with my database using a combination of their SDK and serverless functions. Refer to the code block below, which shows a simple query against my guests table.

// app/api/find-invitation/route.ts import { sql } from '@vercel/postgres'; import { NextResponse } from 'next/server'; export async function GET(request: Request) { try { const result = await sql`SELECT * FROM guests;`; return NextResponse.json({ result }, { status: 200 }); } catch (error) { return NextResponse.json({ error }, { status: 500 }); } }

Feel free to examine my finished serverless function in GitHub. To summarize how it works:

  1. When a guest clicks "Find Your Invitation", my app calls the serverless function with first/last name as query params:

    const response = await fetch(`/api/find-invitation? firstName=${firstName}&lastName=${lastName}`);
  2. The function extracts the first/last name, and makes the following SQL query to find the guest id

    SELECT * FROM guests WHERE first_name ILIKE ${firstName} AND last_name ILIKE ${lastName} ;
  3. When a guest is found, the function queries the events table and joins it to the rsvps table based using the guest id

    SELECT events.id, events.name, events.description, events.datetime, events.location, rsvps.response FROM events LEFT JOIN rsvps ON events.id = rsvps.event_id AND rsvps.guest_id = ${guestId} ;

As such, the serverless function is able to return the following JSON back to my frontend:

{ "guest": { "id": 2, "first_name": "John", "last_name": "Doe", "number_of_guests": 2, "rsvp_message": "My wife and I are so excited to see you get married... feels like just yesterday I taught you how to write serverless functions" }, "rsvps": [ { "id": 1, "name": "Welcome Dinner", "description": null, "datetime": "2030-09-20T17:00:00.000Z", "location": null, "response": true }, { "id": 2, "name": "Wedding", "description": null, "datetime": "2030-09-21T16:30:00.000Z", "location": null, "response": true }, { "id": 3, "name": "Farewell Brunch", "description": null, "datetime": "2030-09-22T10:00:00.000Z", "location": null, "response": true } ] }

Which can then be easily leveraged in my TSX component, like so:

John Doe RSVP

Production Deployment

When I created my Vercel Postgres database earlier, I had to connect it to a Vercel project (aka site).

Upon connection, Vercel automatically ensured all necessary Postgres environment variables were made available to website's production, preview, and dev environments:

Vercel Database Project

From there, my deploy completed without a mishap βœ….

Closing Thoughts

I was extremely impressed with Vercel's developer experience... it genuinely was so easy getting a database stood up and hooked up to my application within minutes.

The only things I'm left wondering about are...

  1. Is there an easy way to protect a site behind a password using Next.js and Vercel?

    I see that Vercel offers password protection as part of their Pro Plan... ($150 per month). I think I'll pass on that.

  2. Is it kind of odd that I'm using SQL directly via my serverless function, instead of some sort of API?

    I suppose, given the size of my site, I'm not aiming for blazing fast speed... However, I can see why this makes Supabase a compelling choice for develompent, as they auto-generate APIs for you right OOTB.

  3. Registries...

    How the heck is that gonna work? I wonder if I could just create a registry using Zola or The Knot... and then hook that up to my site manually. If anyone knows, please shoot me a DM. Or, leave a comment... once I make it possible to leave comments on my blog posts πŸ˜‰

Thank you very much for reading, and I hope you enjoy the wedding website.

Peace normalgif