Blog Main

Svelte, Strapi, Nginx (3)

Nils

16.03.2021 20:00:00

Fetching Data from the backend and prerendering a blog page.

Last post we talked about how to use sapper to create a website with Svelte. We discussed the general Routing and the way a svelte pate is set up. We also made a quick turn to talk about the difference in hosting your website and how sapper supports both. Now it's time to get into more detail and offer an example of how a Blog page could be looking.

TLDR

In this post, we will see how a website showing a blog post could be created in sapper and svelte and how we can fetch data from a server. After the website is built we talk about how to make it ready for deployment to a v-server or a static hoster depending on choice and preference.

Getting Ready

In the last example I explained the prerequisites of getting started with sapper. I also explained how we can create our first project with some dummy data. In here I will use this data, modify it and explain what it does. If you want to follow along with this tutorial you should create your own project, if not done so already.

If you already have the prerequisites installed and just forgot the command its:

npx degit "sveltejs/sapper-template#rollup" testProject

after our project is created we have to first run npm install in the new test project folder and afterward

npm run dev

then we can head over to localhost:3000 to use the dummy version of a blog created by sapper.

The current version of the blog post can then be seen under localhost:3000/blog/how-to-use-sapper . After this, we are done and ready to start coding.

_posts.js (aka setting up dummy data)

We will keep all the defaults in regards to the header and the index blog page for now and focus on the blog post in this tutorial. In fact, we are going to use files that are already set to offer the data.

The first file we will look at is at testProject/src/routes/blog/_posts.js.

// Ordinarily, you'd generate this data from markdown files in your
// repo, or fetch them from a database of some kind. But in order to
// avoid unnecessary dependencies in the starter template, and in the
// service of obviousness, we're just going to leave it here.

// This file is called `_posts.js` rather than `posts.js`, because
// we don't want to create an `/blog/posts` route — the leading
// underscore tells Sapper not to do that.

const posts = [
    {
        title: 'What is Sapper?',
        slug: 'what-is-sapper',
        html: `
            <p>First, you have to know what <a href='https://svelte.dev'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.dev/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>

            <p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>

            <ul>
                <li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
                <li>Server-side rendering (SSR) with client-side hydration</li>
                <li>Service worker for offline support, and all the PWA bells and whistles</li>
                <li>The nicest development experience you've ever had, or your money back</li>
            </ul>

            <p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
        `
    },

    {
        title: 'How to use Sapper',
        slug: 'how-to-use-sapper',
        html: `
            <h2>Step one</h2>
            <p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>

            <pre><code>npx degit "sveltejs/sapper-template#rollup" my-app
            cd my-app
            npm install # or yarn!
            npm run dev
            </code></pre>

            <h2>Step two</h2>
            <p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>src/routes</code> directory or add new ones.</p>

            <h2>Step three</h2>
            <p>...</p>

            <h2>Step four</h2>
            <p>Resist overdone joke formats.</p>
        `
    },

    {
        title: 'Why the name?',
        slug: 'why-the-name',
        html: `
            <p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>

            <p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
        `
    },

    {
        title: 'How is Sapper different from Next.js?',
        slug: 'how-is-sapper-different-from-next',
        html: `
            <p><a href='https://github.com/zeit/next.js'>Next.js</a> is a React framework from <a href='https://vercel.com/'>Vercel</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>

            <ul>
                <li>It's powered by <a href='https://svelte.dev'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
                <li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>src/routes/blog/[slug].svelte</code></li>
                <li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
                <li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
            </ul>
        `
    },

    {
        title: 'How can I get involved?',
        slug: 'how-can-i-get-involved',
        html: `
            <p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://svelte.dev/chat'>Discord chatroom</a>. Everyone is welcome, especially you!</p>
        `
    }
];

posts.forEach(post => {
    post.html = post.html.replace(/^\t{3}/gm, '');
});

export default posts;

Later when we learn how to add Strapi to this we will get our initial data differently but for now this will be the file that we get our data from.

For our example, we won't just stop with a title and some HTML but also add a subHeader, and a thumbnail picture to it. Especially the image is going to be important in a later post when we will talk about Nginx and strapi. For now, we can just keep it here.

One Object in the array might then look like this:

    {
        title: 'How can I get involved?',
        subHeader: 'Detailed information how to get involved',
        slug: 'how-can-i-get-involved',
        html: `
            <p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://svelte.dev/chat'>Discord chatroom</a>. Everyone is welcome, especially you!</p>
        `,
        pictureSrc: 'https://images.pexels.com/photos/169573/pexels-photo-169573.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260'
    }

The picture is from server-sidePexels which is alongside Unsplash a great source for free commercially usable images.

Later when we talk about Strapi the images are going to be hosted alongside all the other data on our own backend but since this is going to be a later topic we will use external images for now.

[slug].json.js

Now that our dummy data is ready its time to do the server side fetching of data. In the last post I talked about dynamic pages vs static pages. To enable the advantages of a dynamic page the fetching to the data is done on the server. Since on our case the data won't change very often this enables things like caching of the data because the prerendered html is going to be send to the client. This also allows to only send the necessary data to the client or do the database call only once data are changed.

