mktcms 0.3.16 → 0.3.18

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
@@ -28,6 +28,35 @@ This is my personal, minimalist alternative to @nuxt/content and Studio, which a
28
28
  npx nuxi module add mktcms
29
29
  ```
30
30
 
31
+ The module also applies a small set of app defaults that you can still override in your own `nuxt.config.ts`:
32
+
33
+ - `router.options.scrollBehaviorType = 'smooth'`
34
+ - default German `app.head` language, title, description, and favicon
35
+ - `mdc.headings.anchorLinks = false`
36
+ - includes and configures `@nuxtjs/robots` with admin route disallow rules
37
+ - includes and configures `@nuxt/fonts` with default font weights
38
+ - includes and configures `@nuxtjs/plausible` with `proxy = true` and `autoPageviews = false`
39
+ - default frontmatter schema for `Seiten/Startseite.md` and `Seiten/**/*.md`
40
+
41
+ Example override:
42
+
43
+ ```ts
44
+ export default defineNuxtConfig({
45
+ app: {
46
+ head: {
47
+ title: 'Meine Website',
48
+ },
49
+ },
50
+ mktcms: {
51
+ frontmatter: {
52
+ 'Seiten/**/*.md': {
53
+ seoTitle: { type: 'string', label: 'Eigener SEO-Titel' },
54
+ },
55
+ },
56
+ },
57
+ })
58
+ ```
59
+
31
60
  ```bash
32
61
  NUXT_PUBLIC_MKTCMS_SITE_URL="http://localhost:3000"
33
62
  NUXT_PUBLIC_MKTCMS_SHOW_VERSIONING=false
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.3.16",
4
+ "version": "0.3.18",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,100 @@
1
1
  import { defineNuxtModule, createResolver, addComponent, addImports, addServerImports, addServerPlugin, addServerHandler, extendPages } from '@nuxt/kit';
2
2
  import defu from 'defu';
3
3
 
