This post was written during the old blog (Next.js + MDX) era. After migrating to the current static site, all of these features happen at build time: TradingView widgets became ticker cards that only load the live chart when you click "Live chart"; ECharts blocks became static SVG charts with hover values; source embeds are pinned commit snippets with line numbers; and language icons became corner language labels on code blocks.

This update pushes the blog's writing experience another step forward: code blocks are easier to scan, code references link straight to GitHub, and financial posts can embed TradingView charts with a tiny shortcode.

What's New

Language Icons on Code Blocks

A language logo and copy button float in the top-right corner of every code block—no title bar, no badge border, and no extra vertical space. When a block has a title or comes from GitHub, a small path line appears above the code and the GitHub link sits tucked into a small icon in the top-right corner.

const feature = 'language icons'
const theme = 'catppuccin'
console.log(feature, theme)
ts

Restyled Inline Code

Inline code in prose like git log --follow, $AAPL, and ::tv AAPL no longer sticks out like a bright patch. In light mode it blends closer to Catppuccin Latte's paper tone, and in dark mode it settles into a low-luminance Mocha surface.

Quoting GitHub Code Directly

When writing a post, enter:

::github-code repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="contentlayer.config.ts" lines="251-253" lang="ts" title="contentlayer.config.ts"
md

It renders as a code block with Shiki syntax highlighting, a language icon, and a direct link to GitHub:

contentlayer.config.tsL251–253 · c067042 ↗
251const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
252const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
253const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/

Line-by-Line Diff Highlighting Like GitHub

When writing a post, enter:

::github-diff repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="css/prism.css" lines="1-15"
md

Rendered output:

Changes to css/prism.css in c067042

