Nuxt Studio is an open-source, self-hostable Nuxt module that enables visual content editing in production for Nuxt Content websites. Originally a premium hosted platform, it's now a free MIT-licensed module that runs entirely on your own infrastructure.
Key Concept: This module adds a full-featured CMS editor directly into your Nuxt Content application, allowing non-technical users to edit content and commit changes to Git without needing local development tools.
- Visual Editors: TipTap-based Notion-like editor for Markdown with MDC component support, form-based editor for YAML/JSON
- Code Editor: Monaco editor with syntax highlighting for direct file editing
- Real-time Preview: See changes instantly on production website
- Git Integration: Commits directly to GitHub/GitLab repositories
- Multi-provider Auth: GitHub, GitLab, Google OAuth, or custom authentication
- Media Management: Visual media library with drag-and-drop support
- Development Mode: Local filesystem sync for development
- Production Mode: OAuth + Git publishing for deployed sites
- i18n Support: 22 languages built-in
studio/
├── src/
│ ├── app/ # Vue app for the Studio editor interface
│ │ ├── src/ # Vue components, composables, utils
│ │ └── service-worker.ts
│ └── module/ # Nuxt module code
│ └── src/
│ ├── runtime/ # Runtime server routes & plugins
│ └── module.ts # Main module definition
├── playground/ # Development examples
│ ├── docus/ # Full-featured example
│ └── minimal/ # Minimal example
├── docs/ # Documentation site (also a Nuxt Content app)
-
Development Mode (default in dev)
- Direct filesystem access via server routes
- No auth required
- Changes sync immediately to local files
- No Git operations
-
Production Mode (default in prod)
- OAuth authentication required
- Git provider integration for commits
- Changes pushed to repository
- Triggers CI/CD pipeline
- Nuxt 3: Core framework
- Nuxt Content: Content management layer (peer dependency)
- @nuxtjs/mdc: MDC (Markdown Components) parsing and rendering
- TipTap: Visual WYSIWYG editor
- Monaco Editor: Code editor
- Vue Router: SPA routing inside Studio
- IndexedDB: Client-side draft storage via
idb-keyval - Service Worker: Offline support and caching
- Shiki: Syntax highlighting
- Zod: Schema validation for forms
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content', 'nuxt-studio'],
studio: {
route: '/_studio', // Admin route
// Git repository config (required for production)
repository: {
provider: 'github', // 'github' | 'gitlab'
owner: 'username',
repo: 'repo-name',
branch: 'main',
rootDir: '', // For monorepos
private: true // Request private repo access
},
// i18n
i18n: {
defaultLocale: 'en' // 22 languages available
},
// Component filtering
meta: {
components: {
include: ['Content*'], // Whitelist
exclude: ['Hidden*'] // Blacklist
}
}
}
})GitHub OAuth + GitHub Git Provider:
STUDIO_GITHUB_CLIENT_ID=xxx
STUDIO_GITHUB_CLIENT_SECRET=xxx
STUDIO_GITHUB_MODERATORS=email1@example.com,email2@example.com # OptionalGitLab OAuth + GitLab Git Provider:
STUDIO_GITLAB_APPLICATION_ID=xxx
STUDIO_GITLAB_CLIENT_SECRET=xxx
STUDIO_GITLAB_MODERATORS=email1@example.com,email2@example.com # OptionalGoogle OAuth (requires PAT):
STUDIO_GOOGLE_CLIENT_ID=xxx
STUDIO_GOOGLE_CLIENT_SECRET=xxx
STUDIO_GOOGLE_MODERATORS=email1@example.com,email2@example.com # Required!
STUDIO_GITHUB_TOKEN=xxx # or STUDIO_GITLAB_TOKENCustom Auth (requires PAT):
STUDIO_GITHUB_TOKEN=xxx # or STUDIO_GITLAB_TOKENImportant distinction:
- Auth Providers: Control who can login (GitHub OAuth, GitLab OAuth, Google OAuth, Custom)
- Git Providers: Control where content is committed (GitHub, GitLab)
You can mix and match: e.g., Google OAuth for auth + GitHub for Git operations (requires PAT).
# Install dependencies
pnpm install
# Generate type stubs
pnpm dev:prepare
# Build app and service worker
pnpm prepack# Terminal 1: Start the Vue app dev server
pnpm dev:app # Runs on :5151
# Terminal 2: Start the Nuxt playground
pnpm dev # Runs on :3000, points to :5151 for Studio apppnpm verify # Runs all checks
pnpm test # Vitest tests
pnpm typecheck # Type checking
pnpm lint # ESLintUser can edit:
- Markdown files with MDC syntax
- YAML files
- JSON files
Vue components in Markdown
Studio leverages Nuxt Content's MDC syntax for embedding Vue components in Markdown with props and slots.
::component-name
---
prop: value
---
#slot-name
Slot content here
::Frontmatter
Frontmatter is a convention of Markdown-based CMS to provide meta-data to pages, like description or title. In Nuxt Content, the frontmatter uses the YAML syntax with key: value pairs.
---
title: 'Title of the page'
description: 'meta description of the page'
---
<!-- Content of the page in markdown (MDC) format -->{
"title": "Title of the page",
"description": "meta description of the page"
}title: 'Title of the page'
description: 'meta description of the page'Tiptap editor
- Can edit Markdown files with MDC syntax
- Tiptap editor manipulate Tiptap AST which is directly converted to MDC AST and stored in the existing SQLite database
- If we want to display raw markdown of a file, we can use the
generateContentFromDocumentfunction to get the raw markdown (ie. preview page or monaco editor) - If we want to generate the MDC AST from the raw markdown, we can use the
generateDocumentFromMarkdownContentfunction
Form editor
- Can edit YAML files
- Can edit JSON files
- A vmodel form is generated based on the collection schema
- Every time the form is updated, the content is converted to pure json to be stored in the database
Code editor
- Markdown files with MDC syntax
- YAML files
- JSON files
- The code editor is the only one that can edit raw content, this is a debug editor we don't want to improve it contrary to the other editors.
In production mode:
- Exisiting db files is stored in SQLite browser side database by Nuxt Content. It's loaded by a dump file.
- Markdown files are stored as MDC AST
- YAML and JSON files are stored as pure json
- Drafts files and meta are stored client-side in IndexedDB
- Drafts files content is merged with the existing db files in the browser before being rendered => app is rerendered with updated content in db => this is the preview you see in the browser
In development mode:
- There is no draft system, changes are synced with the server filesystem directly
Studio uses nuxt-component-meta to:
- Discover available components in the user's project
- Find props editors for components
- Find slots for components
Studio uses @nuxtjs/mdc to:
- Parse MDC syntax
- Render MDC syntax
- Generate MDC AST
Studio uses shiki to highlight code in code blocks.
- Gives information about the content of the website and the collections.
- Provides the database adapter to the Studio.
- Provides the content collections to the Studio.
- Provides the schema of the collections to the Studio (form generation)
- Provides the query builder to the Studio.
Development Mode:
/__nuxt_studio/dev/content/*- File operations/__nuxt_studio/dev/public/*- Media operations
Production Mode:
/__nuxt_studio/auth/*- OAuth callbacks- Service routes use Git provider APIs
- src/module/src/module.ts - Main module definition
- src/module/src/auth.ts - Auth provider setup
- src/module/src/dev.ts - Dev mode configuration
- src/app/src/main.ts - Vue app entry point
- src/app/src/service-worker.ts - Service worker for media draft system
- src/module/src/runtime/server/routes/ - Server routes
- Add OAuth config to src/module/src/auth.ts
- Create server route in
src/module/src/runtime/server/routes/auth/ - Add provider type to types
- Update docs
- Create provider implementation in
src/app/src/utils/providers/ - Add provider API client
- Update Git provider types
- Add configuration options
- Define input type in collection schema using
.editor({ input: 'custom' }) - Extend Zod schema types
- Create corresponding form component
- Map input type in form generator
Required:
@nuxt/content- Content layer (peer dependency)@nuxtjs/mdc- MDC parsing/rendering
Core:
unstorage- Storage abstractionidb-keyval- IndexedDB for draftsshiki- Syntax highlighting
Editors:
modern-monaco- Code editorminimark- Markdown processing (TipTap)
Git Providers:
@octokit/types- GitHub API@gitbeaker/core- GitLab API
Studio requires server-side rendering for authentication routes. While you can pre-render pages with nitro.prerender, the site must be deployed on a platform that supports SSR (not static hosting like GitHub Pages).
Use hybrid rendering to pre-render pages while keeping auth routes server-side:
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/'],
crawlLinks: true
}
}
})- Nuxt Content - Underlying content management
- MDC Syntax - Component syntax in Markdown
- Nuxt Content Collections - Schema-based content
- TipTap Editor - Visual editor framework
- Official Studio Docs - User-facing documentation
- Enable debug mode in footer menu to see TipTap JSON ↔ MDC AST ↔ Markdown conversion
- Check browser IndexedDB for draft storage
- Use Vue DevTools to inspect Studio state
- Check service worker logs for medias issues
- Verify OAuth callback URLs match exactly (including protocol)
- Ensure PAT has correct permissions for repository operations
- Nuxt Studio always runs inside a Nuxt app
- Nuxt Content is required and authoritative for content structure
- Studio does not replace Nuxt Content querying or rendering
- All persisted changes ultimately go through Git (for the moment)
- Temporary changes are stored in IndexedDB and synced with the SQLite db in production mode and sync with filesystem in development mode
- The Studio UI is a separate Vue x Vite app embedded via the Nuxt module