Skip to main content

Code Blocks in Astro

A Code Quick Tip on how to render different kinds of Code Blocks in Astro.

This site is built on Astro, which I originally fell in love with, when working on our team’s Documentation website.

That website is built on a lot of Code snippets of different kinds. Sometimes, we want to show the full JavaScript file, that we’re using in a component, sometimes we want to show just snippets of a component, and sometimes we want to display the full final rendered HTML.

And, same goes for this website, as I’m moving more into writing about code again.

So, I wanted to share how I achieve those different tasks.

Expressive Code

Expressive Code

First off, I absolutely recommend setting up Expressive Code in your Astro project.

I also use the line numbers plugin, which is a separate package.

Terminal window
1
#installs expressive code and line numbers plugin
2
npm i astro-expressive-code @expressive-code/plugin-line-numbers

The Basics, Code Blocks with no relevance to Files.

The Basics, Code Blocks with no relevance to Files.

Obviously, the most basic way to show code in Astro, is to just write it in a code block, in the mdx file where the content lives and define the language after the ```.

1
```js
2
console.log('example');
3
```

which then renders as:

1
console.log('example');

This is all well and good, but what if you want to show one of the files that actually lives in your site and don’t want to copy and paste it into the mdx file?

Full Project Files

Full Project Files

Let’s look at one of my example files from my The Math of Calligraphy Grids article.

I made a file for my DotGrid Example, which looks like this

dot Grid with pattern
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
style="max-width: 100%; border: 1px solid red"
4
viewBox="0 0 219 100"
5
xmlns="http://www.w3.org/2000/svg"
6
>
7
<title>dot Grid with pattern</title>
8
<defs>
9
<pattern
10
id="horizontal-dotted-line"
11
viewBox="0 0 219 0.5"
12
width="100%"
13
height="4%"
14
>
15
<line
16
x1="1.5"
17
y1="0"
18
x2="219"
19
y2="0"
20
stroke="currentColor"
21
stroke-width="0.5"
22
stroke-dasharray="0,4"
23
stroke-linecap="round"
24
></line>
25
</pattern>
26
</defs>
27
<rect
28
x="0"
29
y="0"
30
width="219"
31
height="100"
32
fill="url(#horizontal-dotted-line)"
33
></rect>
34
</svg>

Now, if I wanted to display the code of that, I’d import the Code component as well as the Components raw content and render it like this.

importing and rendering the ExampleDot file
1
import ExampleDotCode from "@components/article-content/svg/ExampleDot.astro?raw";
2
import { Code } from "astro-expressive-code/components";
3
4
<Code code={ExampleDotCode} lang="astro" title="ExampleDot.astro"/>

Which renders as:

ExampleDot.astro
1
---
2
import Example from "./Example.astro";
3
import SVG from "./SVG.astro";
4
5
const docWidth = 219;
6
const docHeight = 100;
7
const cellSize = 4;
8
const strokeColor = "currentColor";
9
const gridYStart = (docHeight % cellSize) / 2;
10
const gridXStart = (docWidth % cellSize) / 2;
11
const dotSize = .5;
12
const patternHeight = (cellSize / docHeight) * 100;
13
---
14
15
<Example>
16
<SVG
17
w={null}
18
dW={docWidth}
19
dH={docHeight}
20
title="dot Grid with pattern"
21
>
22
<defs>
23
<pattern
24
id="horizontal-dotted-line"
25
viewBox={`0 0 ${docWidth} ${dotSize}`}
26
width="100%"
27
height={`${patternHeight}%`}
28
>
29
<line
30
x1={gridXStart}
31
y1={gridYStart}
32
x2={docWidth}
33
y2={gridYStart}
34
stroke={strokeColor}
35
stroke-width={dotSize}
36
stroke-dasharray={`0,${cellSize}`}
37
stroke-linecap="round"
38
></line>
39
</pattern>
40
</defs>
41
<rect
42
x="0"
43
y="0"
44
width={docWidth}
45
height={docHeight}
46
fill="url(#horizontal-dotted-line)"
47
></rect>
48
</SVG>
49
</Example>

Partial Component Files

Partial Component Files

This is great, but what if I just wanted to get a part of that file?

For that, I wrote a new Astro Component, which takes a code string as well as a partial prop.

In that prop, I can specify wheter or not not I want to show the frontmatter, the template, or a specific part of the file.

PartialFile.astro
1
---
2
import { Code } from "astro-expressive-code/components";
3
4
type Partial = 'frontmatter' | 'template' | { lineStart: number; lineEnd: number };
5
6
interface Props {
7
codeTitle?: string;
8
rawFile?: string;
9
lang?: string;
10
partial: Partial;
11
}
12
13
const { codeTitle, rawFile = '', lang, partial} = Astro.props;
14
15
const title = codeTitle ? `${codeTitle}.${lang}` : undefined;
16
const lines = rawFile.split("\n");
17
18
const getCodeString = (lines: string[], partial: Partial) => {
19
if (partial === 'frontmatter') {
20
const start = lines.findIndex((line) => line === '---');
21
const end = lines.findIndex((line, index) => index > start && line === '---') + 1;
22
return lines.slice(start, end).join('\n');
23
}
24
if (partial === 'template') {
25
const start = lines.findIndex((line) => line === '---');
26
const end = lines.findIndex((line, index) => index > start && line === '---');
27
return lines.slice(end + 1).join('\n');
28
}
29
if (typeof partial === 'object') {
30
return lines.slice(partial.lineStart, partial.lineEnd).join('\n');
31
}
32
return lines.join('\n');
33
};
34
35
const codeString = getCodeString(lines, partial);
36
---
37
<Code
38
code={codeString}
39
lang={lang}
40
title={title}
41
/>

So if, I wanted to just show the SVG part of the ExampleDot file, I’d use the PartialFile component like this:

1
<PartialFile codeTitle="frontmatter" rawFile={ExampleDotCode} lang="astro" partial="frontmatter" />
2
3
<PartialFile codeTitle="template" rawFile={ExampleDotCode} lang="astro" partial="template" />
4
5
<PartialFile codeTitle="partial" rawFile={ExampleDotCode} lang="astro" partial={{lineStart: 40, lineEnd: 47}} />

Which renders like this:

frontmatter.astro
1
---
2
import Example from "./Example.astro";
3
import SVG from "./SVG.astro";
4
5
const docWidth = 219;
6
const docHeight = 100;
7
const cellSize = 4;
8
const strokeColor = "currentColor";
9
const gridYStart = (docHeight % cellSize) / 2;
10
const gridXStart = (docWidth % cellSize) / 2;
11
const dotSize = .5;
12
const patternHeight = (cellSize / docHeight) * 100;
13
---
template.astro
1
<Example>
2
<SVG
3
w={null}
4
dW={docWidth}
5
dH={docHeight}
6
title="dot Grid with pattern"
7
>
8
<defs>
9
<pattern
10
id="horizontal-dotted-line"
11
viewBox={`0 0 ${docWidth} ${dotSize}`}
12
width="100%"
13
height={`${patternHeight}%`}
14
>
15
<line
16
x1={gridXStart}
17
y1={gridYStart}
18
x2={docWidth}
19
y2={gridYStart}
20
stroke={strokeColor}
21
stroke-width={dotSize}
22
stroke-dasharray={`0,${cellSize}`}
23
stroke-linecap="round"
24
></line>
25
</pattern>
26
</defs>
27
<rect
28
x="0"
29
y="0"
30
width={docWidth}
31
height={docHeight}
32
fill="url(#horizontal-dotted-line)"
33
></rect>
34
</SVG>
35
</Example>
partial.astro
1
<rect
2
x="0"
3
y="0"
4
width={docWidth}
5
height={docHeight}
6
fill="url(#horizontal-dotted-line)"
7
></rect>

Final rendered HTML

Final rendered HTML

Okay, so this is if we’re passing in files in the original syntax. But what if we want to get the resulting HTML of that template?

This is where the ShowCode component comes in.

Let’s have a look at how the Example file works first though.

If you go back to the preview of the Dot, you see there is a summary with Show SVG Code, which expands the SVG code.

In the Example.astro Component, the Code is actually added twice.

Example.astro.astro
1
---
2
import ShowCode from "@components/ShowCode.astro";
3
---
4
5
<div class="svg-example">
6
<div class="svg-wrap">
7
<slot />
8
</div>
9
<ShowCode
10
codeTitle="rendered-svg"
11
toggleText="Show SVG Code"
12
>
13
<slot />
14
</ShowCode>
15
</div>

So, once is when we render the actual SVG and the second is then the Code we’d like to render.

Since that HTML is rendered and potentially no longer nicely formatted, I personally like to use the prettier package to format the HTML.

We can access the slot content which is rendered and then run prettier over it and then get a code string from it.

Astro may add some script tags or data-astro attributes which we don’t want, so we can use a quick replace to get rid of those.

And that’s all the magic there is to it:

ShowCode.astro
1
---
2
interface Props {
3
codeTitle?: string;
4
lang?: string;
5
toggleText?: string;
6
}
7
8
const { codeTitle = "rendered-example.html", lang = "html", toggleText = "Show Code" } = Astro.props;
9
10
import * as prettier from "prettier";
11
import { Code } from "astro-expressive-code/components";
12
import Icon from "@components/Icon.astro";
13
// note no <slot/> is placed in content, we manually add the formatted code.
14
const defaultSlotContent = await Astro.slots.render("default");
15
// slot content will add script tags and astro-ids if there is custom css, we don't want these added
16
const scriptPattern = /<script[^>]*>[\s\S]*?<\/script>/g;
17
const astroPattern = /(\s*data-astro-cid-[^\s"'`>]+)/g;
18
const modifiedHtmlString = await defaultSlotContent.replace(scriptPattern, "").replace(astroPattern, "");
19
const codeString = await prettier.format(modifiedHtmlString, {
20
htmlWhitespaceSensitivity: "ignore",
21
singleAttributePerLine: true,
22
parser: lang,
23
});
24
---
25
26
<details class="doc-show-code">
27
<summary class="doc-show-code__summary t-mono">
28
{toggleText}
29
<Icon icon="angle-up" />
30
</summary>
31
{
32
Astro.slots.has("default") && (
33
<Code
34
code={codeString}
35
lang={lang}
36
title={`${codeTitle}.${lang}`}
37
/>
38
)
39
}
40
</details>
41
42
<style lang="scss" is:global>
43
.doc-show-code {
44
margin-top: 0.5em;
45
border: 1px solid var(--sys-c-line);
46
transition: 300ms border ease;
47
48
&:hover {
49
border-color: var(--sys-c-interactive);
50
}
51
52
&[open] {
53
.c-icon {
54
transform: rotate(0deg);
55
}
56
}
57
58
summary {
59
padding: 0.2em 0.5em;
60
61
&::marker {
62
content: "";
63
}
64
65
.c-icon {
66
transition: 300ms transform ease;
67
transform: rotate(180deg);
68
}
69
}
70
71
.expressive-code {
72
margin-block: 0;
73
}
74
}
75
</style>

Conclusion

Conclusion

Displaying Code Blocks and keeping them in sync was always a big hassle in previous setups. So this is a huge win for me.

I hope this helps you in your code block endeavours as well, even if you aren’t using Astro, I hope you can take some inspiration from this and apply it to your own setup.

Search

Results will appear as you type