Merge pull request #83 from nurRiyad/content-v3

Update @nuxt/content to version 3.x and refactor content queries
This commit is contained in:
Al Asad Nur Riyad
2025-03-13 04:54:00 +06:00
committed by GitHub
16 changed files with 22688 additions and 15611 deletions

View File

@@ -1,6 +1,3 @@
# This workflow will do a clean installation of node dependencies, check linting and build
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI name: Node.js CI
on: on:
@@ -17,23 +14,20 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22
cache: 'pnpm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: pnpm install run: npm ci
- name: Check Linting - name: Check Linting
run: pnpm run lint run: npm run lint
- name: Check Format - name: Check Format
run: pnpm run format run: npm run format
- name: Playgourd build check - name: Playgourd build check
run: pnpm run build run: npm run build

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const { path } = useRoute() const { path } = useRoute()
const articles = await queryContent(path).findOne() const articles = await queryCollection('content').path(path).first()
const links = articles?.body?.toc?.links || [] const links = articles?.body?.toc?.links || []
</script> </script>

View File

@@ -1,21 +1,42 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BlogPost } from '~/types/blog'
// Function to parse dates in the format "1st Mar 2023"
function parseCustomDate(dateStr: string): Date {
// Remove ordinal indicators (st, nd, rd, th)
const cleanDateStr = dateStr.replace(/(\d+)(st|nd|rd|th)/, '$1')
// Parse the date
return new Date(cleanDateStr)
}
// Get Last 6 Publish Post from the content/blog directory // Get Last 6 Publish Post from the content/blog directory
const { data } = await useAsyncData('recent-post', () => const { data } = await useAsyncData('recent-post', () =>
queryContent('/blogs').limit(3).sort({ _id: -1 }).find(), queryCollection('content')
.all()
.then((data) => {
return data
.sort((a, b) => {
const aDate = parseCustomDate(a.meta.date as string)
const bDate = parseCustomDate(b.meta.date as string)
return bDate.getTime() - aDate.getTime()
})
.slice(0, 3)
}),
) )
const formattedData = computed(() => { const formattedData = computed(() => {
return data.value?.map((articles) => { return data.value?.map((articles) => {
const meta = articles.meta as unknown as BlogPost
return { return {
path: articles._path, path: articles.path,
title: articles.title || 'no-title available', title: articles.title || 'no-title available',
description: articles.description || 'no-description available', description: articles.description || 'no-description available',
image: articles.image || '/not-found.jpg', image: meta.image || '/not-found.jpg',
alt: articles.alt || 'no alter data available', alt: meta.alt || 'no alter data available',
ogImage: articles.ogImage || '/not-found.jpg', ogImage: meta.ogImage || '/not-found.jpg',
date: articles.date || 'not-date-available', date: meta.date || 'not-date-available',
tags: articles.tags || [], tags: meta.tags || [],
published: articles.published || false, published: meta.published || false,
} }
}) })
}) })

View File

@@ -1,21 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
// Get Last 6 Publish Post from the content/blog directory import type { BlogPost } from '~/types/blog'
const { data } = await useAsyncData('trending-post', () => const { data } = await useAsyncData('trending-post', () =>
queryContent('/blogs').limit(3).sort({ _id: 1 }).find(), queryCollection('content').limit(3).all(),
) )
const formattedData = computed(() => { const formattedData = computed(() => {
return data.value?.map((articles) => { return data.value?.map((articles) => {
const meta = articles.meta as unknown as BlogPost
return { return {
path: articles._path, path: articles.path,
title: articles.title || 'no-title available', title: articles.title || 'no-title available',
description: articles.description || 'no-description available', description: articles.description || 'no-description available',
image: articles.image || '/not-found.jpg', image: meta.image || '/not-found.jpg',
alt: articles.alt || 'no alter data available', alt: meta.alt || 'no alter data available',
ogImage: articles.ogImage || '/not-found.jpg', ogImage: meta.ogImage || '/not-found.jpg',
date: articles.date || 'not-date-available', date: meta.date || 'not-date-available',
tags: articles.tags || [], tags: meta.tags || [],
published: articles.published || false, published: meta.published || false,
} }
}) })
}) })

23
content.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { asRobotsCollection } from '@nuxtjs/robots/content'
import { asSitemapCollection } from '@nuxtjs/sitemap/content'
import { asOgImageCollection } from 'nuxt-og-image/content'
export default defineContentConfig({
collections: {
content: defineCollection({
...asRobotsCollection({
type: 'page',
source: '**/*.md',
}),
...asSitemapCollection({
type: 'page',
source: '**/*.md',
}),
...asOgImageCollection({
type: 'page',
source: '**/*.md',
}),
}),
},
})

