Article cover image

Building on Today's Web: An Interplay of Technologies

  • Claudio
    Claudio
    Director of Engineering

Each tool in the developer's kit serves a specific purpose, and their amalgamation leads to a dynamic, performant, and engaging web application. This guide will delve deeper into the interplay of these digital gears, showcasing their individual strengths and how they contribute to the grand machinery of web development.

The Cornerstones: Frontend Frameworks

As a cornerstone of modern frontend development, Next.js is built on the foundation of React but goes steps further by addressing some of the challenges faced in real-world applications. It introduces concepts like server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR), which can significantly enhance performance and user experience.

In Next.js, getStaticProps is a function you can export from a page component file to generate the props for your component at build time. This allows you to render your page based on data fetched at build time, effectively pre-rendering the page into static HTML.

Here's an example of how you might use getStaticProps:

export async function getStaticProps(context) {
  const res = await fetch(`https://api.example.com/data`);
  const data = await res.json();
  return {
    props: { data }, // will be passed to the page component as props
  };
}

In this example, data is fetched at build time and provided to your page via props. This page will now be statically generated and served as HTML, enhancing its load speed and cache-ability significantly.

Further enhancing SSG, Next.js also offers the ability to define a revalidate property in the returned object from getStaticProps. The revalidate property takes a number of seconds as the value and will attempt to regenerate the page at most once every specified number of seconds. This is useful for content that updates regularly but doesn't need to be updated for each request.

export async function getStaticProps(context) {
  const res = await fetch(`https://api.example.com/data`);
  const data = await res.json();

  return {
    props: { data }, // will be passed to the page component as props
    revalidate: 60, // In seconds
  };
}

In this example, Next.js will attempt to regenerate the page every minute if there's a request. If a request to get data becomes rate-limited during regeneration, the server will return the cached page while it waits for the rate limits to reset. This is part of Next.js's "stale-while-revalidate" strategy, which ensures that users always get a response, even if the page couldn't be regenerated at the time of the request.

In this scenario, users might see slightly out-of-date information (stale data) until the rate limits reset and the data can be fetched successfully to update the static page (revalidate). This way, even when facing rate limitations or other temporary issues with fetching data, your application can still serve content and provide a smooth user experience.

It's worth noting that in situations where data fetching may frequently encounter rate limits, it would be prudent to consider adjustments. These might include increasing the revalidate interval to reduce the frequency of data fetches, or working with the API provider to negotiate higher rate limits. Alternatively, you might consider implementing a backoff strategy or using a caching layer to store and retrieve data more efficiently.

As for SvelteKit, it's a wonderful frontend framework that we've chosen to build this very site! SvelteKit takes a unique approach compared to traditional frameworks like React or Vue. Instead of using a virtual DOM, Svelte compiles your code into efficient imperative code that directly manipulates the DOM. The result is a leaner, faster website with quicker initial load times, smoother updates, and more efficient memory usage. The static site generation capabilities of SvelteKit work harmoniously with our use of Prismic as a headless CMS, allowing us to deliver a fast, reliable, and engaging user experience. More on Prismic later.

A typical SvelteKit application might have files that define routes, represented by +page.svelte files in the file system. A dynamic route, for example, fetching a blog post based on an id, would look like this:

<script>
  export let post;
</script>

<h1>{post.title}</h1>
<p>{post.content}</p>
// src/routes/posts/[slug]/+page.js
export async function load({ fetch, page }) {
  const { slug } = page.params;
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const data = await res.json();

  return {
    props: {
      post: data
    }
  };
}

Just like Next.js, SvelteKit also supports dynamic routes and server routes, adding to its versatility. It provides a simplified developer experience, with a focus on reducing the amount of boilerplate code, which in turn accelerates the development process.

In the above snippet, SvelteKit's load function fetches data at the server-side before the component is rendered, offering capabilities for SEO-friendly dynamic routes right out of the box. As with other parts of full-stack applications, the fetched data can be typesafe if TypeScript is adopted, which SvelteKit supports natively. We'll talk more about TypeScript in a bit. The efficient, straightforward, and feature-rich nature of SvelteKit makes it a formidable tool in the modern web development toolkit.

The Launchpad: ▲ Vercel

Vercel is a cloud platform for static sites and Serverless Functions that fits perfectly with our Next.js or SvelteKit applications. It's like the launchpad for your website, propelling it into the world wide web. Vercel is built with a "develop, preview, and ship" philosophy. It allows you to preview your changes with deployment previews and makes collaboration easier with deploy summaries.

To deploy a Next.js application, it's as simple as pushing your code to a Git provider (like GitHub), importing the project into Vercel, and Vercel will handle the rest. Every push triggers a new deployment, and every Pull Request gives a unique deployment URL to preview the changes.