@@ -8,6 +8,44 @@ figure[data-rehype-pretty-code-figure] [data-rehype-pretty-code-title] {
   @apply flex items-center justify-between gap-3 border-b border-slate-200 bg-white/70 px-4 py-2 text-xs text-slate-600 dark:border-[#405064] dark:bg-white/[0.035] dark:text-white/65;
 }
 
+figure[data-rehype-pretty-code-figure] .code-title-main {
+  @apply inline-flex min-w-0 items-center gap-2;
+}
+
+figure[data-rehype-pretty-code-figure] .code-language-icon {
+  @apply inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-[5px] border border-slate-300 bg-slate-100 px-1 font-sans text-[0.62rem] leading-none font-semibold text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-white/70;
+}
+
+figure[data-rehype-pretty-code-figure] .code-language-icon[data-code-language='ts'],
+figure[data-rehype-pretty-code-figure] .code-language-icon[data-code-language='tsx'] {
+  @apply border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-400/25 dark:bg-sky-400/10 dark:text-sky-200;
diff

Single-Line Tickers Become Mini Charts

Put a ticker on its own line:

$AAPL
$BINANCE:BTCUSDT.P
md

On the old site, each of these lines turned into a Mini Chart. The current static site doesn't load TradingView scripts upfront—instead, it renders a ticker card linking to TradingView:

NASDAQ:AAPLApple Inc.TradingView ↗
BINANCE:BTCUSDT.PBitcoin / USDT PerpetualTradingView ↗

Shortcode for Advanced Charts

When writing a post, enter:

::tv AAPL interval=60 height=460
md

On the old site this rendered an interactive Advanced Chart; here it is likewise a ticker card.

Rendering ECharts Directly from Code Blocks

Use an ```echarts code block whose body is the chart's option JSON, with title and height supported in the meta string:

```echarts title="Weekly Visits" height=320
{
  "tooltip": { "trigger": "axis" },
  "xAxis": {
    "type": "category",
    "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
  },
  "yAxis": { "type": "value" },
  "series": [
    {
      "name": "Visits",
      "type": "line",
      "smooth": true,
      "areaStyle": {},
      "data": [120, 200, 150, 80, 70, 110, 130]
    }
  ]
}
```
md

On the old site these were two ECharts instances. The current site draws the same data as static SVG at build time; since there is no pie chart type, the second example is represented as a data table:

Weekly Visits
050100150200TueThuSat200 · Tue
Posts by Category
CategoryPostsShare
Tinkering4040%
Dev3232%
Notes1818%
Other1010%

If the JSON is invalid, it gracefully falls back to a regular code block instead of breaking the build. When you need functions or other options that plain JSON cannot express, you can also pass a JS object directly to the <ECharts> component. Charts follow the site's light/dark theme, and the echarts library is lazy-loaded on demand so it stays out of the initial bundle.

How It Works Under the Hood

Turning Shortcodes into Components in Remark

These regular expressions match the ticker chart syntax. $AAPL becomes a Mini Chart and ::tv AAPL becomes an Advanced Chart.

contentlayer.config.tsL251–253 · c067042 ↗
251const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
252const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
253const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/

The Mini Chart transform is intentionally conservative: it only triggers when an entire paragraph consists of a single ticker, so mentioning $AAPL in the middle of a sentence won't drop a giant chart into the paragraph.

contentlayer.config.tsL288–295 · c067042 ↗
288function createTradingViewMiniNode(symbol: string): MdastNode | null {
289  if (!isTradingViewTicker(symbol)) {
290    return null
291  }
292
293  return createMdxFlowNode('TradingViewMiniChart', {
294    symbol: normalizeTradingViewSymbol(symbol),
295  })

Advanced Chart adds parameter parsing so attributes like height=460 and interval=60 are forwarded as JSX props.

contentlayer.config.tsL298–313 · c067042 ↗
298function createTradingViewAdvancedNode(source: string): MdastNode | null {
299  const [rawSymbol, ...attrParts] = source.trim().split(/\s+/)
300
301  if (!rawSymbol || !isTradingViewTicker(rawSymbol)) {
302    return null
303  }
304
305  const attrs = parseEmbedAttributes(attrParts.join(' ')) as TradingViewAttrs
306
307  return createMdxFlowNode('TradingViewAdvancedChart', {
308    height: attrs.height,
309    interval: attrs.interval,
310    locale: attrs.locale,
311    symbol: normalizeTradingViewSymbol(rawSymbol),
312    timezone: attrs.timezone,
313  })

Finally, the plugins are wired into Contentlayer's remark pipeline. Because shortcodes are transformed before MDX compilation, no string scanning happens in the browser.

contentlayer.config.tsL1063–1069 · c067042 ↗
1063export default makeSource({
1064  contentDirPath: 'data',
1065  documentTypes: [Blog, Authors],
1066  mdx: {
1067    cwd: process.cwd(),
1068    remarkPlugins: [remarkGfm, remarkTradingViewWidgets, remarkIconShortcodes, remarkGitHubEmbeds],
1069    rehypePlugins: [

Loading TradingView Only on the Client

The official TradingView widget requires injecting an external <script>, so the component stays a client component. On render, it clears the container and inserts the widget mount node and config script; whenever the theme or props change, it rebuilds the widget.

components/TradingViewWidgets.tsxL83–101 · c067042 ↗
83useEffect(() => {
84  const container = containerRef.current
85
86  if (!container) {
87    return
88  }
89
90  container.innerHTML = '<div class="tradingview-widget-container__widget"></div>'
91
92  const script = document.createElement('script')
93  script.async = true
94  script.src = scriptSrc
95  script.text = JSON.stringify(config)
96  container.appendChild(script)
97
98  return () => {
99    container.innerHTML = ''
100  }
101}, [config, scriptSrc])

Ticker symbols are normalized first. Bare AAPL defaults to NASDAQ:AAPL, while an explicit symbol like NYSE:IBM is left untouched.

lib/tradingview.tsL4–20 · c067042 ↗
4export function normalizeTradingViewSymbol(value: string, defaultExchange = 'NASDAQ') {
5  const symbol = value.trim().replace(/^\$/, '').toUpperCase()
6
7  if (!symbol) {
8    return ''
9  }
10
11  if (explicitTradingViewSymbolPattern.test(symbol)) {
12    return symbol
13  }
14
15  if (plainTickerPattern.test(symbol)) {
16    return `${defaultExchange}:${symbol}`
17  }
18
19  return symbol
20}

Enhancing Code Block Title Bars in Rehype

Language icons aren't written by hand in each post; they're injected across the board during the rehype pass. It locates the figure generated by Shiki and ensures a title bar exists.

contentlayer.config.tsL850–868 · c067042 ↗
850function enhanceCodeTitle(node: HastNode) {
851  if (
852    node.tagName !== 'figure' ||
853    node.properties?.['data-rehype-pretty-code-figure'] === undefined
854  ) {
855    return
856  }
857
858  const title = ensureCodeTitle(node)
859  const code = findDescendantElement(node, 'code')
860  const sourceUrl = typeof code?.data?.githubSourceUrl === 'string' ? code.data.githubSourceUrl : ''
861  const language = getCodeLanguage(code)
862  const titleText = getNodeText(title) || getLanguageName(language)
863
864  title.children = [createCodeTitleNode(titleText, language)]
865
866  if (sourceUrl) {
867    title.children.push(createCodeSourceLinkNode(sourceUrl, language))
868  }

The left side of the title bar pairs the language icon with the title text. When no explicit title is provided, it falls back to the language display name—such as TypeScript, Diff, or Markdown.

contentlayer.config.tsL787–799 · c067042 ↗
787function createCodeTitleNode(titleText: string, language: string): HastNode {
788  return {
789    type: 'element',
790    tagName: 'span',
791    properties: { className: ['code-title-main'] },
792    children: [
793      createCodeLanguageIconNode(language),
794      {
795        type: 'element',
796        tagName: 'span',
797        properties: { className: ['code-title-text'] },
798        children: [{ type: 'text', value: titleText || getLanguageName(language) }],
799      },

Visually, the logos come straight from simple-icons brand icons, tree-shaken on demand so they don't bloat first-load JavaScript; languages without a brand icon fall back to a short text badge.

css/prism.cssL11–17 · c067042 ↗
11figure[data-rehype-pretty-code-figure] .code-title-main {
12  @apply inline-flex min-w-0 items-center gap-2;
13}
14
15figure[data-rehype-pretty-code-figure] .code-language-icon {
16  @apply inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-[5px] border border-slate-300 bg-slate-100 px-1 font-sans text-[0.62rem] leading-none font-semibold text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-white/70;
17}

Keeping Inline Code from Stealing Attention

Inline code now uses a low-contrast, subtly bordered token style. It's still unmistakably code, without popping out of the paragraph like a button.

css/tailwind.cssL110–124 · c067042 ↗
110& :where(code):not(pre code) {
111  border: 1px solid color-mix(in oklab, var(--color-slate-300) 72%, transparent);
112  border-radius: 6px;
113  background: linear-gradient(
114    to bottom,
115    color-mix(in oklab, white 92%, var(--color-slate-100)),
116    color-mix(in oklab, var(--color-slate-100) 88%, white)
117  );
118  box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--color-slate-300) 45%, transparent);
119  color: #4c4f69;
120  padding: 0.08rem 0.34rem;
121  font-size: 0.86em;
122  font-weight: 500;
123  line-height: 1.75;
124  word-break: break-word;

GitHub Code and Diffs Still Go Through Shiki

At build time, ::github-code and ::github-diff fetch the real source file or diff and emit a standard code AST node. That means they don't need a custom renderer—Shiki highlights them just like any other code block.

contentlayer.config.tsL543–552 · c067042 ↗
543if (kind === 'code') {
544  const value = selectCodeLines(await getGitHubFile(attrs), attrs.lines)
545  const title =
546    attrs.title ||
547    `${normalizeGitHubRepo(attrs.repo)}:${attrs.path}${attrs.lines ? `#L${attrs.lines}` : ''}`
548
549  return createCodeNode(value, attrs.lang || 'text', title, {
550    showLineNumbers: true,
551    sourceUrl: getGitHubCodeUrl(attrs),
552  })

Diffs follow the exact same path, except the data source is a patch and the language defaults to diff.

contentlayer.config.tsL555–563 · c067042 ↗
555const value = selectCodeLines(await getGitHubDiff(attrs), attrs.lines)
556const title =
557  attrs.title ||
558  `${normalizeGitHubRepo(attrs.repo)}:${attrs.path || `${attrs.base || attrs.ref}...${attrs.head || ''}`}`
559
560return createCodeNode(value || 'No diff matched this query.', attrs.lang || 'diff', title, {
561  showLineNumbers: true,
562  sourceUrl: getGitHubDiffUrl(attrs),
563})

That's why GitHub embeds get the Shiki color theme, language icon, copy button, GitHub link, and diff line backgrounds all at once.

The Mental Model for Writing Posts

For normal posts, you just write Markdown. When you want an icon, write :icon-code:; for a market ticker, write $AAPL or ::tv AAPL; for a data chart, write an ```echarts block; to quote source code, write ::github-code; and to quote a commit diff, write ::github-diff.

The build step turns these shortcodes into stable MDX components or Shiki code blocks, while the runtime only handles interactivity and external widgets. The authoring syntax stays brief, and the rendered page stays predictable.