Post ·
The first version of this post was a gallery of code blocks. It showed off rehype-prism-plus, code titles, line highlighting, and a workaround for esbuild on Windows. Every one of those plugins is gone now. Code blocks are simpler, faster and match the site's theme in light and dark mode.
code, language, filename and any highlightedLines.No highlighter ships to the browser. The page arrives already colored.
The post body allows a code block next to text and images. Limiting the language list keeps the Studio dropdown short and matches what the site can highlight:
defineArrayMember({
type: 'code',
options: {
withFilename: true,
languageAlternatives: [
{title: 'TypeScript', value: 'typescript'},
{title: 'TSX', value: 'tsx'},
{title: 'Shell', value: 'sh'},
{title: 'Python', value: 'python'},
// JSON, CSS, HTML, SQL, YAML, Markdown...
],
},
}),Here's a trimmed version of the component. The Studio stores language names like sh and tsx, and sugar-high's lang() helper turns them into its own names (shell, typescript). stegaClean removes the hidden characters Visual Editing adds to strings in preview, which would otherwise end up inside the code. The highlighted lines handle line highlighting, covered below.
export function CodeBlock({ value }: { value: CodeValue }) {
if (!value.code) return null;
const code = stegaClean(value.code);
const language = stegaClean(value.language);
const highlighted = new Set(value.highlightedLines ?? []);
const html = highlight(code, {
lang: language ? lang(language) : undefined,
// sugar-high counts lines from 0; the Studio counts from 1.
markLine(line) {
if (highlighted.has(line.index + 1)) line.className += ' sh__line--highlighted';
},
});
return (
<figure data-slot="code-block">
<figcaption>
{value.filename}
<CopyButton code={code} />
</figcaption>
<pre data-language={language}>
<code dangerouslySetInnerHTML={{ __html: html }} />
</pre>
</figure>
);
}sugar-high reads every color from a CSS custom property. I picked values that match the site's green and blue accents, then defined them again under .dark. Switching themes with next-themes recolors every code block, with no second stylesheet and no JavaScript.
:root {
--sh-class: oklch(52% 0.18 259.8);
--sh-property: oklch(50% 0.14 211.5);
--sh-string: oklch(52% 0.17 151.7);
--sh-keyword: oklch(52% 0.2 340);
--sh-comment: oklch(55.1% 0.027 264.364);
}
.dark {
--sh-class: oklch(70% 0.15 259.8);
--sh-property: oklch(78.9% 0.154 211.53);
--sh-string: oklch(79.2% 0.209 151.711);
--sh-keyword: oklch(75% 0.17 340);
--sh-comment: oklch(55.1% 0.027 264.364);
}Shiki uses the same TextMate grammars as VS Code, so it's the most accurate option, especially for unusual syntax. It also loads a grammar and a theme for every language you use. sugar-high is a small tokenizer with no dependencies. It covers JavaScript, TypeScript and JSX well, supports about thirty other languages, and runs anywhere JavaScript does, including a Server Component. Nearly everything I write about is TypeScript, so that's the right trade for this blog. If I start posting a lot of Rust or Go, I'll look at this again.
npm create sanity@latest -- --template clean --typescript --output-path my-website-cms
npm install next-sanity @sanity/image-url sugar-highA GROQ query from this site. defineQuery lets TypeGen generate the type of the result.
export const COMPANY_JOBS_QUERY = defineQuery(`
*[_type == "company" && slug.current == $company][0]{
_id,
name,
roles[]->{
_id,
title,
accomplishments[]->{
_id,
accomplishment,
"slug": slug.current,
tags[]->{_id, name, "slug": slug.current}
}
}
}
`);An example server component in the same style as this site's: it fetches through the shared cache boundary and renders a list.
export async function LatestPosts({ perspective, stega }: DynamicFetchOptions) {
const { data: posts } = await cachedSanity({ query: POSTS_QUERY, perspective, stega });
return (
<ul className="space-y-4">
{posts.slice(0, 3).map((post) => (
<li key={post._id}>
<Link href={`/blog/${post.slug}`} className="font-semibold hover:underline">
{post.title}
</Link>
</li>
))}
</ul>
);
}The 2021 version of this post had a long Pig Latin script. Here it is again, rewritten with type hints and a regular expression that leaves punctuation alone. The two highlighted lines do the real work.
import re
VOWELS = set("aeiouAEIOU")
def to_pig_latin(word: str) -> str:
"""Translate one word: leading consonants move to the end, then add ay."""
if word[0] in VOWELS:
return f"{word}yay"
for i, letter in enumerate(word):
if letter in VOWELS:
return f"{word[i:]}{word[:i]}ay"
return f"{word}ay"
def translate(sentence: str) -> str:
# Swap each run of letters; spaces and punctuation stay where they are.
return re.sub(r"[A-Za-z]+", lambda match: to_pig_latin(match.group()), sentence)
if __name__ == "__main__":
print(translate(input("Sentence: ")))The Studio's scripts. typegen extracts the schema and regenerates the site's types in one step.
{
"scripts": {
"dev": "sanity dev",
"build": "sanity build",
"deploy": "sanity deploy",
"typegen": "sanity schema extract --enforce-required-fields --force && sanity typegen generate"
}
}Some examples only need you to look at a line or two. In the Studio, clicking a line number in the code input toggles that line, and Sanity saves the numbers in the block's highlightedLines array. There's no extra field to add to the schema.
On the site, sugar-high's markLine option runs once for every line. The renderer above adds a class to each line in that array (sugar-high counts lines from 0, the Studio from 1), and a few lines of CSS turn the class into a tinted bar with a green edge. The highlighted rules below are the ones doing it:
[data-slot='code-block'] code {
display: grid;
width: max-content;
min-width: 100%;
}
[data-slot='code-block'] .sh__line {
min-height: 1lh;
margin-inline: -1.25rem;
padding-inline: 1.25rem;
}
[data-slot='code-block'] .sh__line--highlighted {
background-color: color-mix(in oklab, var(--color-green-500) 12%, transparent);
box-shadow: inset 3px 0 0 var(--color-green-500);
}Making each line a grid row is what lets the bar run the full width of the block, even when a long line scrolls sideways. The negative margin pulls each line into the block's side padding, and min-height: 1lh keeps empty lines from collapsing. Dark mode uses the same rule with a lighter green.