How this blog is made

Create SEO optimized blogs with Elder/Svelte & Markdown

By Michael Lucht
Keywords: Elderjs, Svelte, Markdown
March 3, 2021

Do you like the style of this blog? Do you perhaps even consider to create one yourself? Let me walk you through the considerations and steps I have taken for the blog you see here. I plan to continuously add what I learned on my journey and in this first part I will explain, which framework I chose and why.

 A Whiteboard with the word blog in the center. Surrounding categories are idea, framework, style, and SEO.


Previously I had created a few projects with React, but recently I discovered Svelte. Once you get your head into Svelte, you can't go back. To me, React improved a lot with functional components, but still, the hook stuff appears forced and boilerplatey compared to the clean Svelte syntax. The number of Svelte libraries is growing, while React already has an enormous eco-system. To be honest, I didn't like most React libraries, except maybe Material UI and react-virtualized/react-window. Often you just get just wrapper for plain javascript libraries, eg. I used D3, Pixijs and Animejs, and in all three cases I ended up cutting out the React wrapper and use the base library. And btw., my experience is that working with base libraries is easier with Svelte than with React, because you are closer to the DOM, the real one.

A blog doesn't have complex interactions or password-protected content. Instead, search engine optimization (SEO) is important. Since there are still many search engines that are unable to use Javascript, a vanilla Svelte app could perform worse than a static page or server-side rendered (SSR) page. My provider (affiliate link, see my disclosures) restricts server code to PHP (at least with my rate) and I don't want two languages for a simple blog, so static site it is. Actually, I use a little PHP on this page, but all is programmed in a REST API style, the content pages are all delivered via static HTML. If you sign up for the newsletter, PHP will send you an email to confirm, and the confirmation is saved in the database. These are things, that are not possible with client-side javascript.

So, 'Static page' simply means that the main content of the page is inside the HTML file. Modern frameworks like React and Svelte by default create the content with Javascript, which may be hard to read for search-engine crawlers.

There are a few options for Svelte based static site frameworks eg. Sapper or Jungle.js. According to Sappers site, it is in early development and it will be discontinued in favor of Svelte Kit. Svelte Kit is not officially released, yet. So I tried out Elder.js. It has a strong focus on SEO, while still allowing for interactive fine-tuning via partial hydration.

What's great about Elderjs:

What is not so great:

Data Flow

The Elder workflow starts with data. The data can come from a database or different sources. In my case, the data is a bunch of Markdown files. This data is programmatically transformed into single static pages. Elder is very efficient, according to the authors you can create thousands of pages within minutes.

The Elder template includes a blog template, which makes it very easy to get started. Just add some markdown files to the folder src/routes/blog/. Elder then builds simple HTML pages. Add some CSS style and you're done.

As you can probably see I'm a bit special, so I customized it a little more, sprinkle in some animation here and there, make headings not bold but underlined, and so on. This is how the data is processed: The markdown files are converted by Elder via remark and the generated Html code is retrieved in src/routes/blog/Blog.svelte in data.html. The Html is then parsed with node-html-parser, which returns an Abstract syntax tree (AST). The root node of the AST is sent to a recursive Svelte component, which is processing the child nodes. It looks like this:

<script> export let node; const content = node.childNodes; </script> {#each content as item} {#if item.rawText && !item.rawTagName} {item.text} {:else if item.rawTagName === 'p'} <p> <svelte:self node={item} /> </p> {:else if item.rawTagName === 'hr'} <HorizontalLine hydrate-client={{}}/> {:else if item.rawTagName === 'ul'} <ul {...makeProps(item.rawAttrs,"list-disc list-outside pl-8 my-4")}> <svelte:self node={item} /> </ul> ... {:else if item.rawTagName === "Magic8"} <MagicEightApp hydrate-client={{}}/> {/if} {/each}

Going through the if-else: if a node contains only text, just display the text. If it is a p- element, add a p- element and process the child nodes. An hr- element represents a horizontal line and I want to animate it, so here I add a custom component.
Admittedly it is not very elegant, since I need to check for each possible element that is in the Html, but markdown is very limited, so even with remark-gfm (GitHub flavored markdown)- plugin, which adds tables and checkboxes, etc., there are only 22 different elements. The ul- element (unordered list) get the props from the Html document and there are some custom CSS-classes added.
Lastly, you see that I can easily extend the functionality by adding custom tags. I can write <Magic8/> in my markdown file, and the code would add the MagicEightApp component at that position, see it in action.

Merge scripts hook

On my pages many items are hydrated. Each header is animated when you scroll near it, but also each code block is generated via hydration. By default, elder adds two script blocks to the page for every hydrated element. When you have so many of them, your html gets bloated. The lighthouse report (F12 on chrome) even warns me of excessive dom size. To reduce the size a little, I added the following hook to src/hook.js:

... const hooks = [ { hook: 'html', name: 'compressHtml', description: "Merge scripts into two in html. This is a big no-no, but let's give it a whirl.", priority: 1, // last please :D run: async ({ htmlString }) => { let first=true; // this function takes the 'htmlString' prop, compresses it with Regex, then returns it. return { htmlString: htmlString .replace(/<\/script>[ \n]*?<script>/g,";") .replace(/<\/script>[ \n]*?<script type="module">/g,(m)=>{ if (first){ first=false; return m } return ";"; }) }; }, }, ...

The second sentence in the description text is from an example hook which comes with elder. Since it fits, I keep it. What I do ther is to remove text from the html files with regular expressions. The pattern in the first .replace matches for all script end tags which are followed by blank spaces and/or line breaks, just to open another script tag. Basically it just merges all <scripts> into a single script. The second replace merges all <script type="module">s into a single script. I need to apply the function syntax in replace to leave the first <script type="module"> untouched.

Social media tags

To promote the blog on social media, you can add special meta tags to your page. These tags help to make your page look good, when shared via facebook, twitter or other platforms. Eg. for tweets you can design a card, with a picture a title and a description. You can look up a list of available open graph protocol tags and for twitter cards.

These meta tags belong into the <head> section of the page. My approach to determine the important ones is to look up a few pages, which I think have a good social media presence. Then open the dev-tools of the browser (usually F12) and look, which they use. When you read tutorials, they usually tell you, that only very few tags are necessary, but in real life, most pages put in every tag they seem to know.

With Elderjs, you can generate the tags programmatically from markdown, so I put in everything I could find as well. I add them at two different occations. The first is for general information in the layout file:

/// src/layouts/Layout.svelte <script> ... export request ... </script> <svelte:head> ... <meta property="og:type" content="blog"> <meta property="og:site_name" content="GradientDescent"> <meta property="og:email" content="" /> <meta property="og:url" content={""+request.permalink} /> <meta name="twitter:url" content={""+request.permalink} /> </svelte:head> ...

In the layout file, you have access to the request object. With request.permalink you can get the processed url.

The majority of tags I add in the src/routes/blog/Blog.svelte. Most of the information is added in the frontmatter part of the markdown files. At the beginning of this file I currently have the following information:

--- title: 'Create SEO optimized blogs with Elder/Svelte & Markdown' excerpt: 'In this series of posts I will go through some of the design decisions I had to make for this blog. The first part will describe the framework and how it focuses on productivity using Markdown while remaining flexible and SEO optimized via Elderjs.' twitter: 'The first part of -How this blog is made- covers the framework and how it focuses on productivity using Markdown while remaining flexible and SEO optimized via Elderjs.' date: '2021-02-11T01:23:45.432Z' first: '2020-12-01' series: 'How this blog is made' author: Michael Lucht keywords: Elderjs, Svelte, Markdown image: blogwhiteboard.jpg ---

I have an extra description for twitter, since there only 200 symbols are allowed. In Blog.svelte this is by default extracted from the data object. Here is part of what meta tags I add:

/// src/routes/blog/Blog.svelte <script> import sizeOf from 'image-size'; ... export let data; // data is mainly being populated from the /plugins/edlerjs-plugin-markdown/index.js const { html, frontmatter } = data; const image = '' + (frontmatter.image ? frontmatter.image : 'gradientdescent.jpg'); const imagedims = sizeOf('./assets/images/' + (frontmatter.image ? frontmatter.image : 'gradientdescent.jpg')); const twitterdescription = frontmatter.twitter || frontmatter.excerpt; const keywords = (frontmatter.keywords||"").split(",").map(v=>v.trim()); ... </script> <svelte:head> <title>{(frontmatter.series ? frontmatter.series + ' - ' : '') + frontmatter.title}</title> <meta name="description" content={frontmatter.excerpt} /> <meta property="og:image:url" content={image} /> <meta property="og:image:type" content="image/jpeg" /> {#if imagedims && imagedims.width && imagedims.height} <meta property="og:image:width" content={imagedims.width} /> <meta property="og:image:height" content={imagedims.height} /> <meta name="twitter:image:width" content={imagedims.width} /> <meta name="twitter:image:height" content={imagedims.height} /> <meta name="twitter:card" content={imagedims.width > 1.45 * imagedims.height ? 'summary_large_image' : 'summary'} /> {/if} {#each keywords as keyword} <meta property="article:tag" content={keyword} /> {/each} ... </svelte:head> ...

image-size is a neat little helper, so that I don't need to look up picture dimensions for every image. I also automatically decide, if the image is better suited as a 'summary', which shoulf have square format, or "summary_large_image", which roughly has 2:1 width to height ratio.


The design framework for this blog is based on the Elderjs template. It offers a powerful build process. It generates simple Html pages, which are very accessible for search engines. I changed a few steps to customize everything to look the way, I want it to look. I will get more into detail about the visuals in the next blog post of this series.

Share this article on twitter - Vote on next topics - Support me


Update 02/11/20: Add the section 'Social media tags'.
Update 03/03/21: Add the section 'Merge scripts hook'.

Next up: How this blog is made ii - Make your website sketchy with Tailwind and Roughjs

This might also be interesting for you:

Support me:

or directly

Design & Logo © 2020 Michael Lucht. All rights reserved.