- Project Overview
- Directory Breakdown
- How It Works
- Creating Content
- Code Highlighting & Custom Components
- Tips for Maintenance
This project is a static portfolio and blog built with Nuxt 3 and @nuxt/content v2. Content (articles, snippets, work) lives as Markdown in the content/ directory. The site is generated as static files for GitHub Pages.
nuxt.config.ts– Nuxt config: app head, static preset, Content module, Google Fonts, SCSS, Prism theme CSS.package.json– Dependencies (Nuxt 3, Vue 3, @nuxt/content, prismjs, prism-themes, etc.) and scripts.app.vue– Root component with<NuxtLayout>and<NuxtPage>.app.html– Custom HTML template (meta, GA, Statcounter).error.vue– Root error page (Nuxt 3 convention).
- images/ – Source images; copied to
public/imagesbynpm run copy:assets(or duringgenerate). - scss/ – SCSS source:
01-config(variables, functions, mixins),02-base,03-components,04-utilities. Content typography lives in_nuxt-content.scssand targets the.nuxt-contentclass.
- content/ – Components used inside markdown rendered by Content:
- MarkdownImage.vue – Renders images with
srcunder/images/. In content use<MarkdownImg>or<MarkdownImage>(both mapped inuseContentComponents()). - ProseCode.vue – Custom code blocks using Prism.js (replaces default Shiki). Wraps in
.nuxt-content-highlight.
- MarkdownImage.vue – Renders images with
- Pagination.vue – Prev/next links; receives
basePath,prev,next,type. - Other UI components (e.g. NavBar, Intro, ContactCopy) are auto-imported.
- articles/ – Blog posts (Markdown + frontmatter).
- snippets/ – Code snippets / short tips.
- work/ – Portfolio / case studies.
Slugs are derived from the filename (Nuxt Content v2 uses _path; the app normalizes with utils/contentSlug.ts). Tags with special characters (e.g. A/B Testing) are encoded in URLs and decoded on the tag pages.
- default.vue – Main layout with
<slot />.
- File-based routing. Dynamic routes use bracket syntax:
[slug].vue,[tag].vue. - blog/ – Index,
[slug],tag/[tag]. - snippets/ – Index,
[slug],tag/[tag]. - work/ – Index,
[slug]. - Other pages: index, about, contact.
- drift.client.js – Client-only (e.g. chat widget). Registered with
defineNuxtPlugin.
- Static assets served as-is:
favicon.ico,js/, images/ (populated fromassets/imagesviacopy:assets).
- contentSlug.ts –
getContentSlug(),withSlug(),withSlugOne()to deriveslugfrom Content’s_path. - composables/useContentComponents.ts – Maps
MarkdownImg/MarkdownImagefor<ContentRenderer :components="...">. - Other helpers (e.g. driftBot, vhHack, copyToClipboard).
- Content: Markdown in
content/is queried withqueryContent()insideuseAsyncData. List pages use.find(); detail pages use.findOne(slug)and.findSurround(path)for prev/next. - Rendering:
<ContentRenderer :value="document" :components="contentComponents" />renders the body. The wrapper div must have classnuxt-contentso_nuxt-content.scssapplies (blog, snippets, and work pages add this class). - Slug: Coming from the file path, slug is normalized via
withSlug/withSlugOneso links and routes use a single slug value. - Tags: Tag links use
encodeURIComponent(tag); tag pages usedecodeURIComponent(route.params.tag)so tags likeA/B Testingwork.
- Add a Markdown file under
content/articles/,content/snippets/, orcontent/work/(e.g.my-post.md). - Add frontmatter (YAML at the top):
title,description,createdAt,publish,tags, etc. See existing files for examples. - Write the body in Markdown. Use
<MarkdownImg src="path/under/images" alt="..." />for images (resolved as/images/...). - The URL slug is the filename without extension. The post appears in the listing and at
/blog/my-post(or/snippets/...,/work/...).
- Prism: Built-in Content highlighting is disabled (
content.highlight: false). A customProseCodecomponent incomponents/content/ProseCode.vueuses Prism.js and the prism-material-oceanic theme (innuxt.config.tscss). - Custom components in markdown: Pass
contentComponentsfromuseContentComponents()toContentRendererso tags like<MarkdownImg>resolve. Seecomposables/useContentComponents.ts.
- Keep content in the correct folder (articles, snippets, work) and use consistent frontmatter.
- Add new Prism languages in
components/content/ProseCode.vueif you need more than the default set (js, ts, css, html, bash, json, yaml). - After adding images under
assets/images, runnpm run copy:assets(ornpm run generate) so they appear inpublic/images. - Use Node 18+ (see README). For migration history, see MIGRATION.md.
Happy tinkering! 🚀