Code blocks & syntax highlighting with Expressive Code
Frame titles, copy buttons, line markers, diffs, terminal frames, word wrap and collapsible sections — every Expressive Code feature this theme exposes.
Every fenced code block on this site is rendered at build time by Expressive Code, powered by Shiki. That means server-rendered, accessible, copy-pasteable HTML with no client-side highlighter — only a tiny script for the copy button.
The integration is configured in
astro.config.mjs with two themes:
github-light for chirpy-light, and github-dark-dimmed for
chirpy-dark. Switching the site theme instantly flips the code
palette too.
Three backticks plus a language identifier:
function greet(name: string) { return `Hello, ${name}!`;}If you want to write raw HTML inside a markdown code block and have it rendered directly instead of highlighted as code, use the ashtml language identifier. A custom remark plugin converts these blocks into raw HTML nodes at build time.
```ashtml<div class="alert alert-success"> <span>This is raw HTML rendered beautifully!</span></div>```Renders as:
If you want daisyUI alert components directly from Markdown/MDX, use the
alert language identifier. A dedicated remark plugin converts each block
into daisyUI-compatible alert markup.
```alerttype: successstyle: softicon: lucide:check-circletitle: Purchase confirmed!description: Your order has been placed successfully.```For the complete matrix of variants (type, style, direction,
icon, title, description, class), see
Alert plugin: showcase of all variants.
Add title="..." to label the block — it shows in a window-style
title bar:
function greet(name: string) { return `Hello, ${name}!`;}Use frame="terminal" for shell snippets — Expressive Code switches
to a terminal-style chrome:
bun installbun run devTo force the code frame on something Expressive Code might auto-detect as a terminal:
echo "this is treated as a code file, not a terminal"Highlight, mark, insert, or delete specific lines.
import { defineCollection, z } from 'astro:content';
export const collections = { posts: defineCollection({ schema: z.object({ title: z.string() }) }),};import { defineCollection, z } from 'astro:content';
export const collections = { posts: defineCollection({ schema: z.object({ title: z.string() }), }),};const old = 'remove me';const stale = 'and me too';const keeper = 'survive';Highlight every occurrence of a literal:
import { useTranslations } from '../i18n/utils';
const t = useTranslations(locale);const heading = t('post.comments');The diff language is supported natively. Lines starting with + and
- are coloured automatically.
const heroSrc = typeof img === 'string' ? img : img?.src;const heroSrc = heroImageSrc(post);const showHero = shouldShowHero(post);Long files? Collapse parts of the snippet so readers focus on the
relevant bit. Use collapse={start-end}:
import { getCollection, type CollectionEntry } from 'astro:content';import { SITE, type Locale } from '../config';
// helper utilities live abovefunction detectLocale(id: string): Locale { return id.startsWith('fr/') ? 'fr' : 'en';}
export async function getPosts(locale: Locale) { const all = await getCollection('posts'); return all.filter((p) => detectLocale(p.id) === locale);}Click the collapsed lines indicator to expand the hidden block.
Long lines are normally horizontal-scrolled. To wrap them instead, use
the wrap modifier:
const veryLongValue = 'a string that would normally cause a horizontal scrollbar but is now soft-wrapped onto multiple lines for narrow viewports';Add preserveIndent to keep wrapped lines aligned with the opening
indentation, or set it once globally in the
expressiveCode block of astro.config.mjs.
Every block has a copy-to-clipboard button in its top-right corner.
The icon switches to a check mark when the copy succeeds, then resets
after a couple of seconds. Translation strings are read from
src/i18n/ui.ts (code.copy, code.copied) so the
French build shows a French label.
Expressive Code does not show line numbers by default — Chirpy doesn’t
either. If you want them, add the
@expressive-code/plugin-line-numbers
plugin to astro.config.mjs and enable it per-block with showLineNumbers.
A realistic block uses several modifiers at once:
import type { CollectionEntry } from 'astro:content';import type { Locale } from '../config';
export function buildSeo(entry: CollectionEntry<'posts'>, locale: Locale) { const canonical = entry.data.canonicalURL ?? defaultCanonical(entry, locale); const ogImage = entry.data.heroImage ?? ogDefaultImg.src; const lang = entry.data.lang ?? locale; return { canonical, ogImage, lang };}Open
astro.config.mjs
and edit the expressiveCode options to swap themes:
expressiveCode({ themes: ['github-light', 'github-dark-dimmed'], themeCssSelector: (theme) => theme.name === 'github-light' ? '[data-theme="chirpy-light"]' : '[data-theme="chirpy-dark"]', styleOverrides: { borderRadius: '0.5rem' },});Pick any Shiki bundled theme — both themes must be listed for the dual-mode CSS to be emitted.