Create a Headings Component for an Astro Project with Svelte

Display headings for each markdown post as links.

Dec 22, 2022

Ross Robino

Markdown headings

Heading tags are the standard way to structure markdown content. Astro makes it simple to to parse markdown out of the box, and handles mdx content with a first party mdx integration.

I enjoy using Svelte to create my components, Astro provides another first party Svelte integration to work with Svelte.

Here’s how I created a component to parse all of the headings from a file or a list of headings and display them as links (see above).

Headings component

First, we will create a Headings.svelte component. We will want to use this in two different places, so we will build it with that flexibility in mind. We can pass in either an imported file, or just the headings from the file in a layout.

Each .md or .mdx file can be imported with a built in Astro method which we will implement in the next section. Imported files contain a variety of data points, we can utlize these two:

  • url contains the path to file
  • headings contains a list of heading objects

Each heading object contained in headings has the following keys among others that we can reference:

  • depth displays the level of the heading, <h2> would have a depth of 2
  • slug contains the automatically generated id which we will utilize in our link
<!-- Headings.svelte -->

	// props - accepts entire file or just the headings
	export let headings = [];
	export let file = {};

	// check if the file has any keys
	const isFile = Boolean(Object.keys(file).length);

	// if the file has keys, use the headings from the file instead
	// of the headings prop
	if (isFile) headings = file.getHeadings();

<!-- iterate through each heading -->
{#each headings as heading}
			prefix each heading with a # and indent accordingly 
			with css classes depending on the heading.depth
			class:ml-3={heading.depth == 3}
			class:ml-6={heading.depth == 4}
			class:ml-9={heading.depth == 5}
			class:ml-12={heading.depth == 6}
			Check if we are passing in a file or the headings:

			If we are passing in a file, this means we are referencing the
			files from another page, so we will link to the full path.

			If we are passing in just the headings, we can link to the 
			#id so the link will jump to the right spot on the page.
		<a href={isFile ? `${file.url}#${heading.slug}` : `#${heading.slug}`}>

	div {
		display: flex;
		margin-bottom: 0.5rem;
	span {
		margin-left: 0.25rem;
	.ml-3 {
		margin-left: 0.75rem;
	.ml-6 {
		margin-left: 1.5rem;
	.ml-9 {
		margin-left: 2.25rem;
	.ml-12 {
		margin-left: 3rem;

Try it out

We can now use this in our markdown layouts by providing headings, or in another page by passing in the entire file through the file prop.

Markdown layout

Here, we can create a layout for posts to display the headings from our markdown file.

// @layouts/post/Post.astro

import Headings from "@components/Headings.svelte";

const { headings } = Astro.props;

<Headings headings={headings} />

<hr />

<slot />

Utilize this layout in our markdown file in src/pages/posts/:

layout: "@layouts/post/Post.astro"

# Heading 1

## Heading 2

### Heading 3

Import in other pages

You may want to display your headings as a preview outside of the page itself (example).

We can import our files from src/pages/posts into the page we want to display them using Astro’s glob method. Then we can pass a post into the Headings component to display the headings from that post.

// src/pages/index.astro

import Headings from "@components/Headings.svelte";

const posts = await Astro.glob("./posts/*");

<!-- pass the first post into the component -->
<Headings file={post[0]} />


Because Astro runs at build time, this Headings component will be sent to the client as HTML and CSS with no JavaScript.

I’ve really enjoyed using Astro to create this blog. The first party .mdx integration and Astro.glob make it easy to maintain my content. I’d recommend it to others creating static sites and wanting to author their pages in markdown.

Thanks for reading!

Find an error in this post? Any updates are appreciated. Edit this post