Usually, those database calls are what make the scalability of websites a challenge. It's easy to just run another instance of the frontend but it's a challenge to duplicate a database. As stated before sapper even allows to statically pretender a website to host the frontend on a static hoster which is usually cheaper. For this, the fetching must be done separately to the direct frontend.

The file testProject/src/routes/blog/[slug].json.js allowes this. The naming is intentionally set like this. It tells svelte that the name itself can be a wildcard. it can be replaced by any value which is then used to do a lookup for the data of the post.

When a user does a fetch call to blog/${slug}.json this file looks up if there are data and returns them, or returns an error message.

The initial version looks like this:

import posts from './_posts.js';

const lookup = new Map();
posts.forEach(post => {
    lookup.set(post.slug, JSON.stringify(post));
});

export function get(req, res, next) {
    // the `slug` parameter is available because
    // this file is called [slug].json.js
    const { slug } = req.params;

    if (lookup.has(slug)) {
        res.writeHead(200, {
            'Content-Type': 'application/json'
        });

        res.end(lookup.get(slug));
    } else {
        res.writeHead(404, {
            'Content-Type': 'application/json'
        });

        res.end(JSON.stringify({
            message: `Not found`
        }));
    }
}

The first part of the function is immediately called when the sapper backend is started. it loads the posts defined in the object set above and creates a Map for easily looking up posts later when the data are loaded. For now, we will still get our data from the _posts.js file, but we will treat this as if the data come from a server instead of just a file, which forces us to use asynchronous code instead of synchronous code currently used.

The Asynchronous Request looks something like this

import posts from './_
posts.js';

let fetchedLast = 0; // MODIFIED To ensure refetcing of the data every 5 minutes
const lookup = new Map();

//MODIFIED Update the new data instead of just doing it once
function updateLookupData(posts){
    posts.forEach(post => {
        lookup.set(post.slug, JSON.stringify(post));
    });
}

//Fetch the data
async function fetchData(){
    let result = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(posts);
        }, 3000)
    });

    updateLookupData( await result)
    return lookup;
}

//MODIFIED Either refetch the data or use saved data
async function getData(){
    if ( ( Date.now() - fetchedLast ) >= 1000*60*5 ){
        let result = await fetchData();
        fetchedLast = Date.now();
        return result;
    }
    return lookup;
}

//MODIFIED to be async
export async function get(req, res, next) {
    // the `slug` parameter is available because
    // this file is called [slug].json.js
    const { slug } = req.params;

    //MODIFIED Get up to date data
    let lookup = await getData();

    if (lookup.has(slug)) {
        res.writeHead(200, {
            'Content-Type': 'application/json'
        });

        res.end(lookup.get(slug));
    } else {
        res.writeHead(404, {
            'Content-Type': 'application/json'
        });

        res.end(JSON.stringify({
            message: `Not found`
        }));
    }
}

I added comments with the word MODIFIED to show the contents that have been updated.

The first thing we did was move the setting of the lookup data in its own function updateLookupData. Once the data are fetched this updates the table to offer the new values.

Next, we created a function fetch data that fetches the data from a server. In our case, this is only simulated since we get the data from the client we just run a setTimeout Function which returns the result after 3 seconds. This function later has to be updated to fetch the actual data from the backend (in our case from Strapi). We will do this in a later post.

Lastly, the function getData returns either fetches the data again from the server if the data have not been fetched for at least 5 minutes or just returns the current table if it has been fetched. Instead of relying on the Map() from a variable, we take the map via this function in the get request to ensure that the data are always up to date.

The get function has to be asynchronous to ensure that we are now waiting for the fetch request before we give out the result.

[slug].svelte

The last File that we need for our blog post is the [slug].svelte file. This is the actual frontend that is going to display what the user sees. This file itself is never going to be seen by the User. It is merely to be compiled into actual HTML, CSS, and javascript by the svelte compiler.

Before we go to the content let's again talk about the name of this file. Like in the case above the name [slug] part of the name is merely used to stand as a wildcard. When loading the website like http://localhost:3000/blog/slug the slug part is merely to tell the compiler what blog data should be displayed.

Again for those who won't code along the file looks like this:

<script context="module">
    export async function preload({ params }) {
        // the `slug` parameter is available because
        // this file is called [slug].svelte
        const res = await this.fetch(`blog/${params.slug}.json`);
        const data = await res.json();

        if (res.status === 200) {
            return { post: data };
        } else {
            this.error(res.status, data.message);
        }
    }
</script>

<script>
    export let post;
</script>

<style>
    /*
        By default, CSS is locally scoped to the component,
        and any unused styles are dead-code-eliminated.
        In this page, Svelte can't know which elements are
        going to appear inside the {{{post.html}}} block,
        so we have to use the :global(...) modifier to target
        all elements inside .content
    */
    .content :global(h2) {
        font-size: 1.4em;
        font-weight: 500;
    }

    .content :global(pre) {
        background-color: #f9f9f9;
        box-shadow: inset 1px 1px 5px rgba(0, 0, 0, 0.05);
        padding: 0.5em;
        border-radius: 2px;
        overflow-x: auto;
    }

    .content :global(pre) :global(code) {
        background-color: transparent;
        padding: 0;
    }

    .content :global(ul) {
        line-height: 1.5;
    }

    .content :global(li) {
        margin: 0 0 0.5em 0;
    }

    .headerImage{
        max-width: 100%;
    }

