A Nuxt Content-compatible content management system for Laravel + Inertia.js + Vue applications. Write Markdown, get Vue components, query with Laravel – all with build-time compilation and zero runtime overhead.
Latest Version: v1.0.0 (Stable)
Traditional CMS solutions force you to pass HTML through Inertia props or parse Markdown at runtime. Inertia Content takes a different approach: compile Markdown to Vue components at build time, while Laravel maintains full control over routing and access.
- 📝 File-based content - Write Markdown files, get compiled Vue components
- ⚡ Build-time compilation - Zero runtime parsing, optimal performance
- 🔍 Powerful queries - Nuxt Content-style query API in PHP
- 🎯 Full TypeScript support - Type safety from PHP to Vue
- 🔥 Hot Module Replacement - Instant updates during development
- 🎨 Familiar API - If you know Nuxt Content, you already know this
- 🔐 Laravel-first - Server-side access control and routing
- 🚀 Production-ready - Caching, optimization, and security built-in
Inertia Content is a single Composer package that includes both PHP and JavaScript code.
# 1. Install via Composer
composer require farsi/inertia-content
# 2. Install JavaScript dependencies (IMPORTANT!)
cd vendor/farsi/inertia-content && npm install && cd -
# 3. Run installer
php artisan inertia-content:installThe JavaScript/Vue components are TypeScript source files that your Vite will compile - no pre-built JavaScript.
📖 See How It Works for detailed explanation.
Add the Vite plugin to your vite.config.ts:
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
import inertiaContent from './vendor/farsi/inertia-content/resources/js/vite'
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.ts'],
refresh: true,
}),
vue(),
inertiaContent(),
],
resolve: {
alias: {
'@inertia-content': '/vendor/farsi/inertia-content/resources/js'
}
}
})Create markdown files in resources/content:
---
title: Getting Started
description: Learn how to use Inertia Content
order: 1
---
## Introduction
Welcome to **Inertia Content**!Add content routes to routes/web.php:
use Farsi\InertiaContent\Facades\Content;
Route::get('/docs/{path?}', function ($path = 'index') {
return Content::pageOrFail("docs/$path");
})->where('path', '.*');Create a Vue component to render content:
<script setup lang="ts">
import { ContentDoc } from '@inertia-content'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
</script>
<template>
<ContentDoc :content-key="page.props.contentKey" />
</template>use Farsi\InertiaContent\Facades\Content;
// Find a single entry
$entry = Content::find('docs/intro');
// Check if content exists
if (Content::exists('docs/intro')) {
// ...
}
// Query multiple entries
$entries = Content::query()
->where('_dir', 'docs')
->where('draft', false)
->orderBy('order', 'asc')
->get();
// Get navigation tree
$nav = Content::navigation('docs');Simple content rendering:
<script setup lang="ts">
import { ContentRenderer } from 'farsi-inertia-content'
</script>
<template>
<ContentRenderer content-key="docs/intro" />
</template>Full document with header, TOC, and footer:
<script setup lang="ts">
import { ContentDoc } from 'farsi-inertia-content'
</script>
<template>
<ContentDoc content-key="docs/intro">
<template #header="{ meta }">
<h1>{{ meta.title }}</h1>
<p>{{ meta.description }}</p>
</template>
<template #toc="{ headings }">
<nav>
<a v-for="h in headings" :key="h.id" :href="`#${h.id}`">
{{ h.text }}
</a>
</nav>
</template>
</ContentDoc>
</template>List multiple content entries:
<script setup lang="ts">
import { ContentList } from 'farsi-inertia-content'
import { Link } from '@inertiajs/vue3'
</script>
<template>
<ContentList :entries="$page.props.entries">
<template #default="{ entries }">
<article v-for="entry in entries" :key="entry._id">
<Link :href="`/${entry._path}`">
<h2>{{ entry.title }}</h2>
<p>{{ entry._excerpt }}</p>
</Link>
</article>
</template>
</ContentList>
</template>For advanced use cases:
<script setup lang="ts">
import { useContent } from 'farsi-inertia-content'
const { component, meta, headings, isLoading, error } = useContent('docs/intro')
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<h1>{{ meta.title }}</h1>
<component :is="component" />
</div>
</template>Supported frontmatter fields:
---
title: Page Title # Required
description: Page description # Optional
draft: false # Optional, default: false
navigation: true # Optional, default: true
order: 1 # Optional, for sorting
excerpt: Custom excerpt # Optional, auto-generated if not provided
# ... any custom fields
---Publish the configuration file:
php artisan vendor:publish --tag=inertia-content-configAvailable options in config/inertia-content.php:
return [
'content_dir' => resource_path('content'),
'manifest_path' => public_path('build/inertia-content-manifest.json'),
'show_drafts' => env('INERTIA_CONTENT_SHOW_DRAFTS', false),
'default_component' => 'Content/Page',
// ... more options
];# Install the package
php artisan inertia-content:install
# Clear content cache
php artisan inertia-content:clear# PHP tests
composer test
# JavaScript tests
npm testInertia Content follows a unique architecture that respects both Laravel and Inertia.js philosophies:
- Vite plugin scans
resources/content/**/*.md - Parses frontmatter, extracts headings, generates excerpts
- Compiles Markdown → Vue components
- Generates JSON manifest with metadata
- Laravel queries the manifest (cached)
- Passes only the content key through Inertia (never HTML/Markdown)
- Vue dynamically imports the pre-compiled component
- Component renders instantly (already compiled)
No HTML through Inertia props. No runtime Markdown parsing.
Laravel maintains authority over:
- ✅ Content queries and filtering
- ✅ Access control and permissions
- ✅ Routing decisions
Vue handles:
- ✅ Component rendering (pre-compiled)
- ✅ User interactions
- ✅ Client-side navigation
| Feature | Inertia Content | Traditional CMS | Static Site Generators |
|---|---|---|---|
| Runtime Parsing | ❌ No | ✅ Yes | ❌ No |
| Build Performance | ⚡ Fast | N/A | ⚡ Fast |
| Laravel Integration | ✅ Native | ❌ Separate | |
| Dynamic Queries | ✅ Yes | ✅ Yes | ❌ Limited |
| Type Safety | ✅ Full | ||
| HMR | ✅ Yes | ✅ Yes | |
| Server Authority | ✅ Complete | ✅ Complete | ❌ No server |
This package implements 80% of Nuxt Content v2 core features, adapted for Laravel + Inertia:
✅ Implemented:
- Query API (
Content::query()≈queryContent()) - Markdown parsing with frontmatter
- Heading extraction & TOC
- Navigation generation
- HMR support
- Vue components
- TypeScript support
⏳ Planned for v1.1:
- MDC (Markdown Components)
- Full-text search
- Syntax highlighting (Shiki)
- YAML/JSON file support
See Nuxt Content Comparison for detailed feature parity analysis.
- Markdown compilation
- Query API (Nuxt Content-compatible)
- Vue components
- HMR support
- TypeScript support
- MDC (Markdown Components) - v1.1
- Full-text search - v1.1
- Shiki syntax highlighting - v1.1
- YAML/JSON support - v1.1
- Content versioning - v1.2
- Multi-language support - v1.2
Contributions are welcome! Please see CONTRIBUTING.md for details.
If you discover any security-related issues, please email dev@farsi.dev instead of using the issue tracker.
See Security Policy for more details.
The MIT License (MIT). Please see License File for more information.
- Farsi Dev
- Inspired by Nuxt Content
- Built with Spatie's Laravel Package Skeleton
- All contributors
This is a single Composer package that includes both PHP and JavaScript code.
📦 Installation: composer require farsi/inertia-content
📂 JavaScript: Included in vendor/farsi/inertia-content/resources/js/
🔌 Vite Plugin: Import from vendor directory
See Package Structure and Architecture for details.
- ⭐ Star the repo
- 🐛 Report bugs via GitHub Issues
- 💬 Discuss features via GitHub Discussions
- 📖 Read the full documentation