Create a barebone static blog with svelte and elderjs

Bones drawing by Joyce McCown on Unsplash

What we are building

We building a barebone (without styling) static blog generated with elderjs (you can google "what is static site if you wish to know more what it is"). By barebone i mean you are free to style it as you wanted. The blog template is written in svelte. Containing 3 pages which are the home page and index page and the blog post page. The home page is choose to be in the template to demonstrate basic usage of route.js. The blog index will have a little bit of customization because plugins take care all those details for use so that we can focus on building the template and write the blog.

Target audience

Anyone who interested to use elderjs as their SSG. understand javascript, html and basic things about svelte. But nonetheless You can study those later if needed.

Why use Elderjs

Svelte doesn't have default SSG. Elderjs is built on top of svelte with SSG as it primary concern. Elderjs support partial hydration and won't output javascript if no javascript is used. Really fast SSG output, refer to this twitter thread by Sai Krisshna. Extends functionality with hooks or create your own plugin instead of messing up with svelte or elderjs.

Starting the project

First we open up terminal (or used terminal inside vscode) to copy the elderjs minimal template using degit. If this is your first time using elderjs, i really recommend you to checkout the elderjs/template to learn more about elderjs.

Note: The template comes with elderjs v1.4.3. To use the latest elderjs v1.5.x, you need to manually update the package.

npx degit Elderjs/minimal elderjs_markdown_ssg
cd elderjs_markdown_ssg
rm LICENSE # remove license unless you want to license your blog it as MIT
npm i # install required dependecies

Project folder structure

We will have a folder sturcture that is somewhat like this. Note that i din't include other files like .eslintrc and other which is pretty self explanatory and we wouldn't have much to do with. Currently elderjs rollup.config.js doesn't provide us ways to directly interact with the rollup config. So it's better we just leave it for the time being.

elderjs_markdown_ssg/
├── node_modules
├── assets # static assets that stay mostly static
├── src # this is where our routes and components resides
├── elderjs.config.js # most of the time we are just gonna deal with this
├── svelte.config.js # when dealing with svelte specific config
├── package.json
├── .gitignore
└── README.md

Adding svelte scss preprocess

Then we should enable scss support for svelte in svelte.config.js, you may configure it as you need but i'm just gonna leave it empty. For this to work we should add node-sass or sass as well.

npm i sass
// svelte.config.js
module.exports = {
  preprocess: [
    sveltePreprocess({
      scss: {},
      postcss: {
        plugins: [require('autoprefixer')],
      },
    }),
  ],
};

Adding gulp for base styling (optional)

Other than using gulp, you can also create a components with :global() styling if you prefer. For me, i just prefer to separate global css as from svelte componet whenever possible. Mainly I use scss file separate file for CSS reset and base styling.

Note that elderjs combine or append global styling together with component styling which a neat feature IMO, whereas in barebone svelte, it doesn't combine global and component styling by default for "perfomance" reason.

Simply run gulp in the project folder to generate new style.css in the assets folder

npm i gulp gulp-postcss gulp-sass
// gulpfile.js
const { src, dest, series, watch, parallel } = require('gulp');
const autoprefixer = require('autoprefixer');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');

const files = {
  scssPath: 'scss/**/*.scss',
}

function scssTask() {
  return src(files.scssPath)
    .pipe(sass())
    .pipe(postcss([autoprefixer()]))
    .pipe(dest('assets/'));
}

function watchTask() {
  watch([files.scssPath], parallel(scssTask));
}

exports.default = series(parallel(scssTask), watchTask);
exports.scssTask = series(scssTask);

Start from the next section we can start our dev server running:

npm run start
# or
npm run dev

Markdown plugins config (core)

This plugin is included with the template but update to v1.1.3 to utilize the custom route config.

npm update @elderjs/plugin-markdown@1.1.3
// elder.config.js
plugins: {

  '@elderjs/plugin-markdown': {
    routes: ['blog'],
    contents: {
      blog: 'contents/blog',
    },
    useSyntaxHighlighting: {
      theme: 'nord', // available themes: https://github.com/shikijs/shiki/blob/master/packages/themes/README.md#literal-values - try material-theme-darker
      // theme is the only option available - for now.
    },
    slugFormatter: function (relativeFilePath, frontmatter) {
      // custom slug - use blog title as slug
      // default is markdown file name
      let slug = frontmatter.title
        .replace(/[^a-zA-Z ]/g, '')
        .replace(/\s+/g, '-')
        .toLowerCase();
      return slug;
    },
    useElderJsPluginImages: true,
    useTableOfContents: true,
  },

}

Using custom Shiki themes

Default available shiki themes with elderjs markdown plugin. elderjs-markdown-plugins used it's own fork of shiki which is v0.2.6

