Upgrading Astro Code Snippets — NamcheeBack to Posts
Upgrading Astro Code Snippets
16 minutes read
Markdown is commonly used to store textual contents when building a site with Astro. During the build process, Astro will take
all markdowns transform them into its HTML equivalent. Users can then style the markups with CSS however they wish.
However, customization through CSS is pretty limited as customizing the actual markup or behavior is out of the table. To allow
greater degree of customization, users can opt-in to MDX.
MDX allows users to utilize custom components with markup-like syntax similar to how we use those components in Astro files or JSX.
However, this is impractical when users want to replace default elements as they
are forced to use JSX-like syntax for every element and lose the simplicity of defining elements in markdown.
Fortunately, MDX also allows users to re-map HTML elements into custom components
by exporting component map in MDX files as components constant.
Alternatively, users can also provide the mapped components by passing them as a prop to the <Content /> element
After seeing the syntax, you might be wondering: how do I manage the props? What attributes are passed to the mapped element?
Can I apply a pre-defined CSS classes to mapped elements?
This post will guide you through customizing default markdown elements using the components syntax. We will using the default code snippets
component as the guinea pig and upgrade them into similar code snippets that you see in this site.
I will be using UnoCSS for styling, which is an alternative of TailwindCSS. However, the styling can 100% be implemented with vanilla CSS.
Deconstructing the Code Snippet
Before moving on to the actual customization of the snippet, we need to understand how Astro handles markdown files.
For each markdown file, Astro will parse the content using a processor called remark, which is a part of the
unified toolchain. From the markdown, remark will produce an abstract syntax tree (AST). The AST will
then be transformed into HTML tags using another processor in the same toolchain: rehype.
You might be wondering why we need to transform the markdown into an AST and not directly use something like RegExp.
This is because markdown (and HTML) is not a regular language. Trying to parse non-regular language with regular tends to be error prone.
The functionality of remark and rehype can be augmented using plugins that provides additional features,
such as custom syntax and syntax highlighting.
By default, Astro enables GitHub-flavored markdown and SmartyPants
plugin to extend the basic markdown syntax. For syntax highlighting, Astro uses Shiki by default or
users can opt-in to Prism; both available as Rehype plugins.
The diagram below shows a simplified pipeline of markdown transformation to HTML that occurs during the build process.
When processing code blocks, the process above will output HTML tags similar to the following:
This HTML tags will then be used by Astro to replace code blocks during the markdown
transformation process into an HTML page for:
All code fences (```) defined in Markdown and MDX files.
Content defined with <Code /> or <Prism /> component.
Replacing The Default Code Snippet
Our first step to customize the code snippet is by replacing the default component with
a custom component that wraps the original one using Astro <slot />.
To do that, we can assign a custom mapping for HTML elements by exporting a new constant
called components in our MDX files.
We can then define CodeSnippet as:
Save the file and we will be greeted by this weird element.
So what’s happening here? Why does wrapping the code block in a <figure> element ruins the structure entirely? Well it turns out, we replaced
the entire <pre> element with <figure> as shown in the following snippet.
Well, that’s not what we wanted at all! We don’t want to remove the <pre> entirely as we need to preserve
the preformatted styling. After wrapping the <code> block inside a <pre> element, the previously
butchered element will look like this.
We’ve restored the formatting at this point, but we’re still missing the theming from the original component. Obviously, we can apply the intended
class, style, and other attributes to the <pre> element directly. However, this means that we wasted the theming we’ve done from Astro’s config entirely.
Fortunately, since the custom component is implemented as an Astro component, Astro treats those passed attributes as component props which is
provided by Astro.props globals.
which results in
We did it! We’ve successfully rendered a custom code snippet component without breaking the existing components. We can now begin customizing the component.
Supercharging The Code Snippet
Since we have successfully replaced the default code block with our custom element, it’s time to upgrade our components with the following additional features:
Captions
Line Numbers
Line or Code Highlighting
Show Code Language
Copy to Clipboard Button
Before we implement these new features however, there is one important trick that we should know: passing additional props.
Passing Additional Props to Code Snippet
In the previous section, all we did with the props is passing an already defined props. However, some of our additional properties requires
additional information that Shiki doesn’t provide by default (namely captions and copy to clipboard).
The good news is, Shiki already addresses this issue by giving users the ability to add metadata to code blocks that users can provide
in the opening tag after language definition.
The bad news is, Shiki integration in Astro doesn’t pass them to the element in any form, so trying to access Astro.props to get the extra metadata will yield nothing.
Since Astro doesn’t pass metadata by default, we need to play around with the Shiki Transformer API to create a transformer
that passes the metadata as an attribute. Fortunately, it’s quite simple to do so through shikiConfig that Astro exposes.
With the above config, Shiki will pass the metadata as an HTML property data-meta which can then be accessed normally through
Astro.props.
Since we might be passing more metadata in the future, it’s advisable to define a schema for writing metadata to avoid
problems during parsing in the future.
For the sake of simplicity, we are going to use key1=value1,key2,[...key] schema in this post that can be parsed with:
Now, we can finally move on to customizing the component.
For the sake of clarity, I’ll be changing the default theme of Shiki to min-light which you can change via Astro config.
Adding Captions
To add captions to the code block, we can pass the caption as title inside the metadata and embed it on figcaption:
After the changes has been applied, our component should look like this:
Adding Line Numbers
Unfortunately, neither Astro nor Shiki provides us with any APIs regarding line numbers. Therefore, we have to rely to good ol’ CSS
to implement this feature.
Since all lines are actually <span>s that have line as one of its class, we can combine the ::before pseudo-element with CSS Counters:
After applying the CSS, our component should look like this:
Adding Line Highlighting
To add highlights, we need to tell Shiki which line should be highlighted and Shiki needs to transform those lines to be distinguishable from non-highlighted lines. We can
then write a CSS style to mark those highlighted lines.
Fortunately, Shiki has already provided us with transformerMetaHighlight transformer that handles
this use case that we can install through @shikijs/transformers package and enable in our Astro config:
We can then pass meta strings that defines lines that should be highlighted and style those highlighted lines with CSS:
After applying the highlighted CSS, our component should now look like this:
Showing Language
Since Shiki has already passed the language in the data-language prop, we can easily grab it and display it on the top right of the <pre> as such:
The language should be shown as a fixed element on the top right of the code block.
Adding Copy to Clipboard Button
After a series of features that are uninteractive, we are finally moving to the final feature that requires a sprinkle of interactivity: copy to clipboard button.
Before we implement this feature, we need to ensure that the original content is accessible by our component. We can implement the same trick that we use
to pass captions: pass them as an attribute called data-code
To access user’s clipboard, we need to use JavaScript to access the Clipboard API
which is available on the navigator interface through the browser. The functionality
can then be triggered through a button click:
Now, there should be a ‘Copy Code’ text on the top right of the code block. Try clicking the element
and see if the code inside the code block is copied to your clipboard by pasting it somewhere.
Final Thoughts
And there we have it. We have explored the capabilities of Astro API when handling MDX to transform our default code snippet component into a more feature-packed
code snippet without having to directly define them as markups.
Lastly, you can try Astro Expressive Code if you want a feature-packed code snippet without the hassle. However, I believe the
the method shown here still has its merits as outside syntax highlighting, we are in full control of the markup and behavior.