mktcms 0.3.17 → 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 +29 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +122 -33
- package/dist/runtime/app/components/content/editor/monacoEditor.vue +162 -0
- package/package.json +6 -1
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
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
|
-
|
|
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,5 +1,6 @@
|
|
|
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
5
|
import "monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js";
|
|
5
6
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
@@ -14,6 +15,165 @@ const rootEl = ref(null);
|
|
|
14
15
|
let editor;
|
|
15
16
|
let resizeObserver;
|
|
16
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
|
+
}
|
|
17
177
|
function ensureMonacoWorkers() {
|
|
18
178
|
const globalAny = globalThis;
|
|
19
179
|
if (globalAny.MonacoEnvironment?.getWorker)
|
|
@@ -42,6 +202,7 @@ onMounted(() => {
|
|
|
42
202
|
return;
|
|
43
203
|
emit("update:modelValue", editor.getValue());
|
|
44
204
|
});
|
|
205
|
+
document.addEventListener("paste", handlePaste, true);
|
|
45
206
|
resizeObserver = new ResizeObserver(() => {
|
|
46
207
|
editor?.layout();
|
|
47
208
|
});
|
|
@@ -63,6 +224,7 @@ watch(() => props.modelValue, (nextValue) => {
|
|
|
63
224
|
onBeforeUnmount(() => {
|
|
64
225
|
resizeObserver?.disconnect();
|
|
65
226
|
resizeObserver = void 0;
|
|
227
|
+
document.removeEventListener("paste", handlePaste, true);
|
|
66
228
|
editor?.dispose();
|
|
67
229
|
editor = void 0;
|
|
68
230
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mktcms",
|
|
3
|
-
"version": "0.3.
|
|
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",
|