// https://github.com/shikijs/shiki/blob/v0.2.6/packages/themes/README.md#literal-values
export type Theme =
  | 'dark-plus'
  | 'github-dark'
  | 'github-light'
  | 'light-plus'
  | 'material-theme-darker'
  | 'material-theme-default'
  | 'material-theme-lighter'
  | 'material-theme-ocean'
  | 'material-theme-palenight'
  | 'min-dark'
  | 'min-light'
  | 'monokai'
  | 'nord'
  | 'slack-theme-dark-mode'
  | 'slack-theme-ochin'
  | 'solarized-dark'
  | 'solarized-light'

however you can also used any vscode themes that you want by downloading your own copy of the respectivesthemes file. You can browse at vscodethemes and get the link to the themes repo. Make sure verify the themes license before you use it.

// so we change the sytax highlgitng config to look like this for exmaple using OneDark-Pro.json
useSyntaxHighlighting: {
    theme: './shiki_theme/OneDark-Pro.json', // available themes: https://github.com/shikijs/shiki/blob/master/packages/themes/README.md#literal-values - try material-theme-darker
    // theme is the only option available - for now.
  },

Building home page

Time to add our first route, the home page. So let's take a look inside the src and default route.

elderjs_minimal/
├── node_modules
├── assets
├── scss
├── src/
│   ├── components
│   ├── layouts
│   └── routes/
│       └── home/
│           ├── Home.svelte
│           └── route.js
├── elderjs.config.js
├── svelte.config.js
├── package.json
├── .gitignore
└── README.md

So here is the default Home.svelte, we are going to remove the Clock.svelte and remove the lines as needed.

<!-- default Home.svelte -->
<script>
  import Clock from '../../components/Clock.svelte'; // remove this
</script>

<style>
</style>

<svelte:head>
  <title>Elder.js Template: Home</title>
</svelte:head>

<Clock hydrate-client={\{\}} /> <!-- remove this -->

You can keep the default route.js as i just add the layout props for reference.

// src/routes/home/route.js
module.exports = {
  all: () => [{ slug: '/' }],
  permalink: ({ request }) => return request.slug,
  data: () => {},
  // we can specify specific layout if needed
  // default to Layout.svelte
  layout: `Layout.svelte`, 
};

So this is our new Home.svelte, I want to keep the example simple as possible, you can adjust on your own as needed or remove the home route altogether is if you don't need it.

<!-- src/routes/home/Home.svelte -->
<script>
  import Footer from '../../components/Footer.svelte';
  import Navbar from '../../components/Navbar.svelte';
  export let helpers, request, settings;

  const title = 'My Home Page';
  const description = 'This is my homepage';
</script>

<svelte:head>
  <title>{ title }</title>
  <meta name="description" content="{ description }" />
  <link href="{settings.origin}{helpers.permalinks.home({ slug: '/' })}" rel="canonical" />
  <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
  <link rel="icon" href="/favicon.ico" type="image/x-icon">
</svelte:head>

<Navbar {helpers} {request} />
<h1>Welcome to My homepage</h1>
<Footer {request} />

Building blog routes

So we are going to add new folder called blog inside the routes. Again template props can be omiited as it just an example that it is possible to define template with different name. By default it will look for .svelte file that have the same name as the route folder.

The permalink is different from the home route this is because we want to have a blog index page and specific blog post page.

Elderjs doesn't have pagination by default so you have to built on your own, or you can use my elderjs pagintion plugin elderjs-plugin-blog-pagination which is exactly what we gonna use here.

What this plugin does is that it allows you to specify specific routes that will use the pagination and specific template for the index page as well as maximum post per page for the index.

// elder.config.js
plugins: {
  // your other plugins
  '@elderjs/plugin-markdown': {
    routes: ['blog'],
    // Your other settings
  },
  'elderjs-plugin-blog-pagination': {
    routes: ['blog'], // change to your blog route same as plugin-markdown route
    postPerPage: 5, // change to your preferred post per apge
    indexTemplate: 'BlogIndex' // change to your own Index.svelte template but without `.svelte`
  },
}
// src/routes/blog/route.js
module.exports = {
  template: 'Blog.svelte',
  all: () => [],
  permalink: ({ request }) => {
    const route = 'blog';
    // if the requst contains `blog` which is the route name
    // it means the slug looks like this `blog`
    if (request.slug.includes(route)) {
      return `${request.slug}`;
    }
    // the markdown
    // it means the slug looks like this `my-post-title`
    // default it will look like this `http://my-blog.com/my-post-title`
    // we want the output to look like this `http://my-blog.com/blog/my-post-title`
    return `${route}/${request.slug}/`;
  },
  data: {},
};

