Overview
Big news... I created my own personal wedding website π! Here it is below, in all its glory:
It comes equipped with the following pages:
- Home
- Wedding Weekend (weekend itinerary)
- TBD and Luc (the story of how my non-existent wife and I met)
- FAQs
- Travel (travel guidance)
- Accomodations
- RSVP
The RSVP Page π
The RSVP page, in particular, is where most of the magic happens β¨.
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 Name | Last Name |
---|---|
John | Doe |
Jane | Smith |
Barack | Obama |
Chuck | Norris |
Luc | Marrie |
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:
- Were fully static in nature (page content is already set in stone)
- 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.
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:
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.
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:
Feature | Description | MVP |
---|---|---|
Static Pages | Pages 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. | β |
Registry | Registry 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 Protection | Secure 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 Ingredients
- Next.js (Front-end)
- Vercel (Hosting + serverless functions)
- Vercel Postgres (Backend database for all my lovely guests and their RSVPs)
- TailwindCSS (Styling)
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:
- A guest can enter their first/last name on the page
- If the guest exists in my database, return their RSVP information (i.e. the events they're currently RSVPed to)
- If the guest doesn't exist, display an error
Ladies and gentlemen... enter the database:
Picking a Database
For this segment of the project, I had two important questions to answer:
- What type of database should I use? π€ (e.g. relational, NoSQL, key-value store, graph... etc.)
- 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.
- Vercel KV: Durable Redis
- Vercel Postgres: Serverless SQL
- Vercel Blob: Large file storage
- Vercel Edge Config: Global, low-latency data store
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:
Product | Reads | Writes | Use Case | Limits |
---|---|---|---|---|
KV | Fast | Milliseconds | Key/value and JSON data | Learn more |
Postgres | Fast | Milliseconds | Structured, relational data | Learn more |
Blob | Fast | Milliseconds | Large, content-addressable files ("blobs") | Learn more |
Edge Config | Ultra-fast | Milliseconds | Runtime 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:
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.
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:
-
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}`);
-
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} ;
-
When a guest is found, the function queries the
events
table and joins it to thersvps
table based using the guestid
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:
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:
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...
-
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.
-
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.
-
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.