mktcms 0.1.6 → 0.1.7

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
@@ -1,13 +1,6 @@
1
- <!--
2
- Get your module up and running quickly.
1
+ # Simple CMS module for Nuxt (pre-alpha)
3
2
 
4
- Find and replace all on all files (CMD+SHIFT+F):
5
- - Name: My Module
6
- - Package name: mktcms
7
- - Description: My new Nuxt module
8
- -->
9
-
10
- # Simple CMS module for Nuxt
3
+ This module is my personal, minimalist, opinionated, independent alternative to @nuxt/content and to a large portion of the WordPress projects I’ve worked on.
11
4
 
12
5
  [![npm version][npm-version-src]][npm-version-href]
13
6
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -20,21 +13,80 @@ Find and replace all on all files (CMD+SHIFT+F):
20
13
 
21
14
  ## Features
22
15
 
23
- <!-- Highlight some of the features your module provide here -->
24
- - &nbsp;Foo
25
- - 🚠 &nbsp;Bar
26
- - 🌲 &nbsp;Baz
27
-
28
- ## Quick Setup
16
+ - S3 bucket explorer/editor at `/admin`
17
+ - API routes at `/api/admin`
18
+ - `ADMIN_AUTH_KEY` env var to set a password
19
+ - `useContent` composable
29
20
 
30
- Install the module to your Nuxt application with one command:
21
+ ## Setup
31
22
 
32
23
  ```bash
33
24
  npx nuxi module add mktcms
34
25
  ```
35
26
 
