diff --git a/package.json b/package.json index 6d9b9028..0dce79a9 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "scripts": { "prepack": "nuxt-module-build build src/module; vite build src/app", "dev": "npm run dev:prepare && STUDIO_DEV_SERVER=http://localhost:5151 nuxi dev playground/docus", - "dev:minimal": "pnpm nuxt dev playground/minimal", + "dev:minimal": "npm run dev:prepare && STUDIO_DEV_SERVER=http://localhost:5151 nuxt dev playground/minimal", "dev:app": "vite src/app --port 5151", "dev:build": "nuxi build playground/docus", "dev:prepare": "nuxt-module-build build --stub src/module && nuxt-module-build prepare src/module && nuxi prepare playground/docus", @@ -43,7 +43,7 @@ "test:types": "vue-tsc --noEmit && cd playground/docus && vue-tsc --noEmit" }, "devDependencies": { - "@nuxt/content": "https://pkg.pr.new/@nuxt/content@22409fe", + "@nuxt/content": "https://pkg.pr.new/@nuxt/content@6d63367", "@nuxt/eslint-config": "latest", "@nuxt/kit": "^4.1.2", "@nuxt/module-builder": "latest", @@ -65,7 +65,7 @@ "zod": "^4.1.11" }, "resolutions": { - "@nuxt/content": "https://pkg.pr.new/@nuxt/content@22409fe", + "@nuxt/content": "https://pkg.pr.new/@nuxt/content@6d63367", "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@61af819", "remark-mdc": "npm:remark-mdc-edge@3.6.0-29310522.024adeb" }, diff --git a/playground/docus/nuxt.config.ts b/playground/docus/nuxt.config.ts index a641e01f..5410b647 100644 --- a/playground/docus/nuxt.config.ts +++ b/playground/docus/nuxt.config.ts @@ -10,11 +10,6 @@ export default defineNuxtConfig({ experimental: { sqliteConnector: 'native', }, - - preview: { - dev: true, - api: 'http://localhost:3000', - }, }, compatibilityDate: '2025-08-26', contentStudio: { diff --git a/playground/minimal/nuxt.config.ts b/playground/minimal/nuxt.config.ts index 5d332939..61a4627b 100644 --- a/playground/minimal/nuxt.config.ts +++ b/playground/minimal/nuxt.config.ts @@ -8,11 +8,6 @@ export default defineNuxtConfig({ experimental: { sqliteConnector: 'native', }, - - preview: { - dev: true, - api: 'http://localhost:3000', - }, }, compatibilityDate: '2025-08-26', }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38290a18..beb7857f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - '@nuxt/content': https://pkg.pr.new/@nuxt/content@22409fe + '@nuxt/content': https://pkg.pr.new/@nuxt/content@6d63367 '@nuxtjs/mdc': https://pkg.pr.new/@nuxtjs/mdc@61af819 remark-mdc: npm:remark-mdc-edge@3.6.0-29310522.024adeb @@ -54,8 +54,8 @@ importers: version: 1.17.1(db0@0.3.2(better-sqlite3@12.2.0))(idb-keyval@6.2.2)(ioredis@5.7.0) devDependencies: '@nuxt/content': - specifier: https://pkg.pr.new/@nuxt/content@22409fe - version: https://pkg.pr.new/@nuxt/content@22409fe(better-sqlite3@12.2.0)(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxt/content@6d63367 + version: https://pkg.pr.new/@nuxt/content@6d63367(better-sqlite3@12.2.0)(magicast@0.3.5) '@nuxt/eslint-config': specifier: latest version: 1.9.0(@typescript-eslint/utils@8.42.0(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2) @@ -117,8 +117,8 @@ importers: playground/docus: dependencies: '@nuxt/content': - specifier: https://pkg.pr.new/@nuxt/content@22409fe - version: https://pkg.pr.new/@nuxt/content@22409fe(better-sqlite3@12.2.0)(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxt/content@6d63367 + version: https://pkg.pr.new/@nuxt/content@6d63367(better-sqlite3@12.2.0)(magicast@0.3.5) '@nuxt/ui': specifier: 4.0.0-alpha.1 version: 4.0.0-alpha.1(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.7(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))(zod@4.1.11) @@ -138,8 +138,8 @@ importers: playground/minimal: dependencies: '@nuxt/content': - specifier: https://pkg.pr.new/@nuxt/content@22409fe - version: https://pkg.pr.new/@nuxt/content@22409fe(better-sqlite3@12.2.0)(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxt/content@6d63367 + version: https://pkg.pr.new/@nuxt/content@6d63367(better-sqlite3@12.2.0)(magicast@0.3.5) better-sqlite3: specifier: ^12.2.0 version: 12.2.0 @@ -813,8 +813,8 @@ packages: engines: {node: ^16.10.0 || >=18.0.0} hasBin: true - '@nuxt/content@https://pkg.pr.new/@nuxt/content@22409fe': - resolution: {tarball: https://pkg.pr.new/@nuxt/content@22409fe} + '@nuxt/content@https://pkg.pr.new/@nuxt/content@6d63367': + resolution: {tarball: https://pkg.pr.new/@nuxt/content@6d63367} version: 3.6.3 peerDependencies: '@electric-sql/pglite': '*' @@ -7228,7 +7228,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/content@https://pkg.pr.new/@nuxt/content@22409fe(better-sqlite3@12.2.0)(magicast@0.3.5)': + '@nuxt/content@https://pkg.pr.new/@nuxt/content@6d63367(better-sqlite3@12.2.0)(magicast@0.3.5)': dependencies: '@nuxt/kit': 4.1.2(magicast@0.3.5) '@nuxtjs/mdc': https://pkg.pr.new/@nuxtjs/mdc@61af819(magicast@0.3.5) @@ -7257,7 +7257,7 @@ snapshots: minimark: 0.2.0 minimatch: 10.0.3 nuxt-component-meta: 0.13.1(magicast@0.3.5) - nypm: 0.6.1 + nypm: 0.6.2 ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.0 @@ -9310,9 +9310,9 @@ snapshots: '@vue/language-core@3.0.6(typescript@5.9.2)': dependencies: '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.21 + '@vue/compiler-dom': 3.5.22 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.21 + '@vue/shared': 3.5.22 alien-signals: 2.0.7 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -10097,7 +10097,7 @@ snapshots: '@iconify-json/lucide': 1.2.68 '@iconify-json/simple-icons': 1.2.53 '@iconify-json/vscode-icons': 1.2.30 - '@nuxt/content': https://pkg.pr.new/@nuxt/content@22409fe(better-sqlite3@12.2.0)(magicast@0.3.5) + '@nuxt/content': https://pkg.pr.new/@nuxt/content@6d63367(better-sqlite3@12.2.0)(magicast@0.3.5) '@nuxt/image': 1.11.0(db0@0.3.2(better-sqlite3@12.2.0))(idb-keyval@6.2.2)(ioredis@5.7.0)(magicast@0.3.5) '@nuxt/kit': 4.1.2(magicast@0.3.5) '@nuxt/ui': 4.0.0(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(better-sqlite3@12.2.0))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.7(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))(zod@4.1.11) @@ -11704,7 +11704,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 diff --git a/src/app/src/components/AppFooter.vue b/src/app/src/components/AppFooter.vue index cb87f42e..cbc7921c 100644 --- a/src/app/src/components/AppFooter.vue +++ b/src/app/src/components/AppFooter.vue @@ -17,7 +17,9 @@ const showTechnicalMode = computed({ const repositoryUrl = computed(() => { switch (host.repository.provider) { case 'github': - return `https://github.com/${host.repository.owner}/${host.repository.repo}/tree/${host.repository.branch}` + return host.repository?.owner + ? `https://github.com/${host.repository.owner}/${host.repository.repo}/tree/${host.repository.branch}` + : '' default: return '' } @@ -26,12 +28,15 @@ const repositoryUrl = computed(() => { const userMenuItems = computed(() => [ [{ slot: 'view-mode' as const, - }, { - label: `${host.repository.owner}/${host.repository.repo}`, - icon: 'i-simple-icons:github', - to: repositoryUrl.value, - target: '_blank', - }], + }, repositoryUrl.value + ? { + label: `${host.repository.owner}/${host.repository.repo}`, + icon: 'i-simple-icons:github', + to: repositoryUrl.value, + target: '_blank', + } + : undefined, + ].filter(Boolean), [{ label: 'Sign out', icon: 'i-lucide-log-out', diff --git a/src/app/src/utils/content.ts b/src/app/src/utils/content.ts index 9cf7012a..0de174f7 100644 --- a/src/app/src/utils/content.ts +++ b/src/app/src/utils/content.ts @@ -17,7 +17,7 @@ export const contentFileExtensions = [ ContentFileExtension.JSON, ] as const -const reservedKeys = ['id', 'stem', 'extension', '__hash__', 'path', 'body', 'meta'] +const reservedKeys = ['id', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody'] export function pickReservedKeysFromDocument(document: DatabaseItem) { return pick(document, reservedKeys) diff --git a/src/module/src/module.ts b/src/module/src/module.ts index 96a802e2..68e3d4a7 100644 --- a/src/module/src/module.ts +++ b/src/module/src/module.ts @@ -100,6 +100,9 @@ export default defineNuxtModule({ }, }) extendViteConfig((config) => { + config.define ||= {} + config.define['import.meta.preview'] = true + config.optimizeDeps ||= {} config.optimizeDeps.include = [ ...(config.optimizeDeps.include || []), @@ -170,14 +173,10 @@ export default defineNuxtModule({ route: '/__nuxt_content/studio/auth/session', handler: runtime('./server/routes/auth/session.delete'), }) - addServerHandler({ - route: '/__nuxt_content/studio', - handler: runtime('./server/routes/admin'), - }) - addServerHandler({ - route: '/sw.js', - handler: runtime('./server/routes/sw'), - }) + addServerHandler({ route: '/__nuxt_content/studio', handler: runtime('./server/routes/admin') }) + // Register meta route for studio + addServerHandler({ route: '/__nuxt_content/studio/meta', handler: runtime('./server/routes/meta') }) + addServerHandler({ route: '/sw.js', handler: runtime('./server/routes/sw') }) // addServerHandler({ // route: '/__nuxt_content/studio/auth/google', // handler: runtime('./server/routes/auth/google.get'), diff --git a/src/module/src/runtime/composables/useMeta.ts b/src/module/src/runtime/composables/useMeta.ts index 38e3fe98..fb4f0889 100644 --- a/src/module/src/runtime/composables/useMeta.ts +++ b/src/module/src/runtime/composables/useMeta.ts @@ -8,8 +8,9 @@ export const useHostMeta = createSharedComposable(() => { async function fetch() { // TODO: look into this approach and consider possible refactors - const data = await $fetch<{ components: ComponentMeta[] }>('/__preview.json') - .catch(() => ({ components: [] })) + const data = await $fetch<{ components: ComponentMeta[] }>('/__nuxt_content/studio/meta', { + headers: { 'content-type': 'application/json' }, + }).catch(() => ({ components: [] })) componentsMeta.value = data.components || [] } diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index 3d19c732..968e8a83 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -1,10 +1,10 @@ import { ref } from 'vue' import { ensure } from './utils/ensure' -import type { CollectionItemBase, DatabaseAdapter } from '@nuxt/content' +import type { CollectionItemBase, CollectionSource, DatabaseAdapter } from '@nuxt/content' import type { ContentDatabaseAdapter } from '../types/content' import { getCollectionByFilePath, generateIdFromFsPath, createCollectionDocument, generateRecordDeletion, generateRecordInsert, getCollectionInfo } from './utils/collection' -import { kebabCase } from 'lodash' -import type { UseStudioHost, StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app' +import { kebabCase } from 'scule' +import type { StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app' import type { RouteLocationNormalized, Router } from 'vue-router' import { generateDocumentFromContent } from './utils/content' // @ts-expect-error queryCollection is not defined in .nuxt/imports.d.ts @@ -29,12 +29,6 @@ function getSidebarWidth(): number { return sidebarWidth } -declare global { - interface Window { - useStudioHost: UseStudioHost - } -} - // TODO: Move styles and these logics out of host (Maybe have a injectCSS util in host) function getHostStyles(): Record> & { css?: string } { const currentWidth = getSidebarWidth() @@ -91,7 +85,14 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH } function useContentCollections() { - return useContent().collections + return Object.fromEntries( + Object.entries(useContent().collections).filter(([, collection]) => { + if (!collection.source.length || collection.source.some((source: CollectionSource) => source.repository || (source as unknown as { _custom: boolean })._custom)) { + return false + } + return true + }), + ) } function useContentCollectionQuery(collection: string) { diff --git a/src/module/src/runtime/server/routes/meta.ts b/src/module/src/runtime/server/routes/meta.ts new file mode 100644 index 00000000..b15bcf58 --- /dev/null +++ b/src/module/src/runtime/server/routes/meta.ts @@ -0,0 +1,55 @@ +import type { ComponentMeta } from 'vue-component-meta' +import { eventHandler, useSession } from 'h3' +import type { RuntimeConfig } from '@nuxt/content' +import { useRuntimeConfig, useAppConfig, createError } from '#imports' +// @ts-expect-error import does exist +import components from '#nuxt-component-meta/nitro' +import { collections, gitInfo, appConfigSchema } from '#content/preview' + +interface NuxtComponentMeta { + pascalName: string + filePath: string + meta: ComponentMeta + global: boolean +} + +export default eventHandler(async (event) => { + const session = await useSession(event, { + name: 'content-studio-session', + password: useRuntimeConfig(event).contentStudio?.auth?.sessionSecret, + }) + + if (!session?.data?.user) { + throw createError({ + statusCode: 404, + message: 'Not found', + }) + } + + const mappedComponents = (Object.values(components) as NuxtComponentMeta[]) + .map(({ pascalName, filePath, meta }) => { + return { + name: pascalName, + path: filePath, + meta: { + props: meta.props, + slots: meta.slots, + events: meta.events, + }, + } + }) + + const appConfig = useAppConfig() + const runtimeConfig = useRuntimeConfig() + const { content } = runtimeConfig + const { version } = content as RuntimeConfig['content'] + + return { + version, + gitInfo, + collections, + appConfigSchema, + appConfig, + components: mappedComponents, + } +}) diff --git a/src/module/src/runtime/utils/content.ts b/src/module/src/runtime/utils/content.ts index 71932390..851bca1b 100644 --- a/src/module/src/runtime/utils/content.ts +++ b/src/module/src/runtime/utils/content.ts @@ -12,7 +12,7 @@ import type { MDCElement } from '@nuxtjs/mdc' import type { MarkdownRoot } from '@nuxt/content' export function removeReservedKeysFromDocument(document: DatabaseItem) { - const result = omit(document, ['id', 'stem', 'extension', '__hash__', 'path', 'body', 'meta']) + const result = omit(document, ['id', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody']) // Default value of navigation is true, so we can safely remove it if (result.navigation === true) { Reflect.deleteProperty(result, 'navigation')