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.
Elder.js
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:
- powerful build process
- partial hydration - only deliver code where it is necessary
- great and easy to use blog-template
- super optimized final page - perfect lighthouse score possible
- default use of intersection observer - improves performance and animations are only played when the component is visible
What is not so great:
- debugging - when you change code, the Dev server needs to be restarted (markdown and CSS changes are ok)
- hydrated components are wrapped in an extra div - can cause trouble eg. with
, or when you want to fill something into anposition:absolute
- elementsvg
- complicated rollup process makes it hard to add things like Tailwind
- some parts are hacky - (hydrated?-) components need to be named according to the filename in
foldersrc\components
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
. Elder then builds simple HTML pages. Add some CSS style and you're done.
src/routes/blog/
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
in src/routes/blog/Blog.svelte
. 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:
data.html
<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
- element, add a p
- element and process the child nodes. An p
- element represents a horizontal line and I want to animate it, so here I add a custom component.hr
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
- element (unordered list) get the props from the Html document and there are some custom CSS-classes added.ul
Lastly, you see that I can easily extend the functionality by adding custom tags. I can write
in my markdown file, and the code would add the <Magic8/>
component at that position, see it in action.
MagicEightApp
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
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 .replace
into a single script. The second replace merges all <scripts>
s into a single script. I need to apply the function syntax in <script type="module">
to leave the first replace
untouched.
<script type="module">
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
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.
<head>
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="micha-lmxt@gradientdescent.de" /> <meta property="og:url" content={"https://gradientdescent.de"+request.permalink} /> <meta name="twitter:url" content={"https://gradientdescent.de"+request.permalink} /> </svelte:head> ...
In the layout file, you have access to the
object. With request
you can get the processed url.
request.permalink
The majority of tags I add in the
. 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:
src/routes/blog/Blog.svelte
--- 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
this is by default extracted from the data object. Here is part of what meta tags I add:
Blog.svelte
/// 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 = 'https://gradientdescent.de/images/' + (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> ...
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.
image-size
Conclusion
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:
- React Hooks in Svelte
- tree-shake-css - project page
- Sveltes tick is my new best friend - porting react-textfit to Svelte
- How to merge cells with svelte-window
- Porting React to Svelte - From react-window 1:1 to svelte-window
- Poll: Which React library would you love to see in Svelte?
- Porting React components to Svelte - It might be easier than you think
- Why are component libraries so complicated with React compared to Svelte?
- Svelte and Typescript
- Showcase: The Descent Ripple Effect or The React Descent Ripple Effect