Building My First Svelte Project:
A Wedding Invitation Website

Main of Post1

See the prototype here

A Special Gift for My Best Friend

I was told that my best friend Ella, who currently lives in South Korea, is getting married. We have been friends since we were seven years old. I would love to attend her wedding but the fact that I can’t be there for her due to the distance and my own schedule makes me sad. Instead of attending in person, I decided to give her a special gift as her best friend: a wedding invitation card and a website!

It is quite common to share wedding invitations both through a paper invitation card and a website. Many couples prefer this approach as it allows them to combine tradition with convenience. While paper invitations maintain a classic and elegant touch, digital invitations provide easy access and instant updates for guests.

Additionally, digital invitations are convenient for guests, as it is common to give money as a wedding gift to celebrate the couple. This makes it easier to include important details such as the bank account number and the account holder’s name.

With these things in mind, I felt confident that this was a project I could handle as a beginner since it only required simple features. However, I was shocked to find that these companies charge a significant amount of money despite the simplicity of their designs and website features.

Step 1. Creating the Paper Invitation

Descriptive pic of the card invitation

© Hyewon Im

I first designed the paper invitation card using Canva. To keep it elegant yet minimal, I utilized free open-source illustrations and carefully selected fonts that matched the wedding theme. The design process involved experimenting with different layouts, colors, and typography to achieve a warm and inviting feel.

Step 2. Website Mockup Design

Descriptive pic of mockup design

© Hyewon Im

Before creating the mockup design, I discussed the necessary features with Ella and finalized the sitemap. Since the website didn’t require many built-in features or multiple pages, I decided to go with a single-page scrollable layout containing the following sections:

  • Hero section
  • Introduction (photo + short invitation message)
  • Gallery
  • Wedding date & countdown
  • Venue details & directions (incl. map)
  • Bank account information (incl. buttons which can copy bank account)
  • Footer (incl. a copy link function)

Given that the website would be accessed on both smartphones and desktop browsers, I prioritized responsiveness to ensure a seamless experience across different screen sizes. Taking all these factors into account, I proceeded with the mockup design using Figma.

Step 3. Turning the Mockup into a Real Website

To begin the development process, I set up the project using Svelte and configured the environment with pnpm as the package manager. Since Svelte is lightweight and optimized for performance, it was a great choice for this single-page wedding invitation website. I chose pnpm over npm or yarn because it is faster, more efficient in disk space usage, and provides better dependency management. Unlike npm, pnpm uses a global store for packages, preventing unnecessary duplication and speeding up installation, making it an ideal choice for managing dependencies in this project.

Key Features I Built

1. Photo gallery

I implemented an interactive gallery feature, allowing users to click on thumbnails to view larger images. This was achieved by dynamically updating the main image's source when a thumbnail is clicked.


    <script lang="ts">
        let currentImageIndex = 0;

        function changeImage(index: number) <
                currentImageIndex = index;
            >
    </script>

    <main>
        /.../
        <div class="container">
            <div class="main-image">
                <img src=<images[currentImageIndex]> alt="Showing Images on Thumbnail" loading="lazy"/>
            </div>

            <div class="thumbnails">
                <#each images as image, index>
                  <button class="thumbnail" on:click=<() => < currentImageIndex = index;>>>
                      <img src=<image.replace('.png', '-button.png')> alt=<`Thumbnail $<index + 1>`> loading="lazy"/>
                  </button>
                </each>
            </div>
    </main>
          

During development, I encountered an issue with image quality and file size. The original PNG images were too large, causing slow loading times, especially on mobile devices. Additionally, reducing the file size manually often led to a noticeable loss in image quality, making it difficult to balance performance and visual clarity. To address this, I researched different image optimization techniques to find a solution that maintains quality while improving performance.

Image resolution refers to the number of pixels in an image—higher resolution means more detail, while lower resolution means results in smaller file sizes but less clarity.

Image compression works in two different ways:

  • Lossless compression retains all original data but takes longer to decompress on the client side.
  • Lossy compression reduces file size by omitting some details, making images more efficient for web usage.

Image formats differ in quality, compression, and use cases.

  • PNGis high-quality and lossless but has larger file sizes.
  • JPEG uses lossy compression to balance quality and sizes.
  • WEBP offers better compression with both lossless and lossy options for optimal performance.

Given that most of the images I used were high-quality and large in size, I decided to convert them to WEBP format for better performance and efficiency. Instead of storing the images locally, I uploaded them to Cloudflare R2 storage and used their URLs for direct access.

descriptive about r2 stroage usage

To retrieve the URLs of the image files, the first step is to connect a custom domain to Cloudflare R2. This allows seamless access to stored assets via a friendly and consistent URL structure. Once the domain is linked, images can be referenced directly using public URLs.

2. Calendar & countdown

I implemented a dynamic calendar and countdown feature to highlight the wedding date and show the remaining days. The countdown updates dynamically, allowing users to see how many days are left until the wedding.


    <script lang="ts">
        const year = 2025;
        const month = 2; // 0 = Jan, (...), 2 = March
        
        const daysInMonth = new Date(year, month + 1, 0).getDate(); // Total dates in a specific month
        const firstDayIndex = new Date(year, month, 1).getDay(); // First day in a specific month (Sunday = 0)
        
        let dates = [];
        
        for (let i = 0; i < firstDayIndex; i++) <
                    dates.push('');
                    >
        for (let i = 1; i <= daysInMonth; i++) <
                    dates.push(i);
                    >
    </script>
    <main>
        <div class="calendar">
                  /.../
            <div class="calendar-dates">
                <#each dates as date>
                   <div class="date <date === 22 ? 'highlight' : ''>"><date></div>
                </each>
            </div>
        </div>
      </main>
        

3. Map API Integration

Since many people in Korea prefer Naver Maps over Google Maps, I integrated the Naver Maps API instead. This ensures a more familiar and convenient experience for local guests, allowing them to easily find the wedding venue using Korea's widely used mapping service.

4. Bank Account Display

I integrated a Dropdown Panel to let users access the account information only when needed. I also implemented a copy-to-clipboard function, allowing users to easily copy the bank name and account number with a single click.

5. AOS Library Integration

I incorporated the AOS (Animate On Scroll) library to enhance visual transitions and improve user engagement. This allows elements to appear dynamically as users scroll through the page, creating a more immersive and visually appealing interface.


    <script lang="ts">
            import { onMount } from 'svelte';
            import AOS from 'aos';
            import 'aos/dist/aos.css';

            onMount(() => {
              AOS.init({
                duration: 1000,
                once: false
              });
            });
    </script>
      

I encountered an issue where animations wouldn't work correctly unless the library was initialized properly. To resolve this, I ensured that AOS.init() was executed inside onMount, preventing errors and ensuring the animations loaded correctly after the component was rendered.

Step 4. Hosting & Deployment

I implemented a Docker-based deployment setup with DevContainer for local development and Cloudflare for hosting.

  • Docker is used for universal, containerized code that runs the same everywhere, regardless of the environment.
  • DevContainer ensures everyone working on the project has the same development setup.
  • Cloudflare provides fast, scalable hosting with built-in support for both static and dynamic functionality.

By utilizing this setup, I successfully deployed my project with full flexibility. 🎉

Reflection

This was my first project built from scratch and my first attempt at using a framework. Although it was just a single-page website and not overly complex, I still feel a sense of achievement knowing that I built it myself. I learned a lot throughout the process and realized how immersive and passionate I am about creating something on my own. This experience gave me a deeper understanding of web development and reinforced my excitement for building projects independently. 🚀

© Copyright 2025 Hyewon Im. All Rights Reserved.