View File

@@ -5,7 +5,7 @@ export const navbarData = {
export const footerData = { export const footerData = {
author: 'Al Asad Nur Riyad', author: 'Al Asad Nur Riyad',
aboutAuthor: aboutAuthor:
'Hi! I am Riyad, a Tech enthusiast, problem solver and software engineer. Currently working at Appscode Inc.', 'Hi! I am Riyad, a Tech enthusiast, problem solver and software engineer. Currently working at FieldNation LLC.',
authorInterest: authorInterest:
"I have a fair amount of knowledge of Javascript, Typescript, VueJs, and Nuxt. If you have an interesting idea, either open source or paid let's connect.", "I have a fair amount of knowledge of Javascript, Typescript, VueJs, and Nuxt. If you have an interesting idea, either open source or paid let's connect.",
aboutTheSite: aboutTheSite:
@@ -33,13 +33,13 @@ export const aboutPage = {
title: 'Al Asad Nur Riyad', title: 'Al Asad Nur Riyad',
description: 'Software Engineer, Problem Solver, Web Enthusiast.', description: 'Software Engineer, Problem Solver, Web Enthusiast.',
aboutMe: aboutMe:
"Hello, fellow human! I'm a software wizard who spends most of his day crafting code spells at @AppsCode in the Bytebuilders team. When I'm not crafting code, you can find me summoning solutions to problems on online judges. Just don't ask me to cast any love spells, my magic only works on machines!", "Hello, fellow human! I'm a software wizard who spends most of his day crafting code spells at @FieldNation in the Workplace Operation team. When I'm not crafting code, you can find me summoning solutions to problems on online judges. Just don't ask me to cast any love spells, my magic only works on machines!",
} }
export const seoData = { export const seoData = {
title: `Riyad's Blog | Riyads Blog`, title: `Riyad's Blog | Riyads Blog`,
ogTitle: `Let's learn Javascript, Typescript, Vue, Nuxt, & Problem Solving - Riyads Blog | Riyad's Blog`, ogTitle: `Let's learn Javascript, Typescript, Vue, Nuxt, & Problem Solving - Riyads Blog | Riyad's Blog`,
description: `Hi I am Riyad. A Software Engineer at AppsCode, with over 2.5+ years experience in software development. - Riyads Blog | Riyad's Blog`, description: `Hi I am Riyad. A Software Engineer at FieldNation, with over 3.5+ years experience in software development. - Riyads Blog | Riyad's Blog`,
twitterDescription: `Riyad's Blog, where I play around with Nuxt, Vue, and more and showcase my blog, resources, etc - Riyads Blog | Riyad's Blog`, twitterDescription: `Riyad's Blog, where I play around with Nuxt, Vue, and more and showcase my blog, resources, etc - Riyads Blog | Riyad's Blog`,
image: image:
'https://res.cloudinary.com/dmecmyphj/image/upload/v1673548905/nuxt-blog/cover_ntgs6u.webp', 'https://res.cloudinary.com/dmecmyphj/image/upload/v1673548905/nuxt-blog/cover_ntgs6u.webp',

View File

@@ -10,10 +10,10 @@ export default defineNuxtConfig({
'@nuxt/fonts', '@nuxt/fonts',
'@nuxt/eslint', '@nuxt/eslint',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxt/content',
'nuxt-og-image',
'@nuxtjs/robots', '@nuxtjs/robots',
'@nuxtjs/sitemap', '@nuxtjs/sitemap',
'nuxt-og-image',
'@nuxt/content',
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
'@nuxtjs/tailwindcss', '@nuxtjs/tailwindcss',
'@formkit/auto-animate', '@formkit/auto-animate',
@@ -32,15 +32,12 @@ export default defineNuxtConfig({
}, },
sitemap: { sitemap: {
strictNuxtContentPaths: true, sources: [seoData.mySite],
}, },
site: { site: {
url: seoData.mySite, url: seoData.mySite,
identity: { name: 'Al Asad Nur Riyad',
type: 'Person',
},
twitter: seoData.twitterHandle,
}, },
typescript: { typescript: {
@@ -61,8 +58,12 @@ export default defineNuxtConfig({
}, },
content: { content: {
build: {
markdown: {
highlight: { highlight: {
theme: 'dracula', theme: 'dracula',
}, },
}, },
},
},
}) })

22549
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,13 @@
}, },
"devDependencies": { "devDependencies": {
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@nuxt/content": "^2.13.4", "@nuxt/content": "^3.3.0",
"@nuxt/eslint": "^0.6.1", "@nuxt/eslint": "^0.6.1",
"@nuxt/fonts": "^0.10.2", "@nuxt/fonts": "^0.10.2",
"@nuxt/image": "^1.8.0", "@nuxt/image": "^1.8.0",
"@nuxtjs/color-mode": "^3.3.2", "@nuxtjs/color-mode": "^3.3.2",
"@nuxtjs/robots": "^4.1.7", "@nuxtjs/robots": "^5.2.6",
"@nuxtjs/sitemap": "^5.3.2", "@nuxtjs/sitemap": "^7.2.7",
"@nuxtjs/tailwindcss": "^6.11.4", "@nuxtjs/tailwindcss": "^6.11.4",
"@stefanobartoletti/nuxt-social-share": "^0.6.1", "@stefanobartoletti/nuxt-social-share": "^0.6.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
@@ -34,10 +34,9 @@
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"nuxt": "^3.16.0", "nuxt": "^3.16.0",
"nuxt-icon": "^0.6.8", "nuxt-icon": "^0.6.8",
"nuxt-og-image": "^3.1.1", "nuxt-og-image": "^5.0.2",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"unenv": "^1.10.0" "unenv": "^1.10.0"
}, }
"packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b"
} }

