N
Nuxt4d ago
maroo

How can I generate a tag cloud from a custom Nuxt Content

Hi everyone, I'm currently using Nuxt Content and have defined a custom schema that includes a tags field with the type z.array(z.string()). On the frontend, this seems to be interpreted as a string[], but under the hood, it appears that the data is stored in the database as a text field with a JSON dump of the array. I'm looking for an effective way to generate a tag cloud—ideally, by extracting the list of distinct tags and calculating their frequency of appearance across all content entries. Has anyone implemented something similar or can recommend an approach for parsing and analyzing these JSON-stored tag arrays efficiently, either at build time or via some content-querying strategy? Any tips, examples, or suggestions would be greatly appreciated! 🙏
5 Replies
kapa.ai
kapa.ai4d ago
Hi @maroo. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To generate a tag cloud from your Nuxt Content collection with a tags field, you can use the queryCollection utility to fetch all your content items and then process the tags to count their frequencies. Based on the knowledge sources, Nuxt Content v3 uses SQL-based storage which allows for efficient querying. Since your tags are stored as a JSON string in the database, you'll need to fetch all content items and process them in JavaScript to extract and count the tags. Here's an approach you can implement:
<script setup>
const { data: allContent } = await useAsyncData('all-content', () => {
// Replace 'yourCollection' with your actual collection name
return queryCollection('yourCollection').all()
})

// Process tags to create a frequency map
const tagFrequency = computed(() => {
if (!allContent.value) return {}

const tagMap = {}

allContent.value.forEach(item => {
if (item.tags && Array.isArray(item.tags)) {
item.tags.forEach(tag => {
tagMap[tag] = (tagMap[tag] || 0) + 1
})
}
})

return tagMap
})

// Convert to array for easier rendering
const tagCloud = computed(() => {
return Object.entries(tagFrequency.value).map(([tag, count]) => ({
tag,
count
})).sort((a, b) => b.count - a.count) // Sort by frequency
})
</script>

<template>
<div class="tag-cloud">
<div
v-for="item in tagCloud"
:key="item.tag"
:style="{ fontSize: `${Math.max(1, Math.min(3, 1 + (item.count / 5)))}rem` }"
class="tag-item"
>
{{ item.tag }} ({{ item.count }})
</div>
</div>
</template>

<style scoped>
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag-item {
padding: 0.25rem 0.5rem;
background-color: #f0f0f0;
border-radius: 0.25rem;
cursor: pointer;
}
</style>
<script setup>
const { data: allContent } = await useAsyncData('all-content', () => {
// Replace 'yourCollection' with your actual collection name
return queryCollection('yourCollection').all()
})

// Process tags to create a frequency map
const tagFrequency = computed(() => {
if (!allContent.value) return {}

const tagMap = {}

allContent.value.forEach(item => {
if (item.tags && Array.isArray(item.tags)) {
item.tags.forEach(tag => {
tagMap[tag] = (tagMap[tag] || 0) + 1
})
}
})

return tagMap
})

// Convert to array for easier rendering
const tagCloud = computed(() => {
return Object.entries(tagFrequency.value).map(([tag, count]) => ({
tag,
count
})).sort((a, b) => b.count - a.count) // Sort by frequency
})
</script>

<template>
<div class="tag-cloud">
<div
v-for="item in tagCloud"
:key="item.tag"
:style="{ fontSize: `${Math.max(1, Math.min(3, 1 + (item.count / 5)))}rem` }"
class="tag-item"
>
{{ item.tag }} ({{ item.count }})
</div>
</div>
</template>

