This guide is for programming newbies who want to create a simple SSR (Server Side Rendering) website (e.g. https://redditrecs.com ) with Nuxt 3 & CloudFlare.

I wrote this because I was once that programming newbie, and I was tearing my hair out because:

  • Existing tutorials and documentations were overly complicated (for what I wanted to build)
  • AI coding assistants suck at Nuxt 3

Hope this helps save someone’s hair!

Why do you need SSR? For me it was mainly for SEO on dynamic content .

What you will build in this example walkthrough

By the end of the tutorial, you will have a live website that looks like this: https://nuxt3-ssr-starter.pages.dev/

The website is ugly as hell but that’s not the point.

The point is to learn the basics through building, so you can build more complex website with the same underlying principles.

The website is actually a simplified version of https://redditrecs.com .

The shape of the website is that the content (list of portable monitors and their details) is generated from a data source via api, and then rendered on the server side.

You can also find the github repo for that website here: https://github.com/lowjootat/nuxt3-ssr-starter

1. Create project & install Nuxt

Follow the instructions here: https://nuxt.com/docs/getting-started/installation

At the end of it, you should be able to start your Nuxt app in development mode and view it in the browser with the following command:

npm run dev -- -o

2. Setup page template

Before we add the pages, lets set up the template that will apply to all pages.

Open the file app.vue. You’ll see some placeholder code.

How Nuxt works is that everything within <template> in app.vue will appear on every page. So this is the place for global stuff like header and footer.

Delete the placeholder code and add header, main, and footer. Then add <NuxtPage /> inside main.

// app.vue
<template>
  <header style="background-color: #333; color: #fff; padding: 10px;">
    Header
  </header>
  <main style="margin: 0 auto; max-width: 800px; padding: 20px;">
    <NuxtPage />
  </main>
  <footer style="background-color: #333; color: #fff; padding: 10px;">
    Footer
  </footer>
</template>

<NuxtPage /> is a special component that acts as a placeholder for the individual pages which you’ll add next.

3. Add home page

Create a pages folder in your project root if it doesnt’ exist. This is where you’ll add your pages.

The file structure in the pages folder determines the URL structure of your application. For example:

  • pages/index.vue –> home page (/)
  • pages/about.vue –> about page (/about)
  • pages/portablemonitors/index.vue –> monitors list page (/portablemonitors)

You can also use square brackets in the filename for dynamic routes, e.g:

  • pages/portablemonitors/monitor/[id] –>
    • monitor 1 detail page (/portablemonitors/monitor/1)
    • monitor 2 detail page (/portablemonitors/monitor/2)
    • etc

You’ll see this in action later. For now, let’s start with the easy one.

To create the home page, create index.vue in pages:

// pages/index.vue
<template>
  <h1>Unbiased product recommendations from Reddit</h1>
  <p>Select a category:</p>
  <NuxtLink to="/portablemonitors">Portable Monitors</NuxtLink>
</template>
<script setup>
</script>

<NuxtLink> is a special component in Nuxt 3 for optimized navigation between pages in your app.

You can Google for all the benefits of using it over <a>. But basically if you’re doing any linking within your app, it’s recommended to use NuxtLink.

In this case, it is linking to /portablemonitors page which will be the list of portable monitors.

3. Add portable monitors list page

It’s not created yet so let’s go ahead and create a new folder portablemonitors inside pages, and then create an index.vue inside it:

// pages/portablemonitors/index.vue -->
<template>
  <h1>Portable Monitors</h1>
  <p>Click to view more details</p>
</template>

In this page, I want to list all the monitors from my data source under “Click to view more details”.

Fetching the data

How do I fetch the data? With useFetch()

After the template, add the script section:

// pages/portablemonitors/index.vue
<template>
  <h1>Portable Monitors</h1>
  <p>Click to view more details</p>
</template>

<script setup> // add script section
const { data: monitors } = await useFetch('/api/getMonitorData')
</script>

For each page, the <script setup> section is where you define your variables, functions, and logic etc.

Here, we use useFetch to fetch data from endpoint /api/getMonitorData (not created yet, will create later), and then assign it to the variable monitors after waiting for the data.

You can read more about useFetch in the docs: https://nuxt.com/docs/api/composables/use-fetch

TLDR is that useFetch is recommended for most situations.

AI coding assistants will suggest useAsyncData and/or $fetch, I think because they aren’t familiar with useFetch yet. Ignore them.

Preparing the endpoint to serve the data

To create the endpoint ‘/api/getMonitorData’ that will serve our portable monitor data:

  1. Create a new folder server in your project root
  2. Create a new folder api inside server
  3. Create a new file getMonitorData.js inside api

The file name becomes part of the API route, and now you can call this api at /api/getMonitorData

In my case, I want to return a simple JSON string containing my monitor data:

// serve/api/getMonitorData.js
export default defineEventHandler(() => {
  return [
    {
      "id": 2,
      "title": "Lenovo L15 Portable Monitor, 15.6\u201d Display, Full HD Resolution, IPS Panel, 250 nits Brightness, 60Hz Refresh Rate, USB-C Ports, Height-Adjustable Stand, Flicker-Free Technology, Grey",
      "url": "https://www.amazon.com/dp/B0B5GRGCX5?tag=jooilibeans-20&linkCode=ogi&th=1&psc=1",
      "brand": "Lenovo",
      "img_url": "https://m.media-amazon.com/images/I/41cnpBwwG8L._SL500_.jpg",
      "price": 215.65
    },
    {
      "id": 3,
      "title": "UPERFECT Portable Monitor 15.6 inch QLED Q1 1080P USB C Second Monitor Mini HDMI Travel Monitor 100% DCI-P3 10 Bit 500 Nits w/Smart Cover & Speakers, External Monitor for Laptop PC Phone PS4",
      "url": "https://www.amazon.com/dp/B08CVQ5SD9?tag=jooilibeans-20&linkCode=ogi&th=1&psc=1",
      "brand": "UPERFECT",
      "img_url": "https://m.media-amazon.com/images/I/51i1tOCpWoL._SL500_.jpg",
      "price": 149.99,
    }
  ];
});

Some notes:

  • Storing data in a json string is a bit unorthodox, but I am doing it this way for now to keep things super simple
  • In case you’re thinking of storing data in a sqlite3 db file in the directory, and then querying it with the API - I tried that and it didn’t work. It works in dev but I couldn’t find an easy way for it to work in production in Cloudflare.
  • Because I am returning a simple string, I didn’t need to make this async. But if you are querying a hosted database, you’ll need to make this async.

Displaying the data

Now that our endpoint is ready to serve data, we can go back to the monitor list page pages/portablemonitors/index.vue to edit the HTML template to display the data.

Here, we use v-for to iterate over the monitors array which now contains the data fetched from the api.

// pages/portablemonitors/index.vue
<template>
  <h1>Portable Monitors</h1>
  <p>Click to view more details</p>
  // iterate over monitors array and generate data
  <div v-for="monitor in monitors" :key="monitor.id">
      <div class="monitor">
        <h2>{{ monitor.title }}</h2>
        <p>${{ monitor.price }}</p>
      </div>
    </NuxtLink>
  </div>
</template>

<script setup>
const { data: monitors } = await useFetch('/api/getMonitorData')
</script>

// Styling for the monitor cards
<style scoped>
.monitor {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

You can run npm run dev -- -o in the terminal to see if the data is displaying correctly.

4. Add monitor detail pages

Now I want to link each monitor here to their respective monitor detail pages.

Wrap each monitor in NuxtLink and point it to a dynamic route based on id.

// pages/portablemonitors/index.vue
<template>
  <h1>Portable Monitors</h1>
  <p>Click to view more details</p>
  // Wrap monitor in NutxLink
  <div v-for="monitor in monitors" :key="monitor.id">
    <NuxtLink :to="`/portablemonitors/monitor/${monitor.id}`">
      <div class="monitor">
        <h2>{{ monitor.title }}</h2>
        <p>${{ monitor.price }}</p>
      </div>
    </NuxtLink>
  </div>
</template>

<script setup>
const { data: monitors } = await useFetch('/api/getMonitorData')
</script>

<style scoped>
.monitor {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

Create dynamic page

Create the dynamic page: portablemonitors/moniotr/[id].vue.

// pages/portablemonitors/monitor/[id].vue
<template>
  <h1>Monitor Details</h1>
  <h2>{{ monitor.title }}</h2>
  <img :src="monitor.img_url" alt="Monitor Image">
  <p>{{ monitor.brand }}</p>
  <p>{{ monitor.price }}</p>
  <p>{{ monitor.url }}</p>
</template>

<script setup>
const route = useRoute();
const { data: monitor } = await useFetch(`/api/getMonitorById/${route.params.id}`);
</script>

Nuxt 3 will recognize the dynamic route from the [id] in the file name.

Here’s how the page will display specific monitor details based on the route:

When there is a request for /portablemonitors/monitor/[id],

  1. We can extract id with useRoute
  2. Use that to call a dynamic API route (to be created) to fetch only the monitor with that id

All that’s missing now is to create the API with dynamic route.

Create API with dynamic route

  1. Create new folder getMoniotrById in server/api
  2. Inside it, create new file [id.js]
// server/api/getMonitorById/[id]
export default defineEventHandler(async (event) => {
  const data = await $fetch('/api/getMonitorData');
  const { id } = event.context.params;
  const monitorData = data.find(monitor => monitor.id === parseInt(id));
  const monitor = {
    id: monitorData.id,
    title: monitorData.title,
    url: monitorData.url,
    brand: monitorData.brand,
    img_url: monitorData.img_url,
    price: monitorData.price,
  };
  return monitor;
})

With this, when endpoint /api/getMonitorById/[id] is called:

  1. We fetch all the monitor data using await $fetch('/api/getMonitorData');
  2. Access the route via event and
  3. Extract id from event.context.params
  4. Use that id to find the specific monitor from the monitor data

Some notes:

  • I’m using $fetch() here because useFetch() only works in pages & composables, not in server-side API routes like here
  • I use async here so that I can wait for the $fetch to complete
  • event.context.params is a string, so I have to convert it to an Int to match my data

Voila! The whole site should be working in dev now.

Deploying on Cloudflare Pages

Cloudflare Pages integrates with Github, so this part is pretty easy.

Once you’ve hooked things up, pushing to your Github repo will automatically build your project and deploy it.

Everything is free, that’s why I use them.

Create a Github repo (if you haven’t) and push to main brach

Login to Github and click “New” in the repositories panel, or go to https://github.com/new

Enter a repository name and click “Create repository”.

Follow the instructions on Github to connect and push your project to the new repository

Create a Cloudflare Page

Sign up for a free Cloudflare account if you don’t already have one.

On the side panel, click “Workers & Pages” and click “Create”

On the next page, switch to the “Pages” tab and click “Connect to Git” button

Cloudflare create pages page

Select your repository on the next page.

On the next page, under “Build Settings”, select Nuxt.js in the “Framework preset” dropdown.

Cloudflare build settings

Click the “Save and Deploy” button.

Voila! Your SSR site is live.

Disable Javascript to verify that the content loads on the server side.