@xleddyl/nuxt-cms 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
4
4
 
5
5
  - **Zero extra infrastructure**: the CMS runs inside your app's Nitro server; you deploy one thing.
6
6
  - **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
7
- - **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage), single-admin auth from env credentials.
7
+ - **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, or a read-only local mode), single-admin auth from env credentials.
8
8
  - **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination.
9
9
  - **SQLite, Postgres or libSQL/Turso**: a local file database by default, one config line to switch (including remote SQLite over the network).
10
10
 
@@ -78,7 +78,7 @@ Full documentation lives in [`docs/`](docs/README.md):
78
78
  - [Schema](docs/schema.md) — `defineCmsConfig`, entries, field types, relations, blocks, i18n.
79
79
  - [Querying content](docs/querying.md) — GraphQL API, `useCms` / `$cmsQuery`, filters, sorting, pagination.
80
80
  - [Admin panel & security](docs/admin.md) — pages, authentication, sessions, admin REST API.
81
- - [Media](docs/media.md) — S3-compatible storage, upload flow, allowed file types.
81
+ - [Media](docs/media.md) — S3-compatible storage or read-only local mode, upload flow, allowed file types.
82
82
 
83
83
  ## LLM guide
84
84
 
package/dist/module.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
+ import { MediaStorageMode } from '../dist/runtime/shared/index.js';
2
3
 
3
4
  type Dialect = 'sqlite' | 'postgres';
4
5
  type Driver = Dialect | 'libsql';
@@ -17,6 +18,7 @@ interface ModuleOptions {
17
18
  authToken?: string;
18
19
  };
