diff --git a/src/module.ts b/src/module.ts index 370f442ec..dbcd0ad5d 100644 --- a/src/module.ts +++ b/src/module.ts @@ -530,12 +530,16 @@ export default defineNuxtModule({ nitro.options.runtimeConfig.public.content.wsUrl = url.replace('http', 'ws') // Watch contents - await nitro.storage.watch((event: WatchEvent, key: string) => { + await nitro.storage.watch(async (event: WatchEvent, key: string) => { // Ignore events that are not related to content if (!key.startsWith(MOUNT_PREFIX)) { return } key = key.substring(MOUNT_PREFIX.length) + + // Remove content Index + await nitro.storage.removeItem('cache:content:content-index.json') + // Broadcast a message to the server to refresh the page ws.broadcast({ event, key }) }) diff --git a/src/runtime/composables/query.ts b/src/runtime/composables/query.ts index b2adb688b..d0d51f0d5 100644 --- a/src/runtime/composables/query.ts +++ b/src/runtime/composables/query.ts @@ -35,7 +35,7 @@ export const createQueryFetch = (path?: string) => (query: Qu }) } - return $fetch(apiPath as any, { + return $fetch(apiPath as any, { method: 'GET', responseType: 'json', params: { diff --git a/src/runtime/server/api/cache.ts b/src/runtime/server/api/cache.ts index 4e22ac63b..589c4dd08 100644 --- a/src/runtime/server/api/cache.ts +++ b/src/runtime/server/api/cache.ts @@ -1,5 +1,6 @@ import { defineEventHandler } from 'h3' -import { serverQueryContent } from '../storage' +import { getContentIndex } from '../content-index' +import { cacheStorage, serverQueryContent } from '../storage' // This route is used to cache all the parsed content export default defineEventHandler(async (event) => { @@ -7,6 +8,12 @@ export default defineEventHandler(async (event) => { // Fetch all content await serverQueryContent(event).find() + // Generate Index + await getContentIndex(event) + + const navigation = await $fetch('/api/_content/navigation') + await cacheStorage.setItem('content-navigation.json', navigation) + return { generatedAt: now, generateTime: Date.now() - now diff --git a/src/runtime/server/api/navigation.ts b/src/runtime/server/api/navigation.ts index 6bd04fc55..7bc4bb7a7 100644 --- a/src/runtime/server/api/navigation.ts +++ b/src/runtime/server/api/navigation.ts @@ -1,12 +1,21 @@ import { defineEventHandler } from 'h3' -import { serverQueryContent } from '../storage' +import { cacheStorage, serverQueryContent } from '../storage' import { createNav } from '../navigation' import { ParsedContentMeta } from '../../types' import { getContentQuery } from '../../utils/query' +import { isPreview } from '../preview' export default defineEventHandler(async (event) => { const query = getContentQuery(event) + // Read from cache if not preview and there is no query + if (!isPreview(event) && Object.keys(query).length === 0) { + const cache = cacheStorage.getItem('content-navigation.json') + if (cache) { + return cache + } + } + const contents = await serverQueryContent(event, query) .where({ /** diff --git a/src/runtime/server/content-index.ts b/src/runtime/server/content-index.ts new file mode 100644 index 000000000..897ef9b99 --- /dev/null +++ b/src/runtime/server/content-index.ts @@ -0,0 +1,39 @@ +import type { CompatibilityEvent } from 'h3' +import type { ParsedContent, QueryBuilder } from '../types' +import { isPreview } from './preview' +import { cacheStorage, getContent, getContentsList, serverQueryContent } from './storage' + +export async function getContentIndex (event: CompatibilityEvent) { + let contentIndex = await cacheStorage.getItem('content-index.json') as Record + if (!contentIndex) { + // Fetch all content + const data = await serverQueryContent(event).find() + + contentIndex = data.reduce((acc, item) => { + acc[item._path!] = item._id + return acc + }, {} as Record) + + await cacheStorage.setItem('content-index.json', contentIndex) + } + + return contentIndex +} + +export async function getIndexedContentsList (event: CompatibilityEvent, query: QueryBuilder): Promise { + const params = query.params() + const path = params?.where?.find(wh => wh._path)?._path + + // Read from Index is not preview and path is string or RegExp + if (!isPreview(event) && (typeof path === 'string' || path instanceof RegExp)) { + const index = await getContentIndex(event) + const keys = Object.keys(index) + .filter(key => (path as any).test ? (path as any).test(key) : key === String(path)) + .map(key => index[key]) + + const contents = await Promise.all(keys.map(key => getContent(event, key))) + return contents as unknown as Promise + } + + return getContentsList(event) as unknown as Promise +} diff --git a/src/runtime/server/storage.ts b/src/runtime/server/storage.ts index c082c04b7..24afbfd66 100644 --- a/src/runtime/server/storage.ts +++ b/src/runtime/server/storage.ts @@ -10,6 +10,7 @@ import { createPipelineFetcher } from '../query/match/pipeline' import { transformContent } from '../transformers' import type { ModuleOptions } from '../../module' import { getPreview, isPreview } from './preview' +import { getIndexedContentsList } from './content-index' // eslint-disable-next-line import/named // @ts-ignore import { useNitroApp, useRuntimeConfig, useStorage } from '#imports' @@ -200,9 +201,7 @@ export const createServerQueryFetch = (event: CompatibilityEv query.sort({ _file: 1, $numeric: true }) } - return createPipelineFetcher( - () => getContentsList(event) as unknown as Promise - )(query) + return createPipelineFetcher(() => getIndexedContentsList(event, query))(query) } /** diff --git a/src/runtime/types.d.ts b/src/runtime/types.d.ts index a8df4b855..a4c62063e 100644 --- a/src/runtime/types.d.ts +++ b/src/runtime/types.d.ts @@ -471,7 +471,7 @@ export interface QueryBuilder { /** * Fetch sorround contents */ - findSurround(query: string | object, options?: Partial<{ before: number; after: number }>): Promise> + findSurround(query: string | QueryBuilderWhere, options?: Partial<{ before: number; after: number }>): Promise> /** * Filter contents based on locale