36
- That's it! You can now use My Module in your Nuxt app ✨
27
+ ```bash
28
+ MKTCMS_ADMIN_AUTH_KEY="your-admin-auth-key"
29
+ MKTCMS_S3_ACCESS_KEY_ID=your-s3-access-key-id
30
+ MKTCMS_S3_SECRET_ACCESS_KEY=your-s3-secret-access-key
31
+ MKTCMS_S3_BUCKET=your-s3-bucket-name
32
+ MKTCMS_S3_REGION=your-s3-bucket-region
33
+ MKTCMS_S3_PREFIX="your-project"
34
+ MKTCMS_SMTP_HOST="your-smtp-host"
35
+ MKTCMS_SMTP_PORT=465
36
+ MKTCMS_SMTP_SECURE=true
37
+ MKTCMS_SMTP_USER="your-smtp-user"
38
+ MKTCMS_SMTP_PASS="your-smtp-pass"
39
+ MKTCMS_MAILER_FROM="your-mailer-from-address"
40
+ MKTCMS_MAILER_TO="your-mailer-to-address"
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Assuming json files in S3 like `your-project:articles:article-1.json`:
46
+
47
+ ```vue
48
+ <script setup lang="ts">
49
+ import { useContent } from 'mktcms'
50
+
51
+ type Article = {
52
+ id: string
53
+ title: string
54
+ content: string
55
+ }
56
+
57
+ const { data: articles } = await useContent<Article[]>('articles')
58
+ </script>
59
+
60
+ <template>
61
+ <article v-for="article in articles" :key="article.id">
62
+ <h2>{{ article.title }}</h2>
63
+ <p>{{ article.content }}</p>
64
+ </article>
65
+ </template>
66
+ ```
67
+
68
+ For a specific article:
37
69
 
70
+ ```vue
71
+ <script setup lang="ts">
72
+ import { useContent } from 'mktcms'
73
+
74
+ type Article = {
75
+ id: string
76
+ title: string
77
+ content: string
78
+ }
79
+
80
+ const { data: article } = await useContent<Article>('articles/article-1.json')
81
+ </script>
82
+
83
+ <template>
84
+ <article>
85
+ <h2>{{ article.title }}</h2>
86
+ <p>{{ article.content }}</p>
87
+ </article>
88
+ </template>
89
+ ```
38
90
 
39
91
  ## Contribution
40
92
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.1.6",
4
+ "version": "0.1.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addServerPlugin, addServerHandler, extendPages } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addServerImportsDir, addServerPlugin, addServerHandler, extendPages } from '@nuxt/kit';
2
2
  import defu from 'defu';
3
3
 
4
4
  const module$1 = defineNuxtModule({
@@ -10,13 +10,21 @@ const module$1 = defineNuxtModule({
10
10
  const resolver = createResolver(import.meta.url);
11
11
  _nuxt.options.runtimeConfig.mktcms = defu((_nuxt.options.runtimeConfig.mktcms, {
12
12
  adminAuthKey: "",
13
- filesPathPrefix: "",
14
13
  s3AccessKey: "",
15
14
  s3SecretKey: "",
16
15
  s3Endpoint: "",
17
16
  s3Bucket: "",
18
- s3Region: ""
17
+ s3Region: "",
18
+ s3Prefix: "",
19
+ smtpHost: "",
20
+ smtpPort: 465,
21
+ smtpSecure: true,
22
+ smtpUser: "",
23
+ smtpPass: "",
24
+ mailerFrom: "",
25
+ mailerTo: ""
19
26
  }));
27
+ addServerImportsDir(resolver.resolve("runtime/server/utils"));
20
28
  addServerPlugin(resolver.resolve("./runtime/server/plugins/storage"));
21
29
  addServerHandler({
22
30
  middleware: true,
@@ -30,6 +38,11 @@ const module$1 = defineNuxtModule({
30
38
  route: "/api/admin/logout",
31
39
  handler: resolver.resolve("./runtime/server/api/admin/logout")
32
40
  });
41
+ addServerHandler({
42
+ route: "/api/admin/content/:path",
43
+ method: "post",
44
+ handler: resolver.resolve("./runtime/server/api/admin/content/[path].post")
45
+ });
33
46
  addServerHandler({
34
47
  route: "/api/admin/content/upload",
35
48
  handler: resolver.resolve("./runtime/server/api/admin/content/upload")
@@ -39,15 +52,25 @@ const module$1 = defineNuxtModule({
39
52
  handler: resolver.resolve("./runtime/server/api/content/list")
40
53
  });
41
54
  addServerHandler({
42
- route: "/api/content/[path]",
55
+ route: "/api/content/:path",
43
56
  handler: resolver.resolve("./runtime/server/api/content/[path]")
44
57
  });
45
58
  extendPages((pages) => {
46
59
  pages.push({
47
60
  name: "Admin Dashboard",
48
- path: "/admin/:path(.*)?",
61
+ path: "/admin/:path?",
49
62
  file: resolver.resolve("./runtime/app/pages/admin/index.vue")
50
63
  });
64
+ pages.push({
65
+ name: "Admin Editor",
66
+ path: "/admin/edit/:path",
67
+ file: resolver.resolve("./runtime/app/pages/admin/edit/[path].vue")
68
+ });
69
+ pages.push({
70
+ name: "Admin New Content",
71
+ path: "/admin/new",
72
+ file: resolver.resolve("./runtime/app/pages/admin/new.vue")
73
+ });
51
74
  pages.push({
52
75
  name: "Admin Login",
53
76
  path: "/admin/login",
@@ -5,5 +5,5 @@
5
5
  </template>
6
6
 
7
7
  <style>
8
- body{font-family:Arial,sans-serif;margin:0}#mktcms-admin{background-color:#f9f9f9;margin:0 auto;max-width:800px;padding:20px}#mktcms-admin h1{color:#333}#mktcms-admin button{background-color:#3cb371;border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:16px;padding:10px 20px;transition:background-color .3s}#mktcms-admin button:hover{background-color:#45a049}#mktcms-admin input[type=email],#mktcms-admin input[type=password],#mktcms-admin input[type=text],#mktcms-admin textarea{border:1px solid #ccc;border-radius:5px;font-size:16px;padding:10px}#mktcms-admin .breadcrumbs{color:#888;font-size:1.5rem;margin:20px 0}#mktcms-admin .breadcrumbs a{color:#888;text-decoration:none}#mktcms-admin .breadcrumbs a:hover{text-decoration:underline}#mktcms-admin .dirs,#mktcms-admin .files{display:flex;flex-direction:column;gap:8px}#mktcms-admin .dirs a,#mktcms-admin .files a{border-radius:4px;display:block;padding:8px 12px;text-decoration:none}#mktcms-admin .dirs a:hover,#mktcms-admin .files a:hover{text-decoration:underline}#mktcms-admin .files a{background-color:#fff;color:#555}#mktcms-admin .dirs a{background-color:#555;color:#fff;display:flex;justify-content:space-between}
8
+ *{box-sizing:border-box}body{font-family:Arial,sans-serif;margin:0}#mktcms-admin{background-color:#f9f9f9;margin:0 auto;max-width:800px;padding:20px}#mktcms-admin h1{color:#333}#mktcms-admin button{background-color:#3cb371;border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:16px;padding:10px 20px;transition:background-color .3s}#mktcms-admin button:hover{background-color:#45a049}#mktcms-admin input[type=email],#mktcms-admin input[type=password],#mktcms-admin input[type=text],#mktcms-admin textarea{border:1px solid #ccc;border-radius:5px;font-size:16px;padding:10px}#mktcms-admin .breadcrumbs{color:#888;font-size:1.5rem;margin:20px 0}#mktcms-admin .breadcrumbs a{color:#888;text-decoration:none}#mktcms-admin .breadcrumbs a:hover{text-decoration:underline}#mktcms-admin .dirs,#mktcms-admin .files{display:flex;flex-direction:column;gap:8px}#mktcms-admin .dirs a,#mktcms-admin .files a{border-radius:4px;display:block;padding:8px 12px;text-decoration:none}#mktcms-admin .dirs a:hover,#mktcms-admin .files a:hover{text-decoration:underline}#mktcms-admin .files a{background-color:#fff;color:#555}#mktcms-admin .dirs a{background-color:#555;color:#fff;display:flex;justify-content:space-between}
9
9
  </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,42 @@
1
+ <script setup>
2
+ import { useFetch, useRoute } from "#app";
3
+ const path = useRoute().params.path || "";
4
+ const pathParts = path.split(":");
5
+ const { data: content } = await useFetch(`/api/content/${path}`);
6
+ async function saveContent() {
7
+ await $fetch(`/api/admin/content/${path}`, {
8
+ method: "POST",
9
+ body: { content: content.value }
10
+ });
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <div>
16
+ <div class="breadcrumbs">
17
+ <a href="/admin">Hauptverzeichnis</a>
18
+ <span
19
+ v-for="(part, index) in pathParts"
20
+ :key="index"
21
+ >
22
+ /
23
+ <a :href="`/admin/${pathParts.slice(0, index + 1).join(':')}`">
24
+ {{ part }}
25
+ </a>
26
+ </span>
27
+ </div>
28
+
29
+ <div v-if="content !== void 0">
30
+ <textarea
31
+ v-model="content"
32
+ style="width: 100%; resize: vertical;"
33
+ />
34
+ <button
35
+ style="margin-top: 10px;"
36
+ @click="saveContent"
37
+ >
38
+ Speichern
39
+ </button>
40
+ </div>
41
+ </div>
42
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -2,7 +2,7 @@
2
2
  import { useFetch, useRoute } from "#app";
3
3
  import { computed } from "vue";
4
4
  const path = useRoute().params.path || "";
5
- const pathParts = path.split("/");
5
+ const pathParts = path.split(":");
6
6
  const { data: keys } = await useFetch("/api/content/list", {
7
7
  query: { path }
8
8
  });
@@ -37,7 +37,7 @@ const dirs = computed(() => {
37
37
  :key="index"
38
38
  >
39
39
  /
40
- <a :href="`/admin/${pathParts.slice(0, index + 1).join('/')}`">
40
+ <a :href="`/admin/${pathParts.slice(0, index + 1).join(':')}`">
41
41
  {{ part }}
42
42
  </a>
43
43
  </span>
@@ -50,7 +50,7 @@ const dirs = computed(() => {
50
50
  <a
51
51
  v-for="file in files"
52
52
  :key="file"
53
- :href="`/admin/${path ? path + '/' : ''}${file}`"
53
+ :href="`/admin/edit/${path ? path + ':' : ''}${file}`"
54
54
  >{{ file }}</a>
55
55
  </div>
56
56
 
@@ -62,10 +62,10 @@ const dirs = computed(() => {
62
62
  <a
63
63
  v-for="dir in dirs"
64
64
  :key="dir"
65
- :href="`/admin/${path ? path + '/' : ''}${dir}`"
65
+ :href="`/admin/${path ? path + ':' : ''}${dir}`"
66
66
  >
67
67
  <span>
68
- {{ dir.replace(/:/g, "/").replace(path, "") }}
68
+ {{ dir }}
69
69
  </span>
70
70
  <span>
71
71
  <svg
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <div style="display: flex; justify-content: space-between; align-items: center;">
3
+ <h1>
4
+ <NuxtLink
5
+ to="/admin"
6
+ style="text-decoration: none; color: inherit;"
7
+ >
8
+ Website Verwaltung
9
+ </NuxtLink>
10
+ </h1>
11
+ <NuxtLink
12
+ to="/admin/new"
13
+ style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; margin-left: auto; margin-right: 10px;"
14
+ >
15
+ Neuer Inhalt
16
+ </NuxtLink>
17
+ <NuxtLink
18
+ external
19
+ to="/api/admin/logout"
20
+ style="background-color: #ccc; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"
21
+ >
22
+ Abmelden
23
+ </NuxtLink>
24
+ </div>
25
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import Admin from "../../../components/admin.vue";
3
+ import Header from "../../../components/header.vue";
4
+ import Editor from "../../../components/content/editor.vue";
5
+ </script>
6
+
7
+ <template>
8
+ <Admin>
9
+ <Header />
10
+ <Editor />
11
+ </Admin>
12
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -1,22 +1,12 @@
1
1
  <script setup>
2
2
  import Admin from "../../components/admin.vue";
3
+ import Header from "../../components/header.vue";
3
4
  import Content from "../../components/content/index.vue";
4
- import ContentUpload from "../../components/content/upload.vue";
5
5
  </script>
6
6
 
7
7
  <template>
8
8
  <Admin>
9
- <div style="display: flex; justify-content: space-between; align-items: center;">
10
- <h1>Website Verwaltung</h1>
11
- <a
12
- href="/api/admin/logout"
13
- style="background-color: #ccc; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"
14
- >
15
- Abmelden
16
- </a>
17
- </div>
18
-
19
- <ContentUpload />
9
+ <Header />
20
10
  <Content />
21
11
  </Admin>
22
12
  </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import Admin from "../../components/admin.vue";
3
+ import Header from "../../components/header.vue";
4
+ import ContentUpload from "../../components/content/upload.vue";
5
+ </script>
6
+
7
+ <template>
8
+ <Admin>
9
+ <Header />
10
+ <ContentUpload />
11
+ </Admin>
12
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { defineEventHandler, getValidatedRouterParams, readValidatedBody } from "h3";
3
+ import { useRuntimeConfig, useStorage } from "nitropack/runtime";
4
+ const paramsSchema = z.object({
5
+ path: z.string().min(1)
6
+ });
7
+ const bodySchema = z.object({
8
+ content: z.string()
9
+ });
10
+ export default defineEventHandler(async (event) => {
11
+ const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
12
+ const { content } = await readValidatedBody(event, (body) => bodySchema.parse(body));
13
+ const { mktcms: { s3Prefix } } = useRuntimeConfig();
14
+ const fullPath = s3Prefix + ":" + path;
15
+ const storage = useStorage("content");
16
+ const file = await storage.setItem(fullPath, content);
17
+ return file;
18
+ });
@@ -9,7 +9,7 @@ const querySchema = z.object({
9
9
  });
10
10
  export default defineEventHandler(async (event) => {
11
11
  const form = await readMultipartFormData(event);
12
- const { mktcms: { filesPathPrefix } } = useRuntimeConfig();
12
+ const { mktcms: { s3Prefix } } = useRuntimeConfig();
13
13
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
14
14
  const sanePath = path ? path.replace(/^\//, "").replace(/\/$/, "") : void 0;
15
15
  if (!form) {
@@ -39,7 +39,7 @@ export default defineEventHandler(async (event) => {
39
39
  statusMessage: "Invalid file type. Only PDF, JPG, JPEG, and PNG files are allowed."
40
40
  });
41
41
  }
42
- const filePath = [filesPathPrefix, sanePath, sanitizeFilename(file.filename)].filter(Boolean).join("/");
42
+ const filePath = [s3Prefix, sanePath, sanitizeFilename(file.filename)].filter(Boolean).join("/");
43
43
  await useStorage("content").setItemRaw(filePath, Buffer.from(file.data));
44
44
  const returnFileName = filePath;
45
45
  return { success: true, path: returnFileName };
@@ -1,13 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { createError, defineEventHandler, getValidatedRouterParams } from "h3";
3
- import { useStorage } from "nitropack/runtime";
3
+ import { useRuntimeConfig, useStorage } from "nitropack/runtime";
4
4
  const paramsSchema = z.object({
5
5
  path: z.string().min(1)
6
6
  });
7
7
  export default defineEventHandler(async (event) => {
8
8
  const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
9
+ const { mktcms: { s3Prefix } } = useRuntimeConfig();
10
+ const fullPath = s3Prefix + ":" + path;
9
11
  const storage = useStorage("content");
10
- const file = await storage.getItem(path);
12
+ const file = await storage.getItem(fullPath);
11
13
  if (!file) {
12
14
  throw createError({
13
15
  statusCode: 404,
@@ -6,8 +6,8 @@ const querySchema = z.object({
6
6
  });
7
7
  export default defineEventHandler(async (event) => {
8
8
  const { path } = await getValidatedQuery(event, (query) => querySchema.parse(query));
9
- const { mktcms: { filesPathPrefix } } = useRuntimeConfig();
9
+ const { mktcms: { s3Prefix } } = useRuntimeConfig();
10
10
  const storage = useStorage("content");
11
- const keys = await storage.getKeys(filesPathPrefix + (path ? ":" + path : ""));
12
- return keys.map((key) => key.replace(filesPathPrefix + ":", ""));
11
+ const keys = await storage.getKeys(s3Prefix + (path ? ":" + path : ""));
12
+ return keys.map((key) => key.replace(s3Prefix + ":", ""));
13
13
  });
@@ -0,0 +1,4 @@
1
+ export default function sendMail({ subject, fields }: {
2
+ subject: string;
3
+ fields: Record<string, any>;
4
+ }): Promise<import("nodemailer/lib/smtp-transport").SentMessageInfo>;
@@ -0,0 +1,22 @@
1
+ import nodemailer from "nodemailer";
2
+ import { useRuntimeConfig } from "#imports";
3
+ export default async function sendMail({ subject, fields }) {
4
+ const { mktcms: { smtpHost, smtpPort, smtpUser, smtpPass, smtpSecure, mailerFrom, mailerTo } } = useRuntimeConfig();
5
+ const transporter = nodemailer.createTransport({
6
+ host: smtpHost,
7
+ port: smtpPort,
8
+ secure: smtpSecure,
9
+ auth: {
10
+ user: smtpUser,
11
+ pass: smtpPass
12
+ }
13
+ });
14
+ const mailOptions = {
15
+ from: mailerFrom,
16
+ to: mailerTo,
17
+ subject,
18
+ html: Object.entries(fields).map(([key, value]) => `<p><strong>${key}:</strong> ${value}</p>`).join(""),
19
+ text: Object.entries(fields).map(([key, value]) => `${key}: ${value}`).join("\n")
20
+ };
21
+ return await transporter.sendMail(mailOptions);
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",
@@ -38,6 +38,7 @@
38
38
  "@nuxt/kit": "^4.2.2",
39
39
  "aws4fetch": "^1.0.20",
40
40
  "defu": "^6.1.4",
41
+ "nodemailer": "^7.0.12",
41
42
  "zod": "^4.3.5"
42
43
  },
43
44
  "devDependencies": {
@@ -47,6 +48,7 @@
47
48
  "@nuxt/schema": "^4.2.2",
48
49
  "@nuxt/test-utils": "^3.22.0",
49
50
  "@types/node": "latest",
51
+ "@types/nodemailer": "^7.0.5",
50
52
  "changelogen": "^0.6.2",
51
53
  "eslint": "^9.39.2",
52
54
  "nuxt": "^4.2.2",