View File

@@ -5,21 +5,22 @@ import { navbarData, seoData } from '~/data'
const { path } = useRoute() const { path } = useRoute()
const { data: articles, error } = await useAsyncData(`blog-post-${path}`, () => const { data: articles, error } = await useAsyncData(`blog-post-${path}`, () =>
queryContent(path).findOne(), queryCollection('content').path(path).first(),
) )
if (error.value) navigateTo('/404') if (error.value) navigateTo('/404')
const data = computed<BlogPost>(() => { const data = computed<BlogPost>(() => {
const meta = articles?.value?.meta as unknown as BlogPost
return { return {
title: articles.value?.title || 'no-title available', title: articles.value?.title || 'no-title available',
description: articles.value?.description || 'no-description available', description: articles.value?.description || 'no-description available',
image: articles.value?.image || '/not-found.jpg', image: meta?.image || '/not-found.jpg',
alt: articles.value?.alt || 'no alter data available', alt: meta?.alt || 'no alter data available',
ogImage: articles.value?.ogImage || '/not-found.jpg', ogImage: (articles?.value?.ogImage as unknown as string) || '/not-found.jpg',
date: articles.value?.date || 'not-date-available', date: meta?.date || 'not-date-available',
tags: articles.value?.tags || [], tags: meta?.tags || [],
published: articles.value?.published || false, published: meta?.published || false,
} }
}) })
@@ -33,7 +34,7 @@ useHead({
}, },
// Test on: https://developers.facebook.com/tools/debug/ or https://socialsharepreview.com/ // Test on: https://developers.facebook.com/tools/debug/ or https://socialsharepreview.com/
{ property: 'og:site_name', content: navbarData.homeTitle }, { property: 'og:site_name', content: navbarData.homeTitle },
{ hid: 'og:type', property: 'og:type', content: 'website' }, { property: 'og:type', content: 'website' },
{ {
property: 'og:url', property: 'og:url',
content: `${seoData.mySite}/${path}`, content: `${seoData.mySite}/${path}`,
@@ -78,11 +79,13 @@ useHead({
], ],
}) })
console.log(articles.value)
// Generate OG Image // Generate OG Image
defineOgImageComponent('Test', { defineOgImageComponent('Test', {
headline: 'Greetings 👋', headline: 'Riyads Blog 👋',
title: data.value.title || '', title: articles.value?.seo.title || '',
description: data.value.description || '', description: articles.value?.seo.description || '',
link: data.value.ogImage, link: data.value.ogImage,
}) })
</script> </script>

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import type { BlogPost } from '~/types/blog'
const { data } = await useAsyncData('home', () => queryContent('/blogs').sort({ _id: -1 }).find()) const { data } = await useAsyncData('all-blog-post', () => queryCollection('content').all())
const elementPerPage = ref(5) const elementPerPage = ref(5)
const pageNumber = ref(1) const pageNumber = ref(1)
@@ -10,16 +11,17 @@ const searchTest = ref('')
const formattedData = computed(() => { const formattedData = computed(() => {
return ( return (
data.value?.map((articles) => { data.value?.map((articles) => {
const meta = articles.meta as unknown as BlogPost
return { return {
path: articles._path, path: articles.path,
title: articles.title || 'no-title available', title: articles.title || 'no-title available',
description: articles.description || 'no-description available', description: articles.description || 'no-description available',
image: articles.image || '/not-found.jpg', image: meta.image || '/not-found.jpg',
alt: articles.alt || 'no alter data available', alt: meta.alt || 'no alter data available',
ogImage: articles.ogImage || '/not-found.jpg', ogImage: meta.ogImage || '/not-found.jpg',
date: articles.date || 'not-date-available', date: meta.date || 'not-date-available',
tags: articles.tags || [], tags: meta.tags || [],
published: articles.published || false, published: meta.published || false,
} }
}) || [] }) || []
) )

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BlogPost } from '@/types/blog'
const route = useRoute() const route = useRoute()
// take category from route params & make first char upper // take category from route params & make first char upper
@@ -12,23 +13,29 @@ const category = computed(() => {
}) })
const { data } = await useAsyncData(`category-data-${category.value}`, () => const { data } = await useAsyncData(`category-data-${category.value}`, () =>
queryContent('/blogs') queryCollection('content')
.where({ tags: { $contains: category.value } }) .all()
.find(), .then((articles) =>
articles.filter((article) => {
const meta = article.meta as unknown as BlogPost
return meta.tags.includes(category.value)
}),
),
) )
const formattedData = computed(() => { const formattedData = computed(() => {
return data.value?.map((articles) => { return data.value?.map((articles) => {
const meta = articles.meta as unknown as BlogPost
return { return {
path: articles._path, path: articles.path,
title: articles.title || 'no-title available', title: articles.title || 'no-title available',
description: articles.description || 'no-description available', description: articles.description || 'no-description available',
image: articles.image || '/blogs-img/blog.jpg', image: meta.image || '/blogs-img/blog.jpg',
alt: articles.alt || 'no alter data available', alt: meta.alt || 'no alter data available',
ogImage: articles.ogImage || '/blogs-img/blog.jpg', ogImage: meta.ogImage || '/blogs-img/blog.jpg',
date: articles.date || 'not-date-available', date: meta.date || 'not-date-available',
tags: articles.tags || [], tags: meta.tags || [],
published: articles.published || false, published: meta.published || false,
} }
}) })
}) })

View File

@@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { makeFirstCharUpper } from '@/utils/helper' import { makeFirstCharUpper } from '@/utils/helper'
const { data } = await useAsyncData('all-blog-post-for-category', () => const { data } = await useAsyncData('all-blog-post-by-category', () =>
queryContent('/blogs').sort({ _id: -1 }).find(), queryCollection('content').all(),
) )
const allTags = new Map() const allTags = new Map()
data.value?.forEach((blog) => { data.value?.forEach((blog) => {
const tags: Array<string> = blog.tags || [] const tags: Array<string> = (blog.meta.tags as string[]) || []
tags.forEach((tag) => { tags.forEach((tag) => {
if (allTags.has(tag)) { if (allTags.has(tag)) {
const cnt = allTags.get(tag) const cnt = allTags.get(tag)

15527
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
import { Feed } from 'feed' import { Feed } from 'feed'
import { serverQueryContent } from '#content/server'
const basePath = 'https://nurriyad.com' const basePath = 'https://nurriyad.com'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'content-type', 'text/xml') setHeader(event, 'content-type', 'text/xml')
const docs = await serverQueryContent(event).sort({ date: -1 }).find() const docs = await queryCollection(event, 'content').all()
const feed = new Feed({ const feed = new Feed({
title: "Riyad's personal blog site", title: "Riyad's personal blog site",
description: "Riyad's personal blog site", description: "Riyad's personal blog site",
@@ -23,13 +22,14 @@ export default defineEventHandler(async (event) => {
// Add the feed items // Add the feed items
docs.forEach((doc) => { docs.forEach((doc) => {
// console.log(doc)
feed.addItem({ feed.addItem({
title: doc.title || '', title: doc.title || '',
id: basePath + doc._path, id: basePath + doc.path,
link: basePath + doc._path, link: basePath + doc.path,
description: doc.description, description: doc.description,
content: doc.description, content: doc.description,
date: new Date(doc.date), date: new Date(doc.meta?.date as string),
}) })
}) })

3
server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}