コンテンツにスキップ
まだあたたかい

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.

Authoring 2 分で読めます

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:

This is raw HTML rendered beautifully!

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.

```alert
type: success
style: soft
icon: lucide:check-circle
title: 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:

src/utils/greet.ts
function greet(name: string) {
return `Hello, ${name}!`;
}

Use frame="terminal" for shell snippets — Expressive Code switches to a terminal-style chrome:

Terminal
bun install
bun run dev

To 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.

Refactor: hero image source
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}:

src/utils/posts.ts
import { getCollection, type CollectionEntry } from 'astro:content';
import { SITE, type Locale } from '../config';
// helper utilities live above
function 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:

src/utils/seo.ts
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:

astro.config.mjs
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.