19
20
  media: {
21
+ storage: MediaStorageMode;
20
22
  endpoint: string;
21
23
  region: string;
22
24
  bucket: string;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.22",
4
+ "version": "0.1.23",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -437,6 +437,7 @@ const module$1 = defineNuxtModule({
437
437
  authToken: ""
438
438
  },
439
439
  media: {
440
+ storage: "s3",
440
441
  endpoint: "",
441
442
  region: "auto",
442
443
  bucket: "",
@@ -464,6 +465,7 @@ const module$1 = defineNuxtModule({
464
465
  ]);
465
466
  nuxt.options.runtimeConfig.public.cms = {
466
467
  mediaBaseUrl: options.media.publicBaseUrl,
468
+ mediaStorage: options.media.storage,
467
469
  i18n: options.i18n
468
470
  };
469
471
  logger.info(
@@ -643,6 +645,7 @@ const module$1 = defineNuxtModule({
643
645
  ...existingConfig.graphql ?? {}
644
646
  },
645
647
  media: {
648
+ storage: options.media.storage,
646
649
  endpoint: options.media.endpoint,
647
650
  region: options.media.region,
648
651
  bucket: options.media.bucket,
@@ -654,6 +657,7 @@ const module$1 = defineNuxtModule({
654
657
  };
655
658
  nuxt.options.runtimeConfig.public.cms = {
656
659
  mediaBaseUrl: options.media.publicBaseUrl,
660
+ mediaStorage: options.media.storage,
657
661
  i18n: options.i18n
658
662
  };
659
663
  addTypeTemplate(
@@ -8,6 +8,7 @@
8
8
 
9
9
  <div v-else class="flex flex-col gap-5">
10
10
  <CmsMediaUpload
11
+ v-if="!readOnly"
11
12
  :multiple="!selectable"
12
13
  :dense="selectable"
13
14
  :media-type="mediaType"
@@ -89,6 +90,7 @@
89
90
  class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
90
91
  >
91
92
  <CmsButton
93
+ v-if="!readOnly"
92
94
  icon="pencil-square"
93
95
  size="xs"
94
96
  color="neutral"
@@ -107,6 +109,7 @@
107
109
  @click="copy(item)"
108
110
  />
109
111
  <CmsButton
112
+ v-if="!readOnly"
110
113
  icon="trash"
111
114
  size="xs"
112
115
  color="error"
@@ -144,11 +147,12 @@
144
147
  <CmsEmptyState
145
148
  v-else
146
149
  icon="photo"
147
- title="No media yet"
148
- body="Files you upload will show up here."
150
+ :title="readOnly ? 'No media' : 'No media yet'"
151
+ :body="readOnly ? 'No media registered.' : 'Files you upload will show up here.'"
149
152
  />
150
153
 
151
154
  <CmsModal
155
+ v-if="!readOnly"
152
156
  :open="!!editing"
153
157
  :title="editing ? mediaFilename(editing.key) : ''"
154
158
  :ui="CMS_MODAL_UI"
@@ -184,6 +188,7 @@
184
188
  import { computed, onMounted, ref } from "#imports";
185
189
  import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
186
190
  import { useCmsConfirm } from "../../composables/cms-confirm";
191
+ import { useCmsRuntime } from "../../composables/cms-runtime";
187
192
  import { useCmsToast } from "../../composables/cms-toast";
188
193
  import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
189
194
  const filterLabels = {
@@ -199,6 +204,8 @@ const props = defineProps({
199
204
  });
200
205
  const emit = defineEmits(["select"]);
201
206
  const toast = useCmsToast();
207
+ const runtime = useCmsRuntime();
208
+ const readOnly = computed(() => runtime.mediaStorage === "local");
202
209
  const endpoint = "/api/cms/admin/media";
203
210
  const items = ref([]);
204
211
  const loading = ref(true);
@@ -1,5 +1,6 @@
1
- import type { CmsI18n } from '../../shared/index.js';
1
+ import type { CmsI18n, MediaStorageMode } from '../../shared/index.js';
2
2
  export declare function useCmsRuntime(): {
3
3
  mediaBaseUrl: string;
4
+ mediaStorage: MediaStorageMode;
4
5
  i18n: CmsI18n;
5
6
  };
@@ -3,7 +3,12 @@ import { z } from "zod";
3
3
  import { useDb } from "#cms-db";
4
4
  import { cms_media } from "#cms-tables";
5
5
  import { objectKeySchema } from "../../shared/validation.js";
6
- import { assertUploadContentType, toMediaItem, useMediaConfig } from "../utils/media.js";
6
+ import {
7
+ assertMediaWritable,
8
+ assertUploadContentType,
9
+ toMediaItem,
10
+ useMediaConfig
11
+ } from "../utils/media.js";
7
12
  import { requireAdmin } from "../utils/require-admin.js";
8
13
  const bodySchema = z.object({
9
14
  key: objectKeySchema,
@@ -16,7 +21,8 @@ const bodySchema = z.object({
16
21
  });
17
22
  export default defineEventHandler(async (event) => {
18
23
  await requireAdmin(event);
19
- const { publicUrl } = useMediaConfig(event);
24
+ const { media, publicUrl } = useMediaConfig(event);
25
+ assertMediaWritable(media);
20
26
  const body = await readValidatedBody(event, bodySchema.parse);
21
27
  if (body.mime) assertUploadContentType(body.mime);
22
28
  const values = {
@@ -3,7 +3,7 @@ import { createError, defineEventHandler, readValidatedBody } from "h3";
3
3
  import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
- import { toMediaItem, useMediaConfig } from "../utils/media.js";
6
+ import { assertMediaWritable, toMediaItem, useMediaConfig } from "../utils/media.js";
7
7
  import { requireAdmin } from "../utils/require-admin.js";
8
8
  const bodySchema = z.object({
9
9
  id: z.number().int().positive(),
@@ -12,7 +12,8 @@ const bodySchema = z.object({
12
12
  });
13
13
  export default defineEventHandler(async (event) => {
14
14
  await requireAdmin(event);
15
- const { publicUrl } = useMediaConfig(event);
15
+ const { media, publicUrl } = useMediaConfig(event);
16
+ assertMediaWritable(media);
16
17
  const body = await readValidatedBody(event, bodySchema.parse);
17
18
  const updates = {};
18
19
  if (body.alt !== void 0) updates.alt = body.alt;
@@ -1,7 +1,8 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import { AwsClient } from 'aws4fetch';
3
- import type { MediaItem } from '../../shared/index.js';
3
+ import type { MediaItem, MediaStorageMode } from '../../shared/index.js';
4
4
  interface MediaConfig {
5
+ storage: MediaStorageMode;
5
6
  endpoint: string;
6
7
  region: string;
7
8
  bucket: string;
@@ -11,6 +12,8 @@ interface MediaConfig {
11
12
  }
12
13
  export declare function assertUploadContentType(contentType: string): void;
13
14
  export declare function encodeKey(key: string): string;
15
+ export declare function assertMediaConfigured(media: MediaConfig): void;
16
+ export declare function assertMediaWritable(media: MediaConfig): void;
14
17
  export declare function useMediaConfig(event: H3Event): {
15
18
  media: MediaConfig;
16
19
  publicUrl: (key: string) => string | null;
@@ -32,21 +32,34 @@ export function assertUploadContentType(contentType) {
32
32
  export function encodeKey(key) {
33
33
  return key.split("/").map(encodeURIComponent).join("/");
34
34
  }
35
- export function useMediaConfig(event) {
36
- const config = useRuntimeConfig(event);
37
- const media = config.cms.media;
35
+ export function assertMediaConfigured(media) {
36
+ if (media.storage !== "s3") return;
38
37
  if (!media.endpoint || !media.bucket || !media.accessKeyId || !media.secretAccessKey) {
39
38
  throw createError({
40
39
  statusCode: 501,
41
40
  statusMessage: "Media storage is not configured (cms.media in nuxt.config)"
42
41
  });
43
42
  }
43
+ }
44
+ export function assertMediaWritable(media) {
45
+ if (media.storage === "local") {
46
+ throw createError({
47
+ statusCode: 501,
48
+ statusMessage: "Media storage is local; the media library is read-only"
49
+ });
50
+ }
51
+ }
52
+ export function useMediaConfig(event) {
53
+ const config = useRuntimeConfig(event);
54
+ const media = config.cms.media;
55
+ assertMediaConfigured(media);
44
56
  const { mediaBaseUrl } = config.public.cms;
45
57
  const publicUrl = (key) => mediaPublicUrl(mediaBaseUrl, key);
46
58
  return { media, publicUrl };
47
59
  }
48
60
  export function useMediaStorage(event) {
49
61
  const { media, publicUrl } = useMediaConfig(event);
62
+ assertMediaWritable(media);
50
63
  const client = new AwsClient({
51
64
  accessKeyId: media.accessKeyId,
52
65
  secretAccessKey: media.secretAccessKey,
@@ -6,6 +6,7 @@ export interface CmsI18n {
6
6
  locales: string[];
7
7
  defaultLocale: string;
8
8
  }
9
+ export type MediaStorageMode = 's3' | 'local';
9
10
  export declare const MEDIA_TYPES: readonly ["image", "video", "file"];
10
11
  export type MediaType = (typeof MEDIA_TYPES)[number];
11
12
  export declare function mediaTypeForKey(key: string): MediaType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",