At Monogram, it considerably streamlines our development process and allows us to focus on building the best possible product by taking care of the rest: hosting, scaling, and performance.

The Storyteller: Headless CMS

In the realm of content, a Headless CMS is a game-changer. It decouples content management from content presentation, allowing for more flexibility and scalability. This means you can manage your content in one place and deliver it across different platforms using APIs.

At Monogram, we are committed to delivering top-tier solutions that empower our clients to convey their unique stories effectively. That's why we've chosen Prismic as our go-to headless Content Management System (CMS).

Prismic's flexibility and power align perfectly with our desire for creative freedom and effective content management. Its intuitive interface and custom type builder enable us to shape and structure content exactly how we envisage it, maintaining our creative integrity while providing our clients with a tailored content strategy.

One feature that has particularly won our hearts is the content versioning and real-time preview in Prismic. It's like having a time machine and a crystal ball in one package. We can revisit any previous versions of our content, and see exactly how the changes will reflect on our website before hitting the publish button.

Here's a sample code snippet that demonstrates how we might fetch content from Prismic in a typical project:

// ./src/routes/portfolio/+page.js
import Prismic from '@prismicio/client'

export async function load({ fetch }) {
  const apiEndpoint = 'https://monogram.cdn.prismic.io/api/v2'
  const client = Prismic.client(apiEndpoint)

  const response = await client.query(
    Prismic.Predicates.at('document.type', 'portfolio')
  )

  return {
    props: {
      portfolio: response.results,
    },
  }
}
// ./src/routes/portfolio/+page.svelte
<script>
  export let portfolio;
</script>

{#each portfolio as item (item.id)}
  <article>
    <h1>{item.data.title}</h1>
    <div>
      {@html item.data.content}
    </div>
  </article>
{/each}

In this SvelteKit snippet, the load function fetches portfolio entries from our Prismic repository at the server-side before the component is rendered. Then, we can opt-in to static generation by adding a line to the +page.js file:

// ./src/routes/portfolio/+page.js
import Prismic from '@prismicio/client'

export async function load({ fetch }) {
  const apiEndpoint = 'https://monogram.cdn.prismic.io/api/v2'
  const client = Prismic.client(apiEndpoint)

  const response = await client.query(
    Prismic.Predicates.at('document.type', 'portfolio')
  )

  return {
    props: {
      portfolio: response.results,
    },
  }
}

// 👇 Opt into static generation
export const prerender = true;

This exported const signals to SvelteKit to statically generate (prerender) this page, giving us the benefits of static site generation: SvelteKit outputs generated, stateless HTML pages that we can store globally on a content delivery network, as close as possible to users, and serve them immediately. This is a major boon for user experience.

The fetched portfolio entries are then passed as props to our SvelteKit component, where they are displayed within an article element. As with other parts of your full-stack application, the fetched data can be type-safe if TypeScript is adopted, which SvelteKit supports natively.

We firmly believe that the tools we use shape the work we produce. With Prismic, we have a tool that not only shapes our work but enhances it, giving us the capabilities to create dynamic, engaging, and personalized content for each of our clients. We're not just using Prismic; we're thriving with it, and we're incredibly proud to have it as part of our creative arsenal.

The Stylist: Tailwind CSS

Tailwind CSS is like the stylist of your website. It's a utility-first CSS framework that gives you the building blocks to design your website directly in your HTML markup. This approach leads to less CSS bloat, more style reuse, and faster development times.

Suppose you want to create a responsive card with Tailwind CSS. Here's how you could do it:

<div
  class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl"
>
  <div class="md:flex">
    <div class="md:flex-shrink-0">
      <img
        class="h-48 w-full object-cover md:w-48"
        src="/img/store.jpg"
        alt="Man looking at item at a store"
      />
    </div>
    <div class="p-8">
      <div
        class="uppercase tracking-wide text-sm text-indigo-500 font-semibold"
      >
        Case study
      </div>
      <a
        href="#"
        class="block mt-1 text-lg leading-tight font-medium text-black hover:underline"
        >Finding customers for your new business</a
      >
      <p class="mt-2 text-gray-500">
        Getting a new business off the ground is a lot of hard work. Here are
        five ideas you can use to find your first customers.
      </p>
    </div>
  </div>
</div>

While Tailwind CSS can be a powerful tool, it also has some tradeoffs to consider. Let's explore these.

Tailwind CSS' very feature that makes it powerful—utility classes—can also lead to verbosity in your markup. Class strings can get quite long, which can impact the readability of your code. Furthermore, adopting Tailwind CSS requires a paradigm shift from traditional CSS methodologies, which might pose a learning curve, especially for developers who are deeply rooted in traditional CSS. Lastly, since styles are applied using utility classes, your HTML can lose its semantic meaning. A div laden with a string of utility classes might leave one wondering about its purpose in the grand scheme of the design.

On the other end of the spectrum is traditional CSS. It's been there since the inception of web design and has evolved over the years, especially with the advent of preprocessors like Sass or Less. Traditional CSS shines when it comes to writing semantic stylesheets. The class names can clearly indicate their purpose, making the associated HTML more readable and understandable. It encourages DRY (Don't Repeat Yourself) principles, allowing you to create reusable classes and components. Traditional CSS, coupled with preprocessors, also offers dynamic styling capabilities. Crafting styles based on states, media queries, or animations can be more intuitive and straightforward.