Now lets configure the Blog.svelte template. This main entry which then decide the actual template to use depends on the template specified in the pagination plugin or the default template if non specified.

<!-- src/routes/blog/Blog.svelte -->
<script>
  import BlogIndex from './BlogIndex.svelte';
  import BlogPost from './BlogPost.svelte';

  export let data, helpers, request, settings; // data is mainly being populated from the @elderjs/plugin-markdown
  const { html, frontmatter } = data;
  let component = BlogPost;
  if (request.template === 'BlogIndex') {
    component = BlogIndex;
  } 
</script>

<svelte:head>
  <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
  <link rel="icon" href="/favicon.ico" type="image/x-icon">
</svelte:head>

<svelte:component this={component} {data} {request} {helpers} {settings} />

BlogIndex.svelte is going to looks like this.

<!-- src/routes/blog/BlogIndex.svelte -->
<script>
  export let data, request, helpers, settings; // data is mainly being populated from the @elderjs/plugin-markdown

  const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
</script>

<svelte:head>
  <title>Noxasch - Blog page {request.page}</title>
  <meta name="description" content="Page {request.page} of {request.postEnd}" />
  <link href="{settings.origin}{helpers.permalinks.blog(request)}" rel="canonical" />
</svelte:head>

<div class="content">
  <ul>
    {#each blogPost as blog}
      <li class="list__item">
        <a  href="{helpers.permalinks.blog({ slug: blog.slug })}">
          <h4 class="item__title title">{blog.frontmatter.title}</h4>
        </a>
        <span class="item__date">{ new Date(blog.frontmatter.date).toLocaleDateString("en-US", dateOptions) }</span>
      </li>
    {/each}
  </ul>
  <div class="pagination">
    {#if request.hasPrevious }
      <a href="{helpers.permalinks.blog(request.previousPage)}" class="prev">&lsaquo;</a>
    {/if}
    Page {request.page} / {request.lastPage}
    {#if request.hasNext}
      <a href="{helpers.permalinks.blog(request.nextPage)}" class="next">&rsaquo;</a>
    {/if}
  </div>
</div>

BlogPost.svelte is gonna look like this. In here what to note is that you need have excerpt field in your markdwon file in order to output the excerpt or here is my other plugin that automatically create excerpt from your blog post (inspired by wordpress excerpt).

If you decide to use my plugin this is another plugin that you need to include in elder.config.js

// elder.config.js
plugins: {

  'elderjs-plugin-blog-excerpt': {
    routes: ['blog'],
    overrideExcerpt: true,
  },

}
<!-- src/routes/blog/BlogPost.svelte -->
<script>
  export let data, request, helpers, settings; // data is mainly being populated from the @elderjs/plugin-markdown
  const { html, frontmatter } = data;
  const date = new Date(frontmatter.date);
  const options = { year: 'numeric', month: 'short', day: 'numeric' };
  let formattedDate = date.toLocaleDateString("en-US", options);
</script>

<svelte:head>
  <title>{frontmatter.title}</title>
  <meta name="description" content={frontmatter.excerpt} />
  <link href="{settings.origin}{request.permalink}" rel="canonical" />
</svelte:head>

<div class="content">
  <div class="title">
  <h1>{frontmatter.title}</h1>
    <div class="post__meta">
      {formattedDate}  &bull; {minutesRead} min read
    </div>
  </div>

  {#if html}
    {@html html}
  {:else}
    <h1>Oops!! Markdown not found!</h1>
  {/if}
</div>
<!-- Post ToC -->
<aside class="sidebar">
  {#if data.tocTree.length > 0}
  <nav class="post_nav">
    <ul class="toc_list">
      {#each tocTree as toc}
        <li class="toc_list_item"><a href="#{toc.id}" rel="nofollow" title="{toc.text}"><span style="font-size: 1rem;"></span>{toc.text}</a></li>
      {/each}
    </ul>
  </nav>
  {/if}
</aside>

Wait the Blog Post Example template

We are using the frontmatter meta format which can looks like, you can google on other frontmatter derivatives. The date i'm using is in ISO string, but you can use any date format you like. This will works to date: '2021-06-14'

---
title: 'blog post title and slug'
date: '2021-06-14T12:38:52.634Z' 
excerpt: 'using the plugin exceprt, this field can be omitted'
author: John Doe
tags: ['']
---

## Start with H2 as the title will be the H1

This my content

Managing images and fonts assets

  • plugin-images
  • plugin-google-fonts

By default we can include image as static assets, thus our image path will looks like this !\[github_action](/my-image.png). But you can also create your own hooks to manage the image. But in here we are just going to use the official image plugin. And the path we are going to use is contents/images where content also consist of our markdown files. To make things simple, other than the path we are just leave other the options as it will opt to default.

plugins: {
 '@elderjs/plugin-images': {
    folders: [
      {
        src: 'contents/images*',
        // src: '/src/routes/blog/*', // glob of where your original images are. Relative to rootDir/process.cwd() defined in your elder.config.js. Careful with **.
        output: '/images/', // where files should be put within the distDir defined in your elder.config.js.
      },
    ],
  }
}

Now we can refer to the image from our markdown post like this

!\[github_action](/images/my-image.png)

Host your own google fonts

We are using elderjs-plugin-google-fonts by Kevin Åberg Kultalahti for this. But you can use your own custom way as always if you prefer that. First install npm i elderjs-plugin-google-fonts and add the fthe plugin options to specify fonts that you use and the font-weight that you use. Example as below:

plugins: {
  'elderjs-plugin-google-fonts': {
    fonts: {
      'Montserrat': ['400', '700'],
      'Noto-serif': ['400'],
      'Baumans': ['400'],
    },
    subsets: 'latin',
    swap: true,
  },
}

And then we can use the font in css/scss file

font-family: 'Noto Serif', Times, serif;

Adding Sitemap

Using the official sitemap plugins and we are done. Below is the config

npm i @elderjs/plugin-sitemap
plugins: {
  '@elderjs/plugin-sitemap': {
    origin: '', // the https://yourdomain.com
    exclude: [], // an array of permalinks or permalink prefixes. So you can do ['500'] and it will match /500**
    routeDetails: {
      home: {
        priority: 1.0,
        changefreq: 'monthly',
      },
      blog: {
        priority: 0.8,
        changfreq: 'monthly',
      }
    },
  },
}

Our elder.config.js so far

module.exports = {
  origin: '', // TODO: update this.
  lang: 'en',
  srcDir: 'src',
  distDir: 'public',
  rootDir: process.cwd(),
  build: {},
  prefix: '',
  server: {},
  debug: {
    stacks: false,
    hooks: false,
    performance: false, // nice summary for time take to for each process
    build: false,
    automagic: false,
  },
  hooks: {},
  plugins: {
    '@elderjs/plugin-images': {
      folders: [
        {
          src: 'contents/images*',
          // src: '/src/routes/blog/*', // glob of where your original images are. Relative to rootDir/process.cwd() defined in your elder.config.js. Careful with **.
          output: '/images/', // where files should be put within the distDir defined in your elder.config.js.
        },
      ],
    },
    '@elderjs/plugin-markdown': {
      // your other plugins
      '@elderjs/plugin-markdown': {
        routes: ['blog'],
        // Your other settings
      },
      'elderjs-plugin-blog-pagination': {
        routes: ['blog'], // change to your blog route same as plugin-markdown route
        postPerPage: 5, // change to your preferred post per apge
        indexTemplate: 'BlogIndex' // change to your own Index.svelte template but without `.svelte`
      },
    },
    'elderjs-plugin-blog-excerpt': {
      routes: ['blog'],
      overrideExcerpt: true,
    },
    '@elderjs/plugin-browser-reload': {
      // this reloads your browser when nodemon restarts your server.
      port: 8080,
    },
    'elderjs-plugin-google-fonts': {
      fonts: {
        'Montserrat': ['400', '700'],
        'Noto-serif': ['400'],
        'Baumans': ['400'],
      },
      subsets: 'latin',
      swap: true,
    },
    'elderjs-plugin-blog-pagination': {},
    '@elderjs/plugin-sitemap': {
      origin: '', // the https://yourdomain.com
      exclude: [], // an array of permalinks or permalink prefixes. So you can do ['500'] and it will match /500**
      routeDetails: {
        home: {
          priority: 1.0,
          changefreq: 'monthly',
        },
        blog: {
          priority: 0.8,
          changfreq: 'monthly',
        }
      },
    },
  },
  shortcodes: { closePattern: '}}', openPattern: '\{\{' },
};

Add static assets

assets folder should be used to store all our static assets other than the global styling. Everything within this folder will copied into the output folder whihc is the public folder.

Other file for example are static template images, CNAME file, netlify.toml and favicon.ico

Local output

we can get optimized output by running the build script

npm run build

Publishing To Netlify

TODO

Using netlify build

TODO

Manually with gh-pages

TODO

Publishing To Github Pages

TODO

using Github Action

TODO

  • https://github.com/marketplace/actions/deploy-to-github-pages

Manually with gh-pages

TODO

Example Repository

TODO:

Meta and OG tags

I'm working on a plugins for this, currently i'm using custom hooks. Will update this section when i'm done.

References