
Product
Introducing Webhook Events for Pull Request Scans
Add real-time Socket webhook events to your workflows to automatically receive pull request scan results and security alerts in real time.
@contentrain/nuxt-json
Advanced tools
Powerful, type-safe, and high-performance JSON-based content management module for Nuxt.js.
# npm
npm install @contentrain/nuxt-json
# yarn
yarn add @contentrain/nuxt-json
# pnpm
pnpm add @contentrain/nuxt-json
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)
}
}
})
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>
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)
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'))
The useContentrainQuery
composable provides a powerful API for querying your content:
// 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)// Single sort
query.orderBy('createdAt', 'desc')
// Multiple sorts
query
.orderBy('priority', 'desc')
.orderBy('title', 'asc')
// 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()
}
// Single relation
query.include('author')
// Multiple relations
query
.include('author')
.include('categories')
// Query for a specific language
query.locale('en')
// Get the first item
const { data: firstItem } = await useAsyncData(() => query.first())
// Get the total count
const { data: countResult } = await useAsyncData(() => query.count())
const total = countResult.total
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
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:
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[]
}
useContentrainQuery<M>
The main composable for querying content.
Parameters:
modelId
: Model IDMethods:
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 stateProperties:
data
: Reactive data arraytotal
: Reactive total countloading
: Reactive loading stateerror
: Reactive error statehasMore
: Reactive has more data stateuseContentrainModels
Composable for managing model data.
Methods:
get(modelId)
: Returns a specific modelgetAll()
: Returns all modelsProperties:
useModel()
: Reactive model datauseModels()
: Reactive model listuseLoading()
: Reactive loading stateuseError()
: Reactive error stateQueryResult<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
}
}
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)
}
}
The module provides automatic cache management to improve performance. The default cache duration is 5 minutes.
<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>
<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>
<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>
This module is designed to work seamlessly with Contentrain, a Git-based Headless CMS that focuses on developer and content editor experience. Contentrain provides:
Learn more about Contentrain at contentrain.io
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.
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()
We welcome your contributions! Please read our contribution guidelines.
FAQs
Contentrain JSON Module for Nuxt
We found that @contentrain/nuxt-json demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
Add real-time Socket webhook events to your workflows to automatically receive pull request scan results and security alerts in real time.
Research
The Socket Threat Research Team uncovered malicious NuGet packages typosquatting the popular Nethereum project to steal wallet keys.
Product
A single platform for static analysis, secrets detection, container scanning, and CVE checksβbuilt on trusted open source tools, ready to run out of the box.