Setting up a blog with mdsvex
Published: 28-Oct-2024, by maardal
Hi ya’ll!👋
In this blogpost we’re gonna take use of use mdsvex, in a Sveltekit Project, with TypeScript and Tailwind CSS.
Format
This post is in three main parts. 🔢
In the first part I mention why I went for mdsvex, and how I am gonna use it.🤔
Second part goes through how I got it set up and posted this post. This will try to be as minimal as possible. (Don’t believe this 😇)
The last part will go through what I got wrong 😑 and what I learned.
If you wanna go straight into it, jump to 👉 [2. Setting Up mdsvex].
If you’re facing challenges in implementing it, you might find out why in 👉 [3. Learning and Mistakes].
1 - Why mdsvex
Yeah why? Okay I’ll tell you about the background for using mdsvex 😋.
1.1 - Background
I’ve had a wish to make a type of blog for a while, and I’ve finally gotten around to it.
Before me I had several options for making a blog. On the top of my head, you can sign up for a blog service, you can use website builders, or you can add the functionality to your own website.
As it happens I have my own website, This one! 🤩
So that is one reason. I have my own place to put it all. Secondly, I wanted to have the ability to my blog posts to be somewhat independent of this site, and in a format that could be used elsewhere. The most resaonable option was have the writings in Markdown.
I did some research, and the only feasible option in Svelte-land I saw was mdsvex, as Svelte and Sveltekit doesn’t have internal library support for rendering Markdown.
So here we are 👉🗺️. I wanted to blog 📸, and I have my existing personal website 🛜, I write my stuff in Markdown, and want to add mdsvex to my website stack, so the Markdown files can rendered correctly (by transforming Markdown markup to HTML markup).
1.2 - What is mdsvex
mdsvex is a Markdown preprocessor compatible with Svelte components. It will make it possible to write Markdown in Svelte components (in effect making Markdown files a Svelte component) and add Svelte components in Markdown files.
1.3 - How will mdsvex be used
I’m gonna write this, and upcoming posts in Markdown files and have mdsvex transform the Markdown to HTML, that we can then load in to a Svelte component. 📝
1.4 - Stack
The current stack is:
- npm 18.7
- Sveltekit 1.20.4 & Svelte 4.0.5
- TypeScript 5.0.0
- Vite 4.4.2
- Tailwind CSS 3.3.3
- Hosted on Netlify
and adding:
- mdsvex 0.12.3
1.5 - Resources
I followed these three resources to get this to work, often slavishly. My gratitude to them for writing about this, making it possible for me to easily add this functionality to my website. 🙇🙇🙇
- 👉mdsvex docs
- 👉Build static Sveltekit Markdown blog, by Josh Collinsworth
- 👉Building a blog with Sveltekit, Tailwind CSS and mdsvex, by Jeff Pohl Meyer
2 - Setting up mdsvex
The installation and setup is fairly easy 🙌, and we should get to the first render of a within 4 steps.
2.1 - Installing mdsvex
I am running Node, so we install mdsvex with the Node package manager (npm):
npm i -D mdsvex
adding it to my dev dependencies in Node. The package.json gets updated accordingly. You find this file in the root folder of your project.
//file: package.json
"devDependencies": {
"mdsvex": "^0.12.3",
"more options:" "1.2.3",
}
2.2 - Configuring mdsvex and Sveltekit
After installing, we need to tell mdsvex how to behave ☝️, and how Sveltekit should interact with mdsvex.
2.2.1 - Configuring mdsvex
First we add the following config file for mdsvex, called mdsvex.config.js. We place this in the root folder.
//file: mdsvex.config.json
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import { fileURLToPath } from 'url';
import * as path from 'path';
const dirname = path.resolve(fileURLToPath(import.meta.url), '../');
const config = defineConfig({
extensions: ['.md', '.svx'],
layout: { blog: path.join(dirname, './src/routes/stuff/blog/[slug]/+layout.svelte') }
});
export default config;
The first variable holds the system specific absolute file path of the config. import.meta.url is a native ESM feature. It reveals the the module’s full URL. The module is this config file above, so the path will end like ‘..parentfolder/mdsvex.config.js. With the Node function fileURLToPath, we transform the URL to a resolved platform specific Node.js file path. This is then used as an argument to another Node function, path.resolve, along with the path ’../. This function returns a resolved path string combined from these two arguments. ’../’ is the relative path for moving up one folder, effectivly removing the filename from the path. This should then give us the root project folder.
I am running the dev server on Windows, so this will produce a path string of the following pattern:
C:path\tomyprojectfolder
Further we use the mdsvex library function defineConfig and supply an argument object with the extensions we want mdsvex to transform. .svx is mdsvex’s file type. In these files we can mix and match Svelte component syntax and Markdown. Then, we supply the layout we want to use. In this case we have a named layout, named blog. Here we use the system specific file path to our project from the dirname variable, and join it with the project specific path to our layout Svelte file, with the Node function path.join.
The layout property lets us reference the layout in our Markdown files (see code snippet below) and also add more layouts, per type of Markdown content, for example for art-blog.
---
title: Foo Bar
date: 2022-04-15
layout: blog
---
If you only have one layout, you could also just define it without a name as so:
mdsvex({
layout: ./src/routes/to/the/blog/content/+layout.svelte'
})
The arguments supplied to defineConfig could also be given as an argument to the mdsvex processor in svelte.config.js. However, I like having this config in it’s own file, as it makes it more clear where the configuration for mdsvex lives, and we keep the Svelte config cleaner.
mdsvex has more neat👉options, that you can use to change up or add to its behavior. It also has the possibility to add 👉 remark and 👉 rehype plugins, adding the capability to further change how the Markdown and HTML is parsed. For example there is a 👉 remark plugin that can parse and make a Table of Content for you. Maybe we can add that for later, once I’ve figured out how I want the blog to look.
2.2.2 - Updating the Sveltekit config.
Next step is to update the Svelte config.
Below I’ve shared the whole svelte.config.js file as is.
// File: svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { mdsvex } from 'mdsvex';
import mdsvexConfig from './mdsvex.config.js';
const config = {
kit: {
adapter: adapter({
edge: false,
split: false
})
},
extensions: ['.svelte', ...mdsvexConfig.extensions],
preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)]
};
export default config;
From before I have only added a Vite preprocesser and a Netlify-adapter, where this site is currently hosted.
We start by importing mdsvex along with the mdsvex.config.js we have made.
Next we use the spread opreator (…) on the mdsvexconfig.extensions array. Remember we defined the .md and .svx as extensions in mdsvex.config.js.
The spread operator is syntactic sugar for kind of iterating over an array or collection of elements. More correctly it allows an iterable to be expanded in place. That means when we write it as we did, the extensions array becomes:
['.svelte', '.md', '.svx'].
Lastly we tell Svelte to use the mdsvex preprocessor, with the config as an argument. This is where we could have written the whole configuration that we wrote in mdsvex.config.js.
2.3 - First render
We’re now ready to see if the install and configuration has bore fruit 🍉.
To test it, we can write a Markdown file, and put it somewhere in the somwhere under the src/routes/ folder. I still will need a place keep my upcoming blogs, so I make the following structure and make a new Markdown file, called +test.md. Remember we added .md as an extension that mdsvex should process, so we can write our Markdown and save it as is.
|-- src
| |-- route
| | |-- stuff
| | | |-- blog
| | | | |-- +test.md
The content of the Markdown is as follows, and we expect to see this content rendered out on the website.
---
title: Foo Bar
author: Jeff
date: 2022-04-15
layout: blog
excerpt: Lorem ipsum dolor sit amet...
---
# Testing
## testing 2
### we love farts
- SvelteFit
- Very Sexual Code
- Emoanjis
I start the local development server, navigate to the path for the file. Lo and behold, the contents of the Markdown file is rendered to the page on the left of this screenshot. On the right we see the Chrome Developer Tools elements view. Under the nav element, we see the same text we marked up with Markdown now being bookended by the corresponding HTML elements. Success, woohoo 🥳.
You might notice that the text is not rendered as expected. The headings are all the same text size as the rest of the file content and the unordered list is missing any legend symbol. This is cause of Tailwind CSS’s default behavior. Had we not have Tailwind CSS installed, this would be rendered correctly by the browser. We will fix this later in section 👉[2.6 Styling with Tailwind CSS]. First we will focus on make a list view of our blog posts.
2.4 - Blog list view
The next step for me was to make a page for listing out all the blogposts. We go through where we’re gonna store the Markdown files, what Frontmatter is, implementing the code to find and present the Markdown files, before we show the rendered page.
2.4.1 - Where to store the Markdown.
I came to the conclusion that I wanted to store my Markdown files, and their related content (like images) in a folder removed from the routes folder. There were two main reasons. The first is that I want to write several posts, and having them in the blog route folder along just sounds stupid, crowding the folder. The second is that I miiight want to my Markdown files in their own Git repository.
So, I create a folder for my Markdown files under the src folder:
|-- src
| |-- content
| | |-- blog
| | | |-- markdownfile1.md
| | | |-- markdownfile2.md
| | | |-- markdownfile3.md
The way we’re implementing this, the Markdown files can exist anywhere on in the project structure, so feel free to do what you want.
2.4.2 - Frontmatter & Markdown
👉Scribendi.com tells us front matter is the first section of a book, used for several things in books. It can be the title, preface, foreword or any other use really.
In Markdown land, it is a commonly used convention. Your Markdown files will be rendered just fine without them, but they open up opportunities for us.
Frontmatter sits, as with books, in the first section of a Markdown file. With it we can define any metadata we want. It will not be rendered out as content by mdsvex. Below we see we have defined a title, a date, a layout, and a jokey field, to illustrate it can be whatever.
---
title: test
date: 2024-12-05
layout: blog
anyFieldYouCanThinkOf: true
---
# Heading 1
## Heading 2
mdsvex lets us access these fields as metadata on an object, after it has transformed the Markdown to HTML. We can act upon this metadata in code.
Like we will do for the layout field. We’re gonna use this tell mdsvex the type of layout we want to use to render it this file. The fields can help you do what ever you want, just be creative!
This next thought isnt really very creative, but in my other posts I’ve added the field published to indicate if the file should be rendered out or not. In the future I want either linting rules or some other way, to enforce certain fields to be present, so I can break builds of they are missing, so I avoid bugs and time wasting (if I end up writing loads of these). But that is for another time.
We see that the frontmatter is sectioned of by --- (three hyphens) above and below the data. Using the hyphens is the YAML way of indicating this is Frontmatter. This is the default that mdsvex 👉expects. If you want to change it, you can 👉configure mdsvex behavior to use other symbols.
2.4.3 - Finding and accessing Markdown files and content
For listing out the Markdown files, we need to implement some functionality.
First we need to locate the Markdown files. Secondly we want to transform some data, and lastly render out that data.
The convention in Sveltekit is to load data with a +page.ts/js or +layout.ts/js. Do you need the data in the +layout.ts/js file, place it there, otherwhise, we use the +page.ts/js file. The function must be named load. This data we can then access in a corresponding +page.svelte or +layout.svelte file.
To show this I start out by showing the +page.ts file I placed in src/routes/stuff/blog folder.
2.4.3.1 - +page.ts - looading in data.
// Folder: src/routes/stuff/blog/+page.ts
import { filterMarkdownFiles, findMarkdownFiles, formatFrontmatter } from '$lib/utils';
export async function load() {
const files = findMarkdownFiles();
const blogFiles = filterMarkdownFilesOnType(files, 'blog');
if (!Object.keys(blogFiles).length) {
return;
}
const formattedFiles = formatFrontmatter(blogFiles);
return { formattedFiles };
}
In this file, we find all Markdown files, before filtering them for Markdown files on files containing the layout with value blog.
Last, we format the frontmatter from the Markdown files.
🔔 All these functions used in the file above are placed in a util.ts file in the src\lib folder.
2.4.3.2 Finding Markdown files
//File src/lib/utils.ts
export const findMarkdownFiles = () => {
return import.meta.glob('/src/**/**/**/*.md', { eager: true });
};
In the findMarkdownFiles we use a Vite function to import all Markdown files in the project.
The first argument for the glob is the pattern for the files it should look for. In this case the it will look for any Markdown files (*.md) in any folder from the src folder, and any folder below for a depth three below (/src/**/**/**/). This argument has to be a string literal, meaning, you can’t do string interprolation, or supply the method with a variable referencing a string.
The second argument of an object with the eager option, makes it so that all the imports (every Markdown file we find) will be imported on the module (this utils.ts file). The 2nd argument is optional.
//
{
'/src/content/blog/test-copy.md': {
default: { render: [Function: render], '$$render': [Function: $$render] },
metadata: [Getter],
[Symbol(Symbol.toStringTag)]: 'Module'
},
'/src/content/test.md': {
default: { render: [Function: render], '$$render': [Function: $$render] },
metadata: [Getter],
[Symbol(Symbol.toStringTag)]: 'Module'
}
}
We see the output from the glob function above. It is an object with the path of the Markdown files as properties, and another object as the value. This other object is the contents of the Markdown file. The metadataproperty is the data we described in the Markdown Frontmatter and default is the contents of the Markdown file. We will take use of this in explaining the next functions.
2.4.3.3 - Filtering out the Markdown files we’re looking for
//File src/lib/utils.ts
export const filterMarkdownFilesOnTypes = (markdownFiles, type: string) => {
return Object.fromEntries(
Object.entries(markdownFiles).filter(
([path, post]) =>
post.metadata.layout.toLowerCase() == type.toLowerCase() && post.metadata.published == true
)
);
};
Now, you don’t need to filter out the Markdown files, but I want to, cause I want to post a different kind of post in the near future, but in another section of my website. If you don’t need to, you can skip this part, and go to the part about formatting the Frontmatter.
filterMarkdownFilesOnTypes takes an object like above and the type of Markdown file as arguments. We use the higher order function Object.entries to iterate over the object properties. From that iterable object, we use the filter function. The filter function takes a Predicate callback function as an argument, and will filter any entries satisfying the predicate function evaluation.
In this case we provide an Arrow function, which is a shorthand for writing Anonymous functions. We define the Predicate Arrow Function with the argument [path, post]. This is the structure of the objects described in the output of the glob function, the path is the path, and post is the contents of the markdown file. Next we define the function body of the Arrow function. There we evaluate if the Markdown files has the same layout type as we want to filter on, and if it has the status as published. Any posts that satisfy these two demands will be filtered to a new array.
The Arrow function is equivalent of writing out the function below. This could then be provided as an argument to the filter function instead.
const blogPredicate = function () {
return (
post.metadata.layout.toLowerCase() == type.toLowerCase() && post.metadata.published == true
);
};
The lastly we make a new object from the filtered array, using the Object.fromEntries function.
We now have the Markdown files of type “blog” and have the status as “published”, and we can go on to formatting those files.
2.4.3.4 - Formatting Markdown metadata
//File src/lib/utils.ts
export const formatFrontmatter = (markdownFiles) => {
return Object.entries(markdownFiles).map(([path, post]) => {
/**
* mdsvex returns an untyped object. To let this compile, the lines referencing
* this untyped object is ignored.
*/
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...post.metadata,
path: path.slice(13, -3),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
published: convertDate(post.metadata.date)
};
});
};
The formatFrontmatter function takes in the now filtered object containing the path, metadata and contents of the Markdown files, and returns an object containing the original metadata, as well as subset of the path, and the date as a string on the format “15-Feb-2022”. We don’t need the contents of the files now, as we only want to list out all the Markdown files with the type of blog.
As with the filter step, we iterate over the object with the Markdown path, metadata and contents. Now we map the path and post data instead.
With path.slice(13,-3) we first remove the 13 first characters. We placed the Markdown files in the path /src/content/blog/*.md. The first 13 characters correspond to /src/content/. With the 2nd argument, we remove the last 3 characters, starting from the end of the path. This is the .md file extension. That means, using the two Markdown files from earlier as an example, we’re left with “blog\test.” and “blog\test-copy”. Basically anything between these sections, we’re left with.
I had to be a bit hacky in this function to disable TypeScript rules, to get this to build. More on that in the section about 👉 [Learnings and mistakes].
I realise now that this name doesn’t really make sense, cause this method only returns data about the Markdown files, and not the contents itself. I will rename this for another time… 😅
// Folder: src/lib/utils.ts
export const convertDate = (dateString: string) => {
const months = new Map<number, string>([
[1, 'Jan'],
[2, 'Feb'],
[3, 'Mar'],
[4, 'Apr'],
[5, 'May'],
[6, 'Jun'],
[7, 'Jul'],
[8, 'Aug'],
[9, 'Sep'],
[10, 'Oct'],
[11, 'Nov'],
[12, 'Dec']
]);
const date = dateString.substring(0, 10);
const [year, month, day] = date.split('-');
return `${day}-${months.get(parseInt(month))}-${year}`;
};
convertDate was used in the *formatFrontmatter function. It simply takes a string with the date, splits it up into year, month and day, converts the month number to a corresponding month text shorthand, and returns a string on the format “15-Feb-2022”.
We then end up with the following data, returned from +page.ts.
[
{
title: 'Foo Bar',
author: 'Jeff',
date: '2022-4-15T00:00:00.000Z',
layout: 'blog',
excerpt: 'Lorem ipsum....',
published: '15-Apr-2022',
path: 'blog/test'
},
{
title: 'Poo Bar',
author: 'Jeff',
date: '2022-4-15:00:00.000Z',
layout: 'blog',
excerpt: 'Lorem ipsum....',
published: '15-Apr-2022',
path: 'blog/test-copy'
}
];
2.4.3.5 - +page.svelte - presenting data
After all this, the +page.ts file returns an array of objects with Markdown file metadata, like shown above. We can use this to build up a list on the website of the blog posts.
<script lang="ts">export let data;
</script>
<div class="w-10/12 sm:w-9/12 lg:w-6/12">
{#if data.formattedFiles !== undefined}
<div class="flex flex-col gap-8 px-10 pb-10">
{#each data.formattedFiles as post}
<a href={post.path}>
<div
class="h-40 overflow-hidden rounded-lg bg-white bg-opacity-10 p-2 shadow-lg hover:bg-opacity-25"
>
<p class="text-xl">{post.title}</p>
<p class="mb-2">{post.published}</p>
<p>{post.excerpt}</p>
</div>
</a>
{/each}
</div>
{:else}
<p class="text-center text-xl">
I've not published any blog posts yet.<br /> Ooooor me done fucked up. <br />Yikes.
</p>
{/if}
</div>
The Sveltekit convention with exposing data from +page.ts now lets us easily access that data in our +page.svelte file, with the export let data statement in the script tag block.
The first thing we decide on for rendering out content, is if there is no data returned from +page.ts, meaning that data has the value of undefined, we don’t try render any data, but instead show the text in the else block.
If we have data from the Markdown files, we iterate over each array entry, with the each Svelte construct.
Then we create an Anchor tag, with the path to the Markdown file as value, and the show the other metadata we want to share.
2.4.4 - The blog post list view
Okay, we’ve done all the code steps needed for rendering out these blog posts. I navigate to the part of the website for these posts, and find the following listed out 😻.
We see the title and excerpt from the Markdown frontmatter, and the converted date format that we added with the formatFrontmatter function. It is not much, but it is something 🧑🌾.
2.5 - Blog detail view
So we’ve managed to find Markdown files in the project folders, extracting metadata from them, and using that metadata to present a list of the Markdwon posts.
We’ve also made the list items into links, pointing to what is supposed to be the Markdown posts. However, clicking these will bring you to a 404 - Not Found error page. That is cause there is not Svelte pages, matching the routes for those files.
example.com / stuff / blog / test || example.com / stuff / blog / test - copy;
For example, these paths shown above I have not created their Svelte +page.svelte for yet. Or rather, I won’t make one for each of them. For that we will use the Dynamic Routing functionality in Svelte, which suprisingly, is the next step we do.
So, this is what we’re gonna solve next, woohooo. 🥳
2.5.1 - Dynamic Routes in Sveltekit
First, there is another method to present these posts. You can make a folder, with the same name as the filename. After that you create a pair of .page.ts and +page.svelte file for loading in, and presenting that Markdown file. If you don’t plan to have a lot of posts, this might be an option. It might even be easier to do different styles for those pages, than the Dynamic Routes approach I am gonna use. But that is just a random thought. Lets move on.
2.5.1.1 - Routes with dynamics parameters - Folder Structure.
The approach I am using, is called Routes with Dynamics Paramaters in Svelte. With this approach, we make a generalized page to use to present all the blog posts we’re gonna write.
The first step of the method is to create a specially named folder.
In our blog folder, we create a new called folder [slug]. The square brackets [] tells SvelteKit that route will be created dynamically. The name slug is the convention naming, but it can really be anything.
Okay, so now we have the following folder structure for the routes to the blog posts.
|-- src
| |-- routes
| | |-- stuff
| | | |-- blog
| | | | |-- [slug]
2.5.2 - Load in data - +page.ts
Inside this folder we place as normal, a +page.ts. We’re gonna use this to load in the individual posts Markdown content.
//File: src/routes/stuff/blog/[slug]/+page.ts
import { convertDate } from '$lib/utils.js';
import { error } from '@sveltejs/kit';
export async function load({ params }) {
try {
const blog = await import(`../../../../content/blog/${params.slug}.md`);
const { title, author } = blog.metadata;
let { date } = blog.metadata;
date = convertDate(date);
const content = blog.default;
return {
title,
author,
date,
content
};
} catch (e) {
error(404, `Could not find ${params.slug}`);
}
}
The first thing you might notice, is that we now have a params parameter in the load function signature. This is where SvelteKit will inject a RouteParams object.
In this case that is the path we provided to the link (or Hypertext Reference) in the blog list page, which will correspond to this route. If we remember back to the formatted frontmatter data, the paths of the test files were blog/test and blog/test-copy. The route to the folder this file is in, is src/routes/stuff/blog/[slug]. Svelte will then figure out, that rest of this path is will be test and test-copy and will give that as a parameter, and switch [slug], with these option params. Pretty fancy.
A little side note. Let us say you had chosen a different name than slug for the folder name for the routes, for example, crayon. The RouteParams object would then have a property called crayon, instead of slug, like so: params.crayon.
So, we now have access to a RouteParams object, containing the slug property with the path, or rather filename, as value. That is the only thing we receive. We now gotta find the actual Markdown file with that same filename. Using the data from the RouteParams object, we import the file from the folder I know my posts exist. We the deconstruct the data from the imported object., and return it from the load function, ready to be presented in the +page.svelte file.
For throughness sake, the returned object from the import has the following structure, and here is where the default, and metadata properties are from. Default holds the Markdown content, and the metadata holds the Frontmatter we defined in the Markdown files.
// Returned object from import.
{
default: { render: [Function: render], '$$render': [Function: $$render] },
metadata: [Getter],
[Symbol(Symbol.toStringTag)]: 'Module'
}
2.5.3 - Layout + mdsvex - +layout.svelte
mdsvex let’s us define named layouts we wanna use for these posts. I myself defined a blog layout, which should be found in the path src/routes/stuff/blog/[slug]/+layout.svelte. See 👉[2.2.1 Configuring mdsvex].
Therefore, the next thing I do is to create this +layout.svelte file.
Now, this might be a let down, but this is how my layout.svelte looks like.
// File: src/routes/stuff/blog/[slug]/+layout.svelte
<slot />
I don’t make use of any data, and I don’t style this +layout.svelte, but (I think) it has to exists for mdsvex to slot content into. (From +page.ts, to this +layout.svelte file, and to +page.svelte). Now, I could have styled some of the layout for the blog posts here, like any structure around the content itself, but I found it more reasonable to just have it all in the +page.svelte file at this moment, cause it is fairly simple.
2.5.4 - Presenting data - +page.svelte
We’re now onto presenting the Markdown files again, just like we did when testing if mdsvex was working. Yaay, back to the start 🤪.
// File: src/routes/stuff/blog/[slug]/+page.svelte
<script lang="ts">export let data;
</script>
<article class="w-10/12">
<h1 class="mb-1 text-center text-4xl">{data.title}</h1>
<p class="mb-2 text-center text-xl">Published: {data.date}, by {data.author}</p>
<div class="text-center">
<svelte:component this={data.content} />
</div>
</article>
The +page.svelte is not very complicated. We expose the data from the +page.ts file, in the script tag. We try to make a somewhat nice heading for the data, using the title, date and author. We then make a unnamed Svelte component with svelte:component. Now, remember, the content from Markdown file is what we see in the next code block. Functions. It can’t be rendered with a \ tag. Therefore we use the svelte:component.
{ render: [Function: render], '$$render': [Function: $$render] }
2.5.5 - The blog view
The moment of truth 🙏, we’re now gonna check if the the posts are rendered out again. As apposed to last time, we’re not getting theMarkdown file itself rendered out, but rather the content itself rendered through a Svelte component, that we can style (please don’t be sad of the following output 😅).
There we have it. The content, now rendered out again. Wohooo 🥳💑.
It looks like shit though. The headings aren’t the right size, and the unordered listed items are in an unordered list. We’re gonna fix that. 😍
2.6 - Styling with Tailwind CSS
As said earlier, Tailwind by default doesn’t play well with Vanilla HTML we don’t control. Like, I control this Markdown, but after mdsvex converts it to HTML, this HTML is not targetable with the regular Tailwind CSS classes, as I have not written it myself and can’t say this part here should be styled like this, and so on.
Tailwind CSS have solved that with a plugin called 👉Tailwind Typography. With it, we can style these HTML elements outside our control.
2.6.1 - Installing Typography and updating the Tailwind config.
Adding Typography to our project is super simple.
First, we install it as a dev-dependency with npm:
npm install -D @tailwindcss/typography
Second, we update the Tailwind config (in your root folder), requiring the plugin to be defined in the plugins array.
// File: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/typography')]
};
And that is it, we now should be able to use Tailwind Typography classes in our code. 👀
2.6.2 - Styling Markdown
We revisit the +page.svelte file for the blog posts. We’re going to lightly style the content.
<script lang="ts">export let data;
</script>
<article class="w-10/12 max-w-prose">
<h1 class="mb-1 text-center text-4xl">{data.title}</h1>
<p class="mb-2 text-center text-xl">Published: {data.date}, by {data.author}</p>
<div class="prose lg:prose-lg prose-headings:text-cyan-800">
<svelte:component this={data.content} />
</div>
</article>
There aren’t many changes required for a big change. We add the prose class to the div surrounding the svelte:component. This prose class is required for the Typography plugin. Other classes must be used along with this class. On the same div , I modify the color of the heading texts, with the prose-headings:text-cyan-800 class. Then I say the that all the content should be larger on larger screens with lg:prose-lg. For the article tag, I add a max-width for the content.
That is how Tailwind Typography works. You can modify the element groups, such as h1 headings, h2 headings, and so on. You can not (to my low level understanding) style one h1-heading one way, and another h1-heading another way. You modify them as a group, cause there is no way for us to target any of the individually.
If you’re curious about more styling choise for Typography, more modifiers can be found at the 👉Tailwind Typography Github.
2.6.3 - The blog view with Markdown content styled
The final render before we ender looks a lot better, juuust you wait!
Okay, it’s a bit jank, but heeeey we got headings rendered in different sizes and an unordered list listed unorderly 🙇🙇🙇.
That is it for now, I won’t style it more at this point, but in the future this is sooo gonna look super good. You may be reading this at a point, where the code blocks looks fairly boring, with no special code and keyword highlithging. I did not find a way to style these blocks with Tailwind Typography. However, mdsvex lets you add highlighters in its config, that can then target these code blocks. Expect a post on that.
2.7 - There we have it - You’re done (I hope)
You did it! 👐 (I hope!). You’ve done a lot, and I hoped you’ve learned something. Let’s go through it. 😀
- You either had, or created a Sveltekit project with TypeScript and Tailwind.
- You installed mdsvex, configured it to the behaviour you wanted.
- You updated the Svelte config to take use of mdsvex configs and to tell it to the mdsvex preprocessor.
- Then you maybe chose to render out the contents of a test Markdown file.
- Next we started the journey to generalize the presentation of Markdown files.
- In this generalizing, you rendered out a list of all the Markdown files, by finding the Markdown files, doing some operations on it and presenting its metadata.
- After that, you made a page to render the Markdown content, maybe by using Routes with Dynamics Parameters.
- Lastly you then styled that page, using the Tailwind CSS Typography plugin.
Now you’re left with a fairly robust system to present your writings with. Have a lot of fun with it!.
If you want to read my reflections on what I personally learnt and what I had misunderstood, read the last section. If not, farewell, and have a nice day 😊.
3 - Learning and mistakes
This section is meant to highlight what I learned, and what I got wrong. The thought behind it is to have a place to reflect on just this, and it might be nice for others to see where it went wrong. Maybe some of you encountered the mistakes and me, and get to get on with building your blog. 😘
3.1 - What I learned
Alot of this was new to me, all the stuff listed in 👉[2.7].
Which is pretty exciting looking back. It felt kinda hard starting out. I had not touched this website for months, and the JavaScript/TypeScript code was awkward to write at first (TypeScript very much so (see later section)). We carried on tho. As it went a long, I got more comfortable writing the code, and I started thinking about ways to improve the code. Saving that for later tho, I am not updating the code here. I don’t wanna do double the work rewriting this🤪.
Further on, I got to learn more about Markdown, it’s conventions, and the ecosystem around it. Especially 👉Remark and 👉Rehype seem to be interesting project additions to explore. And then the Markdown itself. I’ve written alot in this blog post (I’m sorry, I said I was gonna keep it minimal😇), and it feels very natural to write Markdown at this point.
Of course, mdsvex, the champion of this endavour. I’ve not learnt a ton about this library, but enough to get a blog up and running, based on Markdown files. I want to go through it’s docs, and see what else I can incorporate (other than Remark and Rehype stuff).
Then I got to learn more about Tailwind and it’s capabilities surrounding Markdown or other “uncontrolled” HTML, with Typography.
In Svelte land I now feel more comfortable writing components, and I got to use the Routing with Dynamics Parameters in a real project for the firs time. It is an enjoyable experience. Lastly I want to mention I also learned about the features that the Node runtime, and the Vite build tool has. I’ve not written “backend” JavaScript before, so diving into the ecosystem around it, and the capabilities, was great!.
3.2 - What I got wrong or missed
In this section, I want to go through what I did wrong and what I misunderstood.
3.2.1 - Tailwind rendering behavior
The first point, as I’ve mentioned earlier, is that Tailwind doesn’t render “uncontrolled” HTML. You won’t believe the time I spent trying to figure it out. I thought it was something I had done wrong, so I went over again and again, and tried to style it myself. But then I randomly went to the Tailwind docs again, and found it pretty fast, that it was expected behavoir. 😅
3.2.2 - Skill issues and reading comprehension
My biggest gripe with my programming looking back, is how little I actually take advantage of TypeScript and it’s typing. All Svelte objects are typed in TypeScript, but I just ignore them, and go back to writing JavaScript. This also leads to me doing hacky ignores in certain sections of the code, to get passed the TypeScript linting rules and transpiler. I could have had tried to change the code instead.
Another gripe is how much of data I would want to have in an array, often is returned as an object. Often by libraries, or by myself. 🙈 I just feel having “iterable” data in an array just fits my mental model better.
Now for some proper time wasters.
My biggest time waste, was down to me not reading error messages properly. I was trying to use the Vite import functions for the Markdown files. However I was failing again and again. The error message said something about a String, and I thought like “Yeah?? Isn’t that what I am passing to this method??“.
What I was doing was to first try String Interpolation. I sat there, passing in arguments in strings. But it wouldn’t change anything. Then I thought, I could concatenate. This was a bust as well. I think maybe over 1-2 hrs has passed, before I read the error message thoroughly again. After 2-3 reads something clicks. The string itself must be “pure”. It must be a one string, with the path to the file(s), unaltered, and unchangeable, a String Literal.
Lastly, I struggled some with the Routing with dynamic paramters. At one point I was confused as to why the links to the blog posts were not taking me where I wanted. I thought it was a mistake I did in the code in it’s +page.ts or +page.svelte. Some noticable time span later, I found the error in the code for formatting the Frontmatter for the Markdown files. I had fucked up the path formatting for the path to the Markdown files, so it still had the file.extension. 😡
3.3 - It’s okay
So why did I wanna write these last parts? Well, to reflect on what I did wrong, but also to tell myself that it is okay. It is okay to not get it working first try. You will spend time figuring out how things work. That’s how we learn. You can’t have true progress without adversity.
So that is it. See you in another post, or somewhere else. 👋