@contentrain/nuxt-json v1.0.4
@contentrain/nuxt-json
Powerful, type-safe, and high-performance JSON-based content management module for Nuxt.js.
Features
- š High Performance: Optimized query engine and caching system
- š Type Safety: Full TypeScript integration and automatic type generation
- š Relational Data: Easily manage relationships between models
- š Multilingual Support: Complete support for localized content
- š Advanced Querying: Filtering, sorting, and pagination capabilities
- š§© Seamless Integration: Effortless integration with Nuxt.js
- š Automatic Type Generation: TypeScript interfaces generated from your content models
Installation
# npm
npm install @contentrain/nuxt-json
# yarn
yarn add @contentrain/nuxt-json
# pnpm
pnpm add @contentrain/nuxt-json
Quick Start
1. Configure the Module
Configure the module in your nuxt.config.ts
file:
export default defineNuxtConfig({
modules: ['@contentrain/nuxt-json'],
contentrain: {
path: './content', // Path to your content directory
defaultLocale: 'en', // Default language (optional)
storage: {
driver: 'fs', // 'memory' or 'fs'
base: '.contentrain' // Cache directory (optional)
}
}
})
2. Query Content
Query content in your pages or components:
<script setup>
// Automatically generated types from your content models
import type { WorkItem } from '#build/types/contentrain'
// Basic query
const workQuery = useContentrainQuery<WorkItem>('work-items')
const { data: workItems } = await useAsyncData(() => workQuery.get())
// Query with filtering
const featuredQuery = useContentrainQuery<WorkItem>('work-items')
.where('featured', 'eq', true)
.limit(3)
const { data: featuredWorks } = await useAsyncData(() => featuredQuery.get())
// Query with relational data
const projectQuery = useContentrainQuery<Project>('projects')
.include('categories')
.orderBy('createdAt', 'desc')
const { data: projects } = await useAsyncData(() => projectQuery.get())
</script>
<template>
<div>
<h1>My Work</h1>
<div v-for="item in workItems.data" :key="item.ID">
<h2>{{ item.title }}</h2>
<p>{{ item.description }}</p>
</div>
</div>
</template>
Detailed Usage
Model Structure
Contentrain organizes your content into models. Each model has the following structure:
content/
āāā models/
ā āāā metadata.json
ā āāā work-items.json
ā āāā projects.json
āāā work-items/
ā āāā work-items.json (or en.json, fr.json, etc. for multilingual)
āāā projects/
āāā projects.json (or en.json, fr.json, etc. for multilingual)
Managing Models
To retrieve all models or a specific model:
// Get all models
const models = useContentrainModels()
const { data: allModels } = await useAsyncData(() => models.getAll())
// Get a specific model
const { data: workModel } = await useAsyncData(() => models.get('work-items'))
Query API
The useContentrainQuery
composable provides a powerful API for querying your content:
Filtering
// Single filter
query.where('status', 'eq', 'publish')
// Multiple filters
query
.where('category', 'eq', 'web')
.where('featured', 'eq', true)
.where('views', 'gt', 100)
Supported operators:
eq
: Equal tone
: Not equal togt
: Greater thangte
: Greater than or equal tolt
: Less thanlte
: Less than or equal toin
: In arraynin
: Not in arraycontains
: Contains (string)startsWith
: Starts with (string)endsWith
: Ends with (string)
Sorting
// Single sort
query.orderBy('createdAt', 'desc')
// Multiple sorts
query
.orderBy('priority', 'desc')
.orderBy('title', 'asc')
Pagination
// Limit and offset
query
.limit(10)
.offset(20)
// Lazy loading
const query = useContentrainQuery<Post>('posts').limit(10)
const { data: posts } = await useAsyncData(() => query.get())
// Load more data
if (query.hasMore.value) {
await query.loadMore()
}
Relations
// Single relation
query.include('author')
// Multiple relations
query
.include('author')
.include('categories')
Multilingual
// Query for a specific language
query.locale('en')
Getting the First Item
// Get the first item
const { data: firstItem } = await useAsyncData(() => query.first())
Counting
// Get the total count
const { data: countResult } = await useAsyncData(() => query.count())
const total = countResult.total
Reactive Data
The useContentrainQuery
composable provides reactive data:
const query = useContentrainQuery<Post>('posts')
await query.get()
// Reactive data
const posts = query.data
const total = query.total
const loading = query.loading
const error = query.error
const hasMore = query.hasMore
Type Safety
Automatic Type Generation
The module automatically generates TypeScript types from your content models during the build process. These types are available in your project via the #build/types/contentrain
import path:
// Import automatically generated types
import type { Post, Author, Category } from '#build/types/contentrain'
// Use the types in your queries
const query = useContentrainQuery<Post>('posts')
The generated types include:
- All model properties with correct types
- Relation properties with proper typing
- Multilingual support with language-specific types
- Full IntelliSense support in your IDE
Manual Type Definitions
You can also define your content types manually if needed:
// types/content.ts
import type { Content, LocalizedContent } from '@contentrain/nuxt-json'
export interface Post extends Content {
title: string
content: string
slug: string
featured: boolean
category: string
tags: string[]
}
export interface LocalizedPost extends LocalizedContent {
title: string
content: string
slug: string
featured: boolean
category: string
tags: string[]
}
API Reference
Composables
useContentrainQuery<M>
The main composable for querying content.
Parameters:
modelId
: Model ID
Methods:
where(field, operator, value)
: Adds a filter with type-safe field and value checkingorderBy(field, direction)
: Adds a sort with type-safe field checkinglimit(limit)
: Limits the number of resultsoffset(offset)
: Sets the starting indexinclude(relation)
: Includes a relation with type-safe relation checkinglocale(locale)
: Sets the language with type-safe locale checking (only accepts valid locales defined in your model)get()
: Executes the query and returns the resultsfirst()
: Returns the first resultcount()
: Returns the total countloadMore()
: Loads more datareset()
: Resets the query state
Properties:
data
: Reactive data arraytotal
: Reactive total countloading
: Reactive loading stateerror
: Reactive error statehasMore
: Reactive has more data state
useContentrainModels
Composable for managing model data.
Methods:
get(modelId)
: Returns a specific modelgetAll()
: Returns all models
Properties:
useModel()
: Reactive model datauseModels()
: Reactive model listuseLoading()
: Reactive loading stateuseError()
: Reactive error state
Return Types
QueryResult<T>
Standard return type for queries that return multiple results.
interface QueryResult<T> {
data: T[]
total: number
pagination: {
limit: number
offset: number
total: number
}
}
SingleQueryResult<T>
Standard return type for queries that return a single result.
interface SingleQueryResult<T> {
data: T
total: number
pagination: {
limit: number
offset: number
total: number
}
}
ModelResult<T>
Return type for model queries.
interface ModelResult<T> {
data: T
metadata: {
modelId: string
timestamp: number
}
}
ApiResponse<T>
Standard format for API responses.
interface ApiResponse<T> {
success: boolean
data: T
error?: {
code: string
message: string
details?: unknown
}
}
Advanced Features
Custom Error Handling
The module provides a ContentrainError
class for custom error handling:
try {
const result = await query.get()
// Operation successful
} catch (error) {
if (error instanceof ContentrainError) {
console.error(`Error code: ${error.code}`)
console.error(`Error message: ${error.message}`)
console.error(`Details:`, error.details)
}
}
Cache Management
The module provides automatic cache management to improve performance. The default cache duration is 5 minutes.
Examples
Blog Page
<script setup>
import type { Post } from '~/types'
// Get blog posts with pagination
const currentPage = ref(1)
const pageSize = 10
const query = useContentrainQuery<Post>('posts')
.where('status', 'eq', 'publish')
.orderBy('createdAt', 'desc')
.limit(pageSize)
const { data: postsData } = await useAsyncData(() => {
query.offset((currentPage.value - 1) * pageSize)
return query.get()
})
// Reload when page changes
watch(currentPage, async () => {
query.offset((currentPage.value - 1) * pageSize)
await query.get()
})
// Calculate total pages
const totalPages = computed(() => Math.ceil(postsData.value.total / pageSize))
</script>
<template>
<div>
<h1>Blog</h1>
<div v-if="query.loading.value">Loading...</div>
<div v-else-if="query.error.value">
Error: {{ query.error.value.message }}
</div>
<div v-else>
<article v-for="post in query.data.value" :key="post.ID">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
<NuxtLink :to="`/blog/${post.slug}`">Read More</NuxtLink>
</article>
<!-- Pagination -->
<div class="pagination">
<button
:disabled="currentPage === 1"
@click="currentPage--"
>
Previous
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button
:disabled="currentPage === totalPages"
@click="currentPage++"
>
Next
</button>
</div>
</div>
</div>
</template>
Multilingual Support
<script setup>
import type { LocalizedPost } from '~/types'
const { locale } = useI18n()
// Get content for current language
const query = useContentrainQuery<LocalizedPost>('posts')
.locale(locale.value)
.where('featured', 'eq', true)
.limit(5)
const { data: featuredPosts } = await useAsyncData(() => query.get())
// Update content when language changes
watch(locale, async () => {
query.locale(locale.value)
await query.get()
})
</script>
Relational Data
<script setup>
import type { Project, Category } from '~/types'
// Get categories and projects
const categoriesQuery = useContentrainQuery<Category>('categories')
const { data: categories } = await useAsyncData(() => categoriesQuery.get())
const projectsQuery = useContentrainQuery<Project>('projects')
.include('categories')
.orderBy('createdAt', 'desc')
const { data: projects } = await useAsyncData(() => projectsQuery.get())
// Filter projects by category
const selectedCategory = ref(null)
const filteredProjects = computed(() => {
if (!selectedCategory.value) return projects.value.data
return projects.value.data.filter(project => {
const projectCategories = project._relations?.categories || []
return Array.isArray(projectCategories)
? projectCategories.some(cat => cat.ID === selectedCategory.value)
: projectCategories.ID === selectedCategory.value
})
})
</script>
<template>
<div>
<h1>Projects</h1>
<!-- Category filters -->
<div class="filters">
<button
:class="{ active: !selectedCategory }"
@click="selectedCategory = null"
>
All
</button>
<button
v-for="category in categories.data"
:key="category.ID"
:class="{ active: selectedCategory === category.ID }"
@click="selectedCategory = category.ID"
>
{{ category.name }}
</button>
</div>
<!-- Projects -->
<div class="projects">
<div v-for="project in filteredProjects" :key="project.ID" class="project">
<h2>{{ project.title }}</h2>
<p>{{ project.description }}</p>
<!-- Related categories -->
<div class="categories">
<span v-if="project._relations?.categories">
<template v-if="Array.isArray(project._relations.categories)">
<span v-for="cat in project._relations.categories" :key="cat.ID" class="category">
{{ cat.name }}
</span>
</template>
<template v-else>
<span class="category">{{ project._relations.categories.name }}</span>
</template>
</span>
</div>
</div>
</div>
</div>
</template>
Integration with Contentrain CMS
This module is designed to work seamlessly with Contentrain, a Git-based Headless CMS that focuses on developer and content editor experience. Contentrain provides:
- š Git Architecture: Advantages in scaling, maintenance, and low cost
- š ļø Flexible Data Models: No-code interfaces for creating content schemas
- š Perfect for Static Sites: The ideal tool for dynamic content on statically published sites
- š„ Team Collaboration: Custom roles and permissions for content teams
- š Multilingual Support: Create and manage content in multiple languages
Learn more about Contentrain at contentrain.io
Troubleshooting
Common Errors
STORAGE_NOT_READY
Content directory is not properly configured or accessible.
Solution: Ensure that your content directory is properly configured and accessible.
MODEL_NOT_FOUND
The specified model was not found.
Solution: Ensure that the model ID is correct and exists in your content directory.
INVALID_QUERY_PARAMS
Query parameters are invalid.
Solution: Ensure that your query parameters are in the correct format.
TypeScript Errors
Argument of type 'string' is not assignable to parameter of type 'never'
This error occurs when using the locale()
method with a locale that is not defined in your model.
Solution: Make sure you're using a locale that is defined in your model's _lang
property. For example, if your model only supports 'en' and 'tr', you can only use these values with the locale()
method.
// Correct usage
query.locale('en') // Works if 'en' is defined in your model
query.locale('tr') // Works if 'tr' is defined in your model
// Incorrect usage
query.locale('fr') // TypeScript error if 'fr' is not defined in your model
Property '_relations' does not exist on type...
This error occurs when trying to access relations on a model that doesn't have any defined relations.
Solution: Make sure your model has relations defined in its schema, or check if the relation is properly included in your query using the include()
method.
// Make sure to include the relation before accessing it
const query = useContentrainQuery<Post>('posts')
.include('author')
.get()
Contributing
We welcome your contributions! Please read our contribution guidelines.