That said, traditional CSS also has its set of challenges (which is why Tailwind even exists in the first place!). The infamous global namespace issue can lead to naming conflicts and unexpected style inheritance, which can be hard to debug. Developers often find themselves switching between different files (HTML and CSS), which can interrupt the flow and slow down the development process. Moreover, as the project grows, maintaining design consistency can become challenging without a strict system in place.

At the end of the day, both Tailwind CSS and traditional CSS represent two different paths towards the same goal: creating beautiful, user-friendly websites. Tailwind CSS, with its utility-first approach, can enhance developer efficiency and design customization but may come with verbosity and a steep learning curve. Traditional CSS, with its focus on semantics and DRY principles, offers intuitive dynamic styling but can be bogged down by the global namespace issue and frequent context switching. The choice between the two really depends on things like team dynamics, timelines, and project requirements.

The Overseer: TypeScript

TypeScript serves as the overseer in the application development process, providing a safety layer on top of JavaScript. TypeScript introduces static type-checking along with several other features like interfaces, enums, and tuples, which help in writing robust and bug-free code.

Consider a scenario where we want to define a function that takes an object with a specific shape. Here's how TypeScript can ensure the correct usage:

interface User {
  name: string;
  age: number;
}

function greet(user: User) {
  return `Hello, ${user.name}!`;
}

const user: User = {
  name: "Alice",
  age: 30,
};

console.log(greet(user)); // Hello, Alice!

In this example, TypeScript ensures that the greet function always receives an object that adheres to the User interface. This way, we can catch potential bugs at compile-time rather than run-time.

At Monogram, we're dedicated to creating robust, maintainable, and efficient digital solutions, so we realize the importance of catching bugs early, improving developer tooling, and enhancing code readability. TypeScript helps us achieve all of these and more.

Our team leverages TypeScript's static typing feature to catch potential bugs before they creep into production. This means fewer crashes and a smoother user experience. The types in TypeScript serve as in-code documentation, making our code self-explanatory and easier to understand, a boon when collaborating on large projects or when onboarding new developers.

TypeScript also plays nice with modern editors like VS Code, providing powerful features like autocompletion, type inference, and method signature information. This considerably speeds up our development process and makes coding a more enjoyable experience. It is not just a tool for us; it's an integral part of our development workflow. It has helped us reduce bugs, improve code maintainability, and enhance our development experience. We're proud to use TypeScript and are excited about the confidence and productivity it brings to our team.

The Grand Symphony

The technologies mentioned above are like the individual instruments in an orchestra, each playing its unique tune. However, when they come together under the baton of a skilled developer, they create a grand symphony that we call a "web experience."

Your Next.js or SvelteKit components, styled by Tailwind CSS, fetch data from a Headless CMS. TypeScript ensures type safety across your application, making the development process smooth and error-free. Finally, Vercel takes your application, built with precision and finesse, and launches it into the digital space, making it accessible to users worldwide.

The following code snippet encapsulates this grand symphony, demonstrating how these technologies can work together in a real-world scenario:

// pages/index.tsx in a Next.js project
Import { GetStaticProps } from “next”;

type Props = {
  posts: Post[]
}

export const getStaticProps: GetStaticProps<Props> = async () => {
  const res = await fetch("https://api.your-headless-cms.com/posts");
  const posts = await res.json();

  return {
    props: { posts },
  };
}

export default function Home({ posts }: Props) {
  return (
    <section className="p-4 max-w-sm mx-auto rounded shadow flex items-center">
      {posts.map((post) => (
        <article key={post.id}>
          <h2 className="text-xl font-medium text-black">{post.title}</h2>
          <p className="text-gray-500">{post.content}</p>
        </article>
      ))}
    </section>
  );
}

In this Next.js example, we fetch posts from a Headless CMS at build time using the getStaticProps function. These posts are then passed as props to our component, where they are displayed within a styled container provided by Tailwind CSS. The TypeScript integration ensures type safety throughout this process. Once written, this code can be committed to a Git repository and deployed via Vercel, where it becomes a living, breathing website.

The complexity of building a website can indeed be daunting. But, with a thorough understanding of these tools and how they work together, it becomes less of an intimidating challenge and more of an exciting journey. We hope you're as excited as we are about the possibilities that these technologies bring to the table, and are able to enjoy this journey as much as we do.