<style scoped>
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag-item {
padding: 0.25rem 0.5rem;
background-color: #f0f0f0;
border-radius: 0.25rem;
cursor: pointer;
}
</style>
This approach: 1. Fetches all content items from your collection using the queryCollection utility 2. Processes the tags to create a frequency map 3. Converts the map to an array for easier rendering 4. Renders the tags with font sizes proportional to their frequency If you need to optimize this for a large number of content items, you might consider: 1. Using the queryCollectionSearchSections utility to potentially get more targeted data 2. Implementing pagination if your content collection is very large 3. Creating a custom hook to cache the tag cloud data For more advanced querying capabilities, you can explore the queryCollection API documentation which provides methods for filtering, sorting, and paginating your collections. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4d ago
<@403034289869946900> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
maroo
marooOP4d ago
@kapa.ai Ok but selecing all content and collect tags on-fly while loading is not seems good idea. Please let me figure out using custom hook idea?
kapa.ai
kapa.ai4d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're right that loading all content just to generate a tag cloud might not be the most efficient approach, especially with larger collections. Let's explore implementing a custom hook to better handle tag cloud generation in Nuxt Content. Based on the knowledge sources, we can create a custom hook that leverages Nuxt's runtime hooks system to cache the tag data and avoid unnecessary processing on each page load. Here's how you could implement a custom hook for tag cloud generation:
// composables/useTagCloud.js
export const useTagCloud = () => {
const nuxtApp = useNuxtApp()
const tagCloud = useState('content-tag-cloud', () => null)

// Try to get cached data first
const cachedData = nuxtApp.payload.data['content-tag-cloud'] || null
if (cachedData && !tagCloud.value) {
tagCloud.value = cachedData
}

const generateTagCloud = async (collectionName) => {
// If we already have data, return it
if (tagCloud.value) return tagCloud.value

// Otherwise fetch and process
const { data: items } = await useAsyncData(
`tag-cloud-${collectionName}`,
() => queryCollection(collectionName).all(),
{
// Use getCachedData to avoid refetching on client navigation
getCachedData: () => nuxtApp.payload.data[`tag-cloud-${collectionName}`] || null
}
)

// Process tags
const tagFrequency = {}

if (items.value) {
items.value.forEach(item => {
if (item.tags && Array.isArray(item.tags)) {
item.tags.forEach(tag => {
tagFrequency[tag] = (tagFrequency[tag] || 0) + 1
})
}
})
}

// Convert to array format
const result = Object.entries(tagFrequency).map(([tag, count]) => ({
tag,
count
})).sort((a, b) => b.count - a.count)

// Store in state for reuse
tagCloud.value = result

return result
}

// Register a hook to clean up the cache when needed
useRuntimeHook('app:data:refresh', () => {
tagCloud.value = null
})

return {
tagCloud,
generateTagCloud
}
}
// composables/useTagCloud.js
export const useTagCloud = () => {
const nuxtApp = useNuxtApp()
const tagCloud = useState('content-tag-cloud', () => null)

// Try to get cached data first
const cachedData = nuxtApp.payload.data['content-tag-cloud'] || null
if (cachedData && !tagCloud.value) {
tagCloud.value = cachedData
}

const generateTagCloud = async (collectionName) => {
// If we already have data, return it
if (tagCloud.value) return tagCloud.value

// Otherwise fetch and process
const { data: items } = await useAsyncData(
`tag-cloud-${collectionName}`,
() => queryCollection(collectionName).all(),
{
// Use getCachedData to avoid refetching on client navigation
getCachedData: () => nuxtApp.payload.data[`tag-cloud-${collectionName}`] || null
}
)

// Process tags
const tagFrequency = {}

if (items.value) {
items.value.forEach(item => {
if (item.tags && Array.isArray(item.tags)) {
item.tags.forEach(tag => {
tagFrequency[tag] = (tagFrequency[tag] || 0) + 1
})
}
})
}

// Convert to array format
const result = Object.entries(tagFrequency).map(([tag, count]) => ({
tag,
count
})).sort((a, b) => b.count - a.count)

// Store in state for reuse
tagCloud.value = result

return result
}

// Register a hook to clean up the cache when needed
useRuntimeHook('app:data:refresh', () => {
tagCloud.value = null
})

return {
tagCloud,
generateTagCloud
}
}
Then you can use this hook in your components:
<script setup>
const { tagCloud, generateTagCloud } = useTagCloud()
// Generate tag cloud only when needed
onMounted(async () => {
if (!tagCloud.value) {
await generateTagCloud('yourCollection')
}
})
</script>

<template>
<div v-if="tagCloud" class="tag-cloud">
<div
v-for="item in tagCloud"
:key="item.tag"
:style="{ fontSize: `${Math.max(1, Math.min(3, 1 + (item.count / 5)))}rem` }"
class="tag-cloud-item"
>
{{ item.tag }} ({{ item.count }})
</div>
</div>
<div v-else>Loading tag cloud...</div>
</template>
<script setup>
const { tagCloud, generateTagCloud } = useTagCloud()
// Generate tag cloud only when needed
onMounted(async () => {
if (!tagCloud.value) {
await generateTagCloud('yourCollection')
}
})
</script>

<template>
<div v-if="tagCloud" class="tag-cloud">
<div
v-for="item in tagCloud"
:key="item.tag"
:style="{ fontSize: `${Math.max(1, Math.min(3, 1 + (item.count / 5)))}rem` }"
class="tag-cloud-item"
>
{{ item.tag }} ({{ item.count }})
</div>
</div>
<div v-else>Loading tag cloud...</div>
</template>
This approach offers several advantages: 1. Caching: Uses Nuxt's useNuxtApp and payload system to cache the processed tag data 2. Efficiency: Only generates the tag cloud once per session using getCachedData functionality 3. Reactivity: Maintains the tag cloud in a reactive state with useState 4. Cleanup: Uses useRuntimeHook to properly handle cache invalidation when needed For even more optimization, you could consider: 1. Creating a server API route that pre-processes the tags 2. Using Nuxt Content hooks like content:file:afterParse to build the tag cloud during build time 3. Implementing a custom TTL (Time To Live) logic for the cache This approach should provide a more efficient solution than loading all content on each page visit. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4d ago
<@403034289869946900> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?