4
+ const DEFAULT_HEAD_META = [
5
+ {
6
+ name: "description",
7
+ content: "Meine neue mktCMS Website"
8
+ }
9
+ ];
10
+ const DEFAULT_HEAD_LINK = [
11
+ {
12
+ rel: "icon",
13
+ type: "image/png",
14
+ href: "/favicon.png"
15
+ }
16
+ ];
17
+ const defaultFrontmatterSchema = {
18
+ seoTitle: {
19
+ type: "string",
20
+ label: "SEO-Titel"
21
+ }
22
+ };
23
+ const defaultFrontmatterSchemas = {
24
+ "Seiten/Startseite.md": defaultFrontmatterSchema,
25
+ "Seiten/**/*.md": defaultFrontmatterSchema
26
+ };
27
+ function mergeHeadMeta(meta) {
28
+ const nextMeta = Array.isArray(meta) ? [...meta] : [];
29
+ for (const defaultMeta of DEFAULT_HEAD_META) {
30
+ const hasMatch = nextMeta.some((item) => item?.name === defaultMeta.name);
31
+ if (!hasMatch) {
32
+ nextMeta.push(defaultMeta);
33
+ }
34
+ }
35
+ return nextMeta;
36
+ }
37
+ function mergeHeadLinks(links) {
38
+ const nextLinks = Array.isArray(links) ? [...links] : [];
39
+ for (const defaultLink of DEFAULT_HEAD_LINK) {
40
+ const hasMatch = nextLinks.some((item) => item?.rel === defaultLink.rel);
41
+ if (!hasMatch) {
42
+ nextLinks.push(defaultLink);
43
+ }
44
+ }
45
+ return nextLinks;
46
+ }
47
+ function applyMktcmsNuxtDefaults(nuxtOptions, moduleOptions = {}) {
48
+ nuxtOptions.router = defu(nuxtOptions.router, {
49
+ options: {
50
+ scrollBehaviorType: "smooth"
51
+ }
52
+ });
53
+ nuxtOptions.app = defu(nuxtOptions.app, {
54
+ head: {
55
+ htmlAttrs: {
56
+ lang: "de"
57
+ },
58
+ title: "Neue Website"
59
+ }
60
+ });
61
+ nuxtOptions.app.head = nuxtOptions.app.head || {};
62
+ nuxtOptions.app.head.meta = mergeHeadMeta(nuxtOptions.app.head.meta);
63
+ nuxtOptions.app.head.link = mergeHeadLinks(nuxtOptions.app.head.link);
64
+ nuxtOptions.runtimeConfig = defu(nuxtOptions.runtimeConfig, {
65
+ plausibleApiKey: ""
66
+ });
67
+ nuxtOptions.runtimeConfig.public = defu(nuxtOptions.runtimeConfig.public, {
68
+ plausibleApiHost: ""
69
+ });
70
+ nuxtOptions.runtimeConfig.mktcms = defu(nuxtOptions.runtimeConfig.mktcms, {
71
+ adminAuthKey: "",
72
+ authCookieMaxAgeSeconds: 7 * 24 * 60 * 60,
73
+ authCookiePath: "/",
74
+ authCookieSameSite: "lax",
75
+ authCookieSecure: process.env.NODE_ENV === "production",
76
+ loginRateLimitMaxAttempts: 5,
77
+ loginRateLimitWindowSeconds: 300,
78
+ loginRateLimitBlockSeconds: 600,
79
+ uploadMaxBytes: 50 * 1024 * 1024,
80
+ smtpHost: "",
81
+ smtpPort: 465,
82
+ smtpSecure: true,
83
+ smtpUser: "",
84
+ smtpPass: "",
85
+ mailerFrom: "",
86
+ mailerTo: "",
87
+ gitUser: "",
88
+ gitRepo: "",
89
+ gitToken: "",
90
+ frontmatter: defu(moduleOptions.frontmatter || {}, defaultFrontmatterSchemas)
91
+ });
92
+ nuxtOptions.runtimeConfig.public.mktcms = defu(nuxtOptions.runtimeConfig.public.mktcms, {
93
+ siteUrl: "",
94
+ showVersioning: false
95
+ });
96
+ }
97
+
4
98
  const module$1 = defineNuxtModule({
5
99
  meta: {
6
100
  name: "mktcms",
@@ -8,43 +102,38 @@ const module$1 = defineNuxtModule({
8
102
  },
9
103
  moduleDependencies: {
10
104
  "@nuxtjs/mdc": {
11
- version: "^0.20.0"
105
+ version: "^0.20.0",
106
+ defaults: {
107
+ headings: {
108
+ anchorLinks: false
109
+ }
110
+ }
111
+ },
112
+ "@nuxt/fonts": {
113
+ version: "^0.14.0",
114
+ defaults: {
115
+ defaults: {
116
+ weights: [300, 400, 700, 800]
117
+ }
118
+ }
119
+ },
120
+ "@nuxtjs/robots": {
121
+ version: "^6.0.6",
122
+ defaults: {
123
+ disallow: ["/api/admin/*", "/admin/*"]
124
+ }
125
+ },
126
+ "@nuxtjs/plausible": {
127
+ version: "^3.0.2",
128
+ defaults: {
129
+ proxy: true,
130
+ autoPageviews: false
131
+ }
12
132
  }
13
133
  },
14
134
  setup(_options, _nuxt) {
15
135
  const resolver = createResolver(import.meta.url);
16
- _nuxt.options.runtimeConfig = defu(_nuxt.options.runtimeConfig, {
17
- plausibleApiKey: ""
18
- });
19
- _nuxt.options.runtimeConfig.public = defu(_nuxt.options.runtimeConfig.public, {
20
- plausibleApiHost: ""
21
- });
22
- _nuxt.options.runtimeConfig.mktcms = defu((_nuxt.options.runtimeConfig.mktcms, {
23
- adminAuthKey: "",
24
- authCookieMaxAgeSeconds: 7 * 24 * 60 * 60,
25
- authCookiePath: "/",
26
- authCookieSameSite: "lax",
27
- authCookieSecure: process.env.NODE_ENV === "production",
28
- loginRateLimitMaxAttempts: 5,
29
- loginRateLimitWindowSeconds: 300,
30
- loginRateLimitBlockSeconds: 600,
31
- uploadMaxBytes: 50 * 1024 * 1024,
32
- smtpHost: "",
33
- smtpPort: 465,
34
- smtpSecure: true,
35
- smtpUser: "",
36
- smtpPass: "",
37
- mailerFrom: "",
38
- mailerTo: "",
39
- gitUser: "",
40
- gitRepo: "",
41
- gitToken: "",
42
- frontmatter: _options.frontmatter || {}
43
- }));
44
- _nuxt.options.runtimeConfig.public.mktcms = defu((_nuxt.options.runtimeConfig.public.mktcms, {
45
- siteUrl: "",
46
- showVersioning: false
47
- }));
136
+ applyMktcmsNuxtDefaults(_nuxt.options, _options);
48
137
  addComponent({
49
138
  name: "AdminWidget",
50
139
  filePath: resolver.resolve("runtime/app/components/frontend/widget.vue")
@@ -1,7 +1,8 @@
1
1
  import 'monaco-editor/min/vs/editor/editor.main.css';
2
+ import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js';
2
3
  type __VLS_Props = {
3
4
  modelValue: string;
4
- language?: string;
5
+ language?: 'markdown';
5
6
  readOnly?: boolean;
6
7
  };
7
8
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
@@ -9,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
9
10
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
11
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
11
12
  }>, {
12
- language: string;
13
+ language: "markdown";
13
14
  readOnly: boolean;
14
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
16
  declare const _default: typeof __VLS_export;
@@ -1,12 +1,10 @@
1
1
  <script setup>
2
2
  import { onBeforeUnmount, onMounted, ref, watch } from "vue";
3
+ import TurndownService from "turndown";
3
4
  import "monaco-editor/min/vs/editor/editor.main.css";
4
- import * as monaco from "monaco-editor";
5
+ import "monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js";
6
+ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
5
7
  import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
6
- import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
7
- import CssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
8
- import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
9
- import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
10
8
  const props = defineProps({
11
9
  modelValue: { type: String, required: true },
12
10
  language: { type: String, required: false, default: "markdown" },
@@ -17,20 +15,171 @@ const rootEl = ref(null);
17
15
  let editor;
18
16
  let resizeObserver;
19
17
  let suppressModelEmit = false;
18
+ const allowedPasteElementNames = /* @__PURE__ */ new Set([
19
+ "a",
20
+ "b",
21
+ "blockquote",
22
+ "br",
23
+ "em",
24
+ "h1",
25
+ "h2",
26
+ "h3",
27
+ "h4",
28
+ "h5",
29
+ "h6",
30
+ "i",
31
+ "li",
32
+ "ol",
33
+ "p",
34
+ "strong",
35
+ "ul"
36
+ ]);
37
+ const removablePasteElementNames = /* @__PURE__ */ new Set([
38
+ "applet",
39
+ "area",
40
+ "audio",
41
+ "button",
42
+ "canvas",
43
+ "caption",
44
+ "col",
45
+ "colgroup",
46
+ "embed",
47
+ "figcaption",
48
+ "figure",
49
+ "form",
50
+ "hr",
51
+ "iframe",
52
+ "img",
53
+ "input",
54
+ "link",
55
+ "map",
56
+ "math",
57
+ "meta",
58
+ "noscript",
59
+ "object",
60
+ "option",
61
+ "picture",
62
+ "script",
63
+ "select",
64
+ "source",
65
+ "style",
66
+ "svg",
67
+ "table",
68
+ "tbody",
69
+ "td",
70
+ "textarea",
71
+ "tfoot",
72
+ "th",
73
+ "thead",
74
+ "title",
75
+ "tr",
76
+ "track",
77
+ "video"
78
+ ]);
79
+ const turndownService = new TurndownService({
80
+ headingStyle: "atx",
81
+ codeBlockStyle: "fenced",
82
+ bulletListMarker: "-"
83
+ });
84
+ turndownService.addRule("removeUnsupportedPasteElements", {
85
+ filter: (node) => removablePasteElementNames.has(node.nodeName.toLowerCase()) || node.nodeName.includes(":"),
86
+ replacement: () => ""
87
+ });
88
+ function unwrapElement(element) {
89
+ const parent = element.parentNode;
90
+ if (!parent)
91
+ return;
92
+ while (element.firstChild)
93
+ parent.insertBefore(element.firstChild, element);
94
+ parent.removeChild(element);
95
+ }
96
+ function isSafeHref(href) {
97
+ if (!href.trim())
98
+ return false;
99
+ try {
100
+ const url = new URL(href, window.location.origin);
101
+ return ["http:", "https:", "mailto:", "tel:"].includes(url.protocol);
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+ function sanitizePastedHtml(html) {
107
+ const clipboardDocument = new DOMParser().parseFromString(html, "text/html");
108
+ const comments = clipboardDocument.createTreeWalker(clipboardDocument.body, NodeFilter.SHOW_COMMENT);
109
+ const commentsToRemove = [];
110
+ while (comments.nextNode())
111
+ commentsToRemove.push(comments.currentNode);
112
+ for (const comment of commentsToRemove)
113
+ comment.remove();
114
+ const elements = Array.from(clipboardDocument.body.querySelectorAll("*"));
115
+ for (const element of elements) {
116
+ if (!element.isConnected)
117
+ continue;
118
+ const tagName = element.tagName.toLowerCase();
119
+ const inlineStyle = element.getAttribute("style") ?? "";
120
+ if (/display\s*:\s*none/i.test(inlineStyle) || /mso-hide\s*:\s*all/i.test(inlineStyle)) {
121
+ element.remove();
122
+ continue;
123
+ }
124
+ if (tagName.includes(":") || removablePasteElementNames.has(tagName)) {
125
+ element.remove();
126
+ continue;
127
+ }
128
+ if (tagName === "a") {
129
+ const href = element.getAttribute("href") ?? "";
130
+ if (!isSafeHref(href)) {
131
+ unwrapElement(element);
132
+ continue;
133
+ }
134
+ for (const attribute of Array.from(element.attributes)) {
135
+ if (attribute.name !== "href")
136
+ element.removeAttribute(attribute.name);
137
+ }
138
+ continue;
139
+ }
140
+ if (!allowedPasteElementNames.has(tagName)) {
141
+ unwrapElement(element);
142
+ continue;
143
+ }
144
+ for (const attribute of Array.from(element.attributes))
145
+ element.removeAttribute(attribute.name);
146
+ }
147
+ return clipboardDocument.body;
148
+ }
149
+ function insertMarkdown(markdown) {
150
+ if (!editor)
151
+ return;
152
+ const selections = editor.getSelections();
153
+ if (!selections?.length)
154
+ return;
155
+ editor.pushUndoStop();
156
+ editor.executeEdits("paste-html-as-markdown", selections.map((selection) => ({
157
+ range: selection,
158
+ text: markdown,
159
+ forceMoveMarkers: true
160
+ })));
161
+ editor.pushUndoStop();
162
+ editor.focus();
163
+ }
164
+ function handlePaste(event) {
165
+ if (!editor?.hasTextFocus())
166
+ return;
167
+ const html = event.clipboardData?.getData("text/html");
168
+ if (!html)
169
+ return;
170
+ const markdown = turndownService.turndown(sanitizePastedHtml(html)).trim();
171
+ if (!markdown)
172
+ return;
173
+ event.preventDefault();
174
+ event.stopPropagation();
175
+ insertMarkdown(markdown);
176
+ }
20
177
  function ensureMonacoWorkers() {
21
178
  const globalAny = globalThis;
22
179
  if (globalAny.MonacoEnvironment?.getWorker)
23
180
  return;
24
181
  globalAny.MonacoEnvironment = {
25
- getWorker(_, label) {
26
- if (label === "json")
27
- return new JsonWorker();
28
- if (label === "css" || label === "scss" || label === "less")
29
- return new CssWorker();
30
- if (label === "html" || label === "handlebars" || label === "razor")
31
- return new HtmlWorker();
32
- if (label === "typescript" || label === "javascript")
33
- return new TsWorker();
182
+ getWorker() {
34
183
  return new EditorWorker();
35
184
  }
36
185
  };
@@ -53,6 +202,7 @@ onMounted(() => {
53
202
  return;
54
203
  emit("update:modelValue", editor.getValue());
55
204
  });
205
+ document.addEventListener("paste", handlePaste, true);
56
206
  resizeObserver = new ResizeObserver(() => {
57
207
  editor?.layout();
58
208
  });
@@ -74,6 +224,7 @@ watch(() => props.modelValue, (nextValue) => {
74
224
  onBeforeUnmount(() => {
75
225
  resizeObserver?.disconnect();
76
226
  resizeObserver = void 0;
227
+ document.removeEventListener("paste", handlePaste, true);
77
228
  editor?.dispose();
78
229
  editor = void 0;
79
230
  });
@@ -1,7 +1,8 @@
1
1
  import 'monaco-editor/min/vs/editor/editor.main.css';
2
+ import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js';
2
3
  type __VLS_Props = {
3
4
  modelValue: string;
4
- language?: string;
5
+ language?: 'markdown';
5
6
  readOnly?: boolean;
6
7
  };
7
8
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
@@ -9,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
9
10
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
11
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
11
12
  }>, {
12
- language: string;
13
+ language: "markdown";
13
14
  readOnly: boolean;
14
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
16
  declare const _default: typeof __VLS_export;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",
@@ -38,8 +38,11 @@
38
38
  "css:watch": "tailwindcss -i ./src/runtime/app/styles/admin.css -o ./src/runtime/app/styles/admin.min.css --minify --watch"
39
39
  },
40
40
  "dependencies": {
41
+ "@nuxt/fonts": "^0.14.0",
41
42
  "@nuxt/kit": "^4.2.2",
42
43
  "@nuxtjs/mdc": "^0.20.1",
44
+ "@nuxtjs/plausible": "^3.0.2",
45
+ "@nuxtjs/robots": "^6.0.6",
43
46
  "@types/ejs": "^3.1.5",
44
47
  "@vueuse/core": "^14.2.1",
45
48
  "csv-parse": "^6.1.0",
@@ -52,6 +55,7 @@
52
55
  "nodemailer": "^7.0.13",
53
56
  "sharp": "^0.34.5",
54
57
  "simple-git": "^3.32.2",
58
+ "turndown": "^7.2.4",
55
59
  "unzipper": "^0.12.3",
56
60
  "yaml": "^2.8.2",
57
61
  "zod": "^4.3.6"
@@ -66,6 +70,7 @@
66
70
  "@tailwindcss/typography": "^0.5.19",
67
71
  "@types/node": "^25.3.0",
68
72
  "@types/nodemailer": "^7.0.11",
73
+ "@types/turndown": "^5.0.6",
69
74
  "@types/unzipper": "^0.10.11",
70
75
  "changelogen": "^0.6.2",
71
76
  "eslint": "^9.39.3",