Code Block Component: A Simple but Neat Solution

Just wanted to share a quick look at my solution for a Code Block in Nuxt Content. It's pretty straightforward - it displays code blocks with some nice features like syntax highlighting and a copy button. Most of the heavy lifting is already done by Shiki.

What's cool about Nuxt Content is that we simple can replace the Prose Components with our own. We just have to create a component in components/content to replace it.

What Features where important for me?

Aside from syntax highlighting I wanted to have:

  • a Top Bar that displays the Filepath and a File Icon. This Top Bar should be optional to use.
  • a Copy Button
  • Line Numbers

What's Cool About It?

The component handles different programming languages and shows the right icon for each one. Vue, JavaScript, TypeScript - you name it, it'll display it properly with the correct VSCode-style icon. If it doesn't recognize the language, no worries - it'll just use a default document icon.

The copy functionality is handled by VueUse useClipboard.

components/content/ProseCode.vue
function getLanguageIcon(language: string | null) {
  const iconMap: Record<string, string> = {
    vue: 'vscode-icons:file-type-vue',
    js: 'vscode-icons:file-type-js',
    ts: 'vscode-icons:file-type-typescript',
    html: 'vscode-icons:file-type-html',
    css: 'vscode-icons:file-type-css',
    json: 'vscode-icons:file-type-json',
    md: 'vscode-icons:file-type-markdown',
  }
  return language ? iconMap[language] || 'heroicons:document' : 'heroicons:document'
}

The Styling

Used some simple but effective styling with a dark theme that's easy on the eyes. The line numbers are subtle, and highlighted lines stand out just enough to be noticeable without being distracting. Everything's contained in a nice rounded container that fits well with modern UI designs.

That's It!

Not much else to say - it's a simple component that does its job well. If you want to use it, just drop it into your Nuxt project and you're good to go.

Feel free to grab the code and modify it for your own needs. These kinds of components make documentation and code sharing just a bit nicer, and that's always a good thing.

The Code

nuxt.config.ts
content: {
    highlight: {
      theme: 'catppuccin-mocha',
      langs: ['javascript', 'js', 'typescript', 'ts', 'json', 'bash', 'shell', 'yaml', 'markdown', 'css', 'scss', 'html', 'vue'],
    },
  },
components/content/ProseCode.vue
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'

const props = withDefaults(
  defineProps<{
    code?: string
    language?: string | null
    filename?: string | null
    highlights?: Array<number>
  }>(),
  { code: '', language: null, filename: null, highlights: [] },
)

const { copy, copied } = useClipboard()

function getLanguageIcon(language: string | null) {
  const iconMap: Record<string, string> = {
    vue: 'vscode-icons:file-type-vue',
    js: 'vscode-icons:file-type-js',
    ts: 'vscode-icons:file-type-typescript',
    html: 'vscode-icons:file-type-html',
    css: 'vscode-icons:file-type-css',
    json: 'vscode-icons:file-type-json',
    md: 'vscode-icons:file-type-markdown',
  }
  return language ? iconMap[language] || 'heroicons:document' : 'heroicons:document'
}
</script>

<template>
  <div class="group relative my-4 overflow-hidden rounded-lg bg-octogrey-700">
    <div v-if="filename" class="flex items-center justify-between border-b border-primary-500 bg-octogrey-700 px-4 py-2">
      <div class="flex items-center gap-2 text-sm text-gray-400">
        <Icon :name="getLanguageIcon(language)" class="size-5" />
        <span>{{ filename }}</span>
      </div>
      <button
        class="flex items-center justify-center p-1 text-gray-400 transition-colors hover:text-gray-500"
        :class="{ 'text-green-500 hover:text-green-500': copied }"
        @click="copy(code)"
      >
        <Icon :name="copied ? 'heroicons:check' : 'heroicons:clipboard'" class="size-4.5" />
      </button>
    </div>
    <button
      v-else
      class="absolute right-3 top-3 z-10 flex items-center justify-center rounded-md  bg-octogrey-700 p-1.5 text-gray-400 opacity-0 transition-all hover:text-gray-500 group-hover:opacity-100"
      :class="{ 'text-green-500 hover:text-green-500': copied }"
      @click="copy(code)"
    >
      <Icon :name="copied ? 'heroicons:check' : 'heroicons:clipboard'" class="size-4.5" />
    </button>
    <slot />
  </div>
</template>

<style scoped>
/* These styles can't be converted to Tailwind because they use :slotted */
:slotted(pre) {
  margin: 0;
  padding: 1rem;
  background: transparent;
  overflow-x: auto;
  line-height: 1.5;
  counter-reset: lines;
}

:slotted(pre code) {
  color: #a9b1d6;
  width: 100%;
  display: flex;
  flex-direction: column;
}

:slotted(pre code .line) {
  display: inline-table;
  min-height: 1.25rem;
  padding-right: 1rem;
}

:slotted(pre code .line::before) {
  counter-increment: lines;
  content: counter(lines);
  width: 1rem;
  margin-right: 1.5rem;
  display: inline-block;
  text-align: right;
  color: #565f89;
}

:slotted(pre code .highlight) {
  background: #1e2030;
  display: block;
  margin-right: -1rem;
  margin-left: -1rem;
  padding-right: 1rem;
  padding-left: 0.75rem;
  border-left: 0.25rem solid #7aa2f7;
}
</style>