Create a barebone static blog with svelte and elderjs
Last update: Aug 3, 2021
This can be consider a tutorial or a walktrhough on how to create markdown blog with elderjs.
What we are building
We are 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. To make things easier, we will use my copy of elderjs minimal template with an update version of elderjs and svelte included with working example from Nick Talk talks during Svelte Summit 2020.
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 noxasch/elderjs-minimal elderjs_blog
cd elderjs_blog
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_blog/
├── 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/components/Footer.svelte -->
<footer>
copyright © 2021
</footer>
<!-- src/components/NavBar.svelte -->
<script>
export let helpers, request;
const pages = [
{
label: 'home',
permalink : helpers.permalinks.home({ slug: '/' }),
slug: '/'
},
{
label: 'blog',
permalink : helpers.permalinks.blog({ slug: 'blog' }),
slug: 'blog'
},
];
</script>
<nav>
<ul class="nav__menu">
{#each pages as page }
<li class="nav__item {page.slug === request.slug ? 'active' : ''}">
<a class="nav__link" href="{page.permalink}">{page.label}</a>
</li>
{/each}
</ul>
</nav>
<style>
</style>
<!-- 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} />
Removing default styling
- remove css from assets/style.css
- remove from layout
<script>
import style from '../../assets/style.css';
export let templateHtml;
</script>
{@html templateHtml}
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.
npm i elderjs-plugin-blog-pagination
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
let blogPost = data.markdown.blog.slice(request.postStart, request.postEnd);
const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
</script>
<svelte:head>
<title>My Site - {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">
<h1>My Blog</h1>
<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">‹</a>
{/if}
Page {request.page} / {request.lastPage}
{#if request.hasNext}
<a href="{helpers.permalinks.blog(request.nextPage)}" class="next">›</a>
{/if}
</div>
</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).
npm i elderjs-plugin-blog-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}
</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 data.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 an example content 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
First of all, push your blog to github, bitbucket or gitlab or any other git remote repo that you use.
Step 1:
Login to your netfliy account, then choose "new site from git"
Step 2:
Pick your version control remote repo
Step 3:
Choose your repository
Step 4:
Now in "Site Settings and Deploy" we just use the default build settings
Step 5:
Wait a litttle bit and boom your blog is ready.
Step 6:
From previous step, choose "Domain Settings"
Step 7:
Click options > Edit Site Name and give your site a cool name
Step 7:
Now here's the link for this example
The first build is going to take a while since it need to build the site from scratch. Also note that if you make big changes or fix existing feature, make sure to clear your netlify cache if somehow the changes is not reflect on your latest build.
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
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.