</style>

<svelte:head>
    <title>{post.title}</title>
</svelte:head>

<h1>{post.title}</h1>

<div class="content">
    {@html post.html}
</div>

In this file there are several things too look at.

For once there is not one but two script tags. One which has the context="module". This tag is different from the typical script instance in two ways.

  1. The script called in module context runs only once and acts as a static variable, existing for all svelte components. A variable declared here can be accessed by every other instance of this component. In this tutorial, the creators of svelte offer a good example of how this can be useful.
  2. Every variable and function exported in this tag can be accessed via import by every module. This allows the preload function to be accessible for sapper when the file is loaded.

The preload function is the function that Sapper uses to fetch data on the server side even before that Component is rendered to the client. It fetches the blog/slug.json and sets those data in the post property once the data are fetched.

The second script tag is simple there to offer a property that can be accessed set by the parent of this component or via the preload function.

The style tag as I talked about in the previous tutorial is pretty self-explanatory. As discussed before all the styles here are unique to this component so as a developer we won't need to worry that other developers might overwrite our CSS in other stylesheets.

Here we are going to add some CSS to ensure the right scaling of our header image that we specified in the dummy post data.

    .headerImage{
        max-width: 100%;
    }

Next we have the part which is a svelte unique component that allows doing changes on the head of the HTML as long as the component is loaded into the dom.

We use this to change the title of the tab bar when our post is open.

Lastly, we have the HTML part that describes our post. Here we will add two tags:

<img class="headerImage" src={post.pictureSrc}>

above the h1 and

<h2>{post.subHeader}</h2>

below it to display a image for our post and the subheader text.

One thing to mention in here is the last div of the component

<div class="content">
    {@html post.html}
</div>

This div is special since the text that is set here is not evaluated as a string but as HTML. This is achieved by adding @html in front of the variable.

The final result looks like this:

<script context="module">
    export async function preload({ params }) {
        // the `slug` parameter is available because
        // this file is called [slug].svelte
        const res = await this.fetch(`blog/${params.slug}.json`);
        const data = await res.json();

        if (res.status === 200) {
            return { post: data };
        } else {
            this.error(res.status, data.message);
        }
    }
</script>

<script>
    export let post;
</script>

<style>
    /*
        By default, CSS is locally scoped to the component,
        and any unused styles are dead-code-eliminated.
        In this page, Svelte can't know which elements are
        going to appear inside the {{{post.html}}} block,
        so we have to use the :global(...) modifier to target
        all elements inside .content
    */
    .content :global(h2) {
        font-size: 1.4em;
        font-weight: 500;
    }

    .content :global(pre) {
        background-color: #f9f9f9;
        box-shadow: inset 1px 1px 5px rgba(0, 0, 0, 0.05);
        padding: 0.5em;
        border-radius: 2px;
        overflow-x: auto;
    }

    .content :global(pre) :global(code) {
        background-color: transparent;
        padding: 0;
    }

    .content :global(ul) {
        line-height: 1.5;
    }

    .content :global(li) {
        margin: 0 0 0.5em 0;
    }

</style>

<svelte:head>
    <title>{post.title}</title>
</svelte:head>

<img class="headerImage" src={post.pictureSrc}>
<h1>{post.title}</h1>
<h2>{post.subHeader}</h2>

<div class="content">
    {@html post.html}
</div>

This completes our new blogpost. It can be accessed just like the last one localhost:3000/blog/how-to-use-sapper .

This of course won't win any styling awards but it should generally show how creating dynamic content in sapper works.

Building a dynamic or static site

Now that you know how to Create and prefetch data for a blog page you can apply this knowledge to other areas of your site. Like the list of all posts or the home page.

After you are comfortable with how your blog looks it's time to either make a dynamic or a static page. In the previous post, you can see the advantages of a static site or a dynamic site. I will not go further into detail on which one you should choose.

For a dynamic site all you will need to do ist run:

npm run build
npm run start

This will generate the dynamic site for you and start it. Usually, this should be done from a server like the virtual server Digital Ocean offers. Usually, this is going to be a Linux server. If you prefer a static hoster then you will need to run another command

npm run export

This starts the server to enable prefetching of the data, seeks through all the links that your site refers to, and generates a static site for each link it can find.

This then can be uploaded directly to a static hoster.

What's next?

In the next post, we will look at the blog page again to enable support for code highlighting and markdown. Markdown is a great text syntax that allows writing good posts quickly and make them look good. It also allows using HTML whenever there needs to be a better way of displaying parts of the text. If this is not of your interests and you are not planning to create a blog but rather another website then you can skip that post and read the one after when I talk about Strapi. A quick and easy way to create a server for setting content.