@riducms/plugin-seo 0.1.5 → 0.2.0
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/package.json +5 -5
- package/src/generation-controller.svelte.ts +2 -2
- package/src/index.ts +41 -41
- package/src/meta-description-field.svelte +25 -16
- package/src/meta-image-field.svelte +35 -16
- package/src/meta-title-field.svelte +25 -16
- package/src/overview-field.svelte +10 -4
- package/src/preview-field.svelte +11 -4
- package/src/seo-config.ts +43 -20
package/package.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"url": "https://github.com/riducms/ridu/issues"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@riducms/plugin": "0.
|
|
7
|
-
"@riducms/protocol": "0.
|
|
8
|
-
"@riducms/ui": "0.
|
|
6
|
+
"@riducms/plugin": "0.2.0",
|
|
7
|
+
"@riducms/protocol": "0.2.0",
|
|
8
|
+
"@riducms/ui": "0.2.0",
|
|
9
9
|
"svelte": "^5.56.8"
|
|
10
10
|
},
|
|
11
11
|
"description": "Search metadata authoring for Ridu's official SEO plugin.",
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@riducms/build": "0.
|
|
13
|
+
"@riducms/build": "0.2.0"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./src/index.ts"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"type": "module",
|
|
40
|
-
"version": "0.
|
|
40
|
+
"version": "0.2.0"
|
|
41
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldAuthoringHost,
|
|
1
|
+
import type { FieldAuthoringHost, FieldDocumentForm } from "@riducms/plugin";
|
|
2
2
|
|
|
3
3
|
import { generationScopeToken, generationSnapshotToken } from "@plugin-seo/generation-snapshot";
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ export class GenerationController {
|
|
|
13
13
|
|
|
14
14
|
async run(
|
|
15
15
|
authoring: FieldAuthoringHost | undefined,
|
|
16
|
-
form:
|
|
16
|
+
form: FieldDocumentForm,
|
|
17
17
|
path: string
|
|
18
18
|
): Promise<string | undefined> {
|
|
19
19
|
const requestPlugin = authoring?.requestPlugin;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAdminPlugin, defineFieldComponent } from "@riducms/plugin/authoring/v1";
|
|
2
|
+
import {
|
|
3
|
+
decodeString,
|
|
4
|
+
decodeUI,
|
|
5
|
+
decodeLengthConfig,
|
|
6
|
+
decodeOverviewConfig,
|
|
7
|
+
decodeImageConfig,
|
|
8
|
+
decodePreviewConfig,
|
|
9
|
+
} from "@plugin-seo/seo-config";
|
|
2
10
|
|
|
3
11
|
import { seoMessages } from "@plugin-seo/messages";
|
|
4
12
|
|
|
@@ -8,49 +16,41 @@ import MetaTitleField from "@plugin-seo/meta-title-field.svelte";
|
|
|
8
16
|
import OverviewField from "@plugin-seo/overview-field.svelte";
|
|
9
17
|
import PreviewField from "@plugin-seo/preview-field.svelte";
|
|
10
18
|
|
|
11
|
-
export const seoFieldPlugins = [
|
|
12
|
-
defineFieldPlugin({
|
|
13
|
-
type: "ui",
|
|
14
|
-
key: "seo",
|
|
15
|
-
componentKey: "overview",
|
|
16
|
-
component: OverviewField,
|
|
17
|
-
canRender: (field) => field.admin.component?.component === "overview",
|
|
18
|
-
}),
|
|
19
|
-
defineFieldPlugin({
|
|
20
|
-
type: "text",
|
|
21
|
-
key: "seo",
|
|
22
|
-
componentKey: "title",
|
|
23
|
-
component: MetaTitleField,
|
|
24
|
-
canRender: (field) => field.admin.component?.component === "title",
|
|
25
|
-
}),
|
|
26
|
-
defineFieldPlugin({
|
|
27
|
-
type: "textarea",
|
|
28
|
-
key: "seo",
|
|
29
|
-
componentKey: "description",
|
|
30
|
-
component: MetaDescriptionField,
|
|
31
|
-
canRender: (field) => field.admin.component?.component === "description",
|
|
32
|
-
}),
|
|
33
|
-
defineFieldPlugin({
|
|
34
|
-
type: "upload",
|
|
35
|
-
key: "seo",
|
|
36
|
-
componentKey: "image",
|
|
37
|
-
component: MetaImageField,
|
|
38
|
-
canRender: (field) => field.admin.component?.component === "image",
|
|
39
|
-
}),
|
|
40
|
-
defineFieldPlugin({
|
|
41
|
-
type: "ui",
|
|
42
|
-
key: "seo",
|
|
43
|
-
componentKey: "preview",
|
|
44
|
-
component: PreviewField,
|
|
45
|
-
canRender: (field) => field.admin.component?.component === "preview",
|
|
46
|
-
}),
|
|
47
|
-
] as const;
|
|
48
|
-
|
|
49
19
|
export const seoAdminPlugin = defineAdminPlugin({
|
|
50
|
-
apiVersion: ADMIN_PLUGIN_API_VERSION,
|
|
51
20
|
key: "seo",
|
|
52
21
|
pairingVersion: 1,
|
|
53
|
-
|
|
22
|
+
components: {
|
|
23
|
+
overview: defineFieldComponent({
|
|
24
|
+
type: "ui",
|
|
25
|
+
component: OverviewField,
|
|
26
|
+
decodeValue: decodeUI,
|
|
27
|
+
decodeConfig: decodeOverviewConfig,
|
|
28
|
+
}),
|
|
29
|
+
title: defineFieldComponent({
|
|
30
|
+
type: "text",
|
|
31
|
+
component: MetaTitleField,
|
|
32
|
+
decodeValue: decodeString,
|
|
33
|
+
decodeConfig: (raw: unknown) => decodeLengthConfig(raw, "text"),
|
|
34
|
+
}),
|
|
35
|
+
description: defineFieldComponent({
|
|
36
|
+
type: "textarea",
|
|
37
|
+
component: MetaDescriptionField,
|
|
38
|
+
decodeValue: decodeString,
|
|
39
|
+
decodeConfig: (raw: unknown) => decodeLengthConfig(raw, "textarea"),
|
|
40
|
+
}),
|
|
41
|
+
image: defineFieldComponent({
|
|
42
|
+
type: "upload",
|
|
43
|
+
component: MetaImageField,
|
|
44
|
+
decodeValue: decodeString,
|
|
45
|
+
decodeConfig: decodeImageConfig,
|
|
46
|
+
}),
|
|
47
|
+
preview: defineFieldComponent({
|
|
48
|
+
type: "ui",
|
|
49
|
+
component: PreviewField,
|
|
50
|
+
decodeValue: decodeUI,
|
|
51
|
+
decodeConfig: decodePreviewConfig,
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
54
|
messages: seoMessages,
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import { Button, FieldFrame, Textarea } from "@riducms/ui";
|
|
2
|
+
import type { PluginFieldProps } from "@riducms/plugin";
|
|
3
|
+
import { Button, FieldFrame, Textarea, fieldControlARIA } from "@riducms/ui";
|
|
4
4
|
|
|
5
5
|
import { GenerationController } from "@plugin-seo/generation-controller.svelte";
|
|
6
6
|
import LengthIndicator from "@plugin-seo/length-indicator.svelte";
|
|
7
|
-
import {
|
|
7
|
+
import type { LengthConfig } from "@plugin-seo/seo-config";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
field: binding,
|
|
11
|
+
form,
|
|
12
|
+
config,
|
|
13
|
+
i18n,
|
|
14
|
+
authoring,
|
|
15
|
+
}: PluginFieldProps<string, LengthConfig, "textarea"> = $props();
|
|
16
|
+
const field = $derived(binding.schema);
|
|
17
|
+
const editingBlocked = $derived(binding.readOnly);
|
|
8
18
|
|
|
9
|
-
let { field, form, i18n, authoring }: FieldComponentProps = $props();
|
|
10
|
-
const config = $derived(lengthConfig(field));
|
|
11
19
|
const minLength = $derived(field.textarea?.minLength ?? config.minLength);
|
|
12
20
|
const maxLength = $derived(field.textarea?.maxLength ?? config.maxLength);
|
|
13
|
-
const value = $derived(String(
|
|
14
|
-
const issues = $derived(
|
|
15
|
-
const
|
|
21
|
+
const value = $derived(String(binding.value ?? ""));
|
|
22
|
+
const issues = $derived(binding.issues);
|
|
23
|
+
const inputARIA = $derived(
|
|
24
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
25
|
+
);
|
|
16
26
|
const generation = new GenerationController();
|
|
17
27
|
|
|
18
|
-
$effect(() => form.register(field.path));
|
|
19
28
|
$effect(() => () => generation.cancel());
|
|
20
29
|
|
|
21
30
|
async function generate() {
|
|
22
31
|
const result = await generation.run(authoring, form, "generate-description");
|
|
23
|
-
if (result !== undefined)
|
|
32
|
+
if (result !== undefined) binding.set(result);
|
|
24
33
|
}
|
|
25
34
|
</script>
|
|
26
35
|
|
|
@@ -49,7 +58,7 @@
|
|
|
49
58
|
variant="link"
|
|
50
59
|
size="xs"
|
|
51
60
|
class="h-auto px-0"
|
|
52
|
-
disabled={
|
|
61
|
+
disabled={editingBlocked || generation.status === "pending"}
|
|
53
62
|
aria-busy={generation.status === "pending"}
|
|
54
63
|
onclick={generate}
|
|
55
64
|
>
|
|
@@ -61,14 +70,14 @@
|
|
|
61
70
|
id={field.id}
|
|
62
71
|
name={field.path}
|
|
63
72
|
required={field.required}
|
|
64
|
-
readonly={
|
|
73
|
+
readonly={editingBlocked}
|
|
65
74
|
minlength={field.textarea?.minLength}
|
|
66
75
|
maxlength={field.textarea?.maxLength}
|
|
67
|
-
aria-invalid={
|
|
68
|
-
aria-describedby={
|
|
69
|
-
aria-errormessage={
|
|
76
|
+
aria-invalid={inputARIA["aria-invalid"]}
|
|
77
|
+
aria-describedby={inputARIA["aria-describedby"]}
|
|
78
|
+
aria-errormessage={inputARIA["aria-errormessage"]}
|
|
70
79
|
{value}
|
|
71
|
-
oninput={(event) =>
|
|
80
|
+
oninput={(event) => binding.set(event.currentTarget.value)}
|
|
72
81
|
/>
|
|
73
82
|
<LengthIndicator text={value} {minLength} {maxLength} {i18n} />
|
|
74
83
|
{#if generation.status === "error"}<p class="text-[12px] text-destructive" role="alert">
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import { Button, FieldFrame } from "@riducms/ui";
|
|
2
|
+
import type { PluginFieldProps, FieldDocument } from "@riducms/plugin";
|
|
3
|
+
import { Button, FieldFrame, fieldControlARIA } from "@riducms/ui";
|
|
4
4
|
import ImageIcon from "~icons/lucide/image";
|
|
5
5
|
import SearchIcon from "~icons/lucide/search";
|
|
6
6
|
import XIcon from "~icons/lucide/x";
|
|
7
7
|
|
|
8
8
|
import { GenerationController } from "@plugin-seo/generation-controller.svelte";
|
|
9
|
-
import { imageGenerationEnabled } from "@plugin-seo/seo-config";
|
|
10
9
|
|
|
11
|
-
let {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
let {
|
|
11
|
+
field: binding,
|
|
12
|
+
form,
|
|
13
|
+
config,
|
|
14
|
+
i18n,
|
|
15
|
+
authoring,
|
|
16
|
+
}: PluginFieldProps<string, { generate: boolean }, "upload"> = $props();
|
|
17
|
+
const field = $derived(binding.schema);
|
|
18
|
+
const editingBlocked = $derived(binding.readOnly);
|
|
19
|
+
const generationEnabled = $derived(config.generate);
|
|
20
|
+
const value = $derived(String(binding.value ?? ""));
|
|
21
|
+
const issues = $derived(binding.issues);
|
|
22
|
+
const controlARIA = $derived(
|
|
23
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
24
|
+
);
|
|
15
25
|
const target = $derived(
|
|
16
26
|
authoring?.collections.find(
|
|
17
27
|
(collection) =>
|
|
@@ -26,7 +36,6 @@
|
|
|
26
36
|
let loadFailed = $state(false);
|
|
27
37
|
const generation = new GenerationController();
|
|
28
38
|
|
|
29
|
-
$effect(() => form.register(field.path));
|
|
30
39
|
$effect(() => () => generation.cancel());
|
|
31
40
|
$effect(() => {
|
|
32
41
|
const id = value;
|
|
@@ -53,11 +62,11 @@
|
|
|
53
62
|
|
|
54
63
|
async function generate() {
|
|
55
64
|
const result = await generation.run(authoring, form, "generate-image");
|
|
56
|
-
if (result !== undefined)
|
|
65
|
+
if (result !== undefined) binding.set(result);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
function commit(ids: string[]) {
|
|
60
|
-
|
|
69
|
+
binding.set(ids[0] ?? null);
|
|
61
70
|
browserOpen = false;
|
|
62
71
|
}
|
|
63
72
|
|
|
@@ -92,7 +101,7 @@
|
|
|
92
101
|
variant="link"
|
|
93
102
|
size="xs"
|
|
94
103
|
class="h-auto px-0"
|
|
95
|
-
disabled={
|
|
104
|
+
disabled={editingBlocked || generation.status === "pending"}
|
|
96
105
|
aria-busy={generation.status === "pending"}
|
|
97
106
|
onclick={generate}
|
|
98
107
|
>
|
|
@@ -103,8 +112,10 @@
|
|
|
103
112
|
<Button
|
|
104
113
|
id={field.id}
|
|
105
114
|
aria-label={i18n.t("plugin.seo:selectImage")}
|
|
115
|
+
{...controlARIA}
|
|
106
116
|
variant="outline"
|
|
107
|
-
|
|
117
|
+
class="aria-invalid:!border-destructive"
|
|
118
|
+
disabled={editingBlocked || target === undefined || ReferenceBrowser === undefined}
|
|
108
119
|
onclick={() => openBrowser("select")}
|
|
109
120
|
>
|
|
110
121
|
<SearchIcon />
|
|
@@ -112,14 +123,16 @@
|
|
|
112
123
|
</Button>
|
|
113
124
|
{:else}
|
|
114
125
|
<div
|
|
115
|
-
class="flex min-w-0 items-center gap-2.5 rounded-[4px] border border-control-border bg-control p-2"
|
|
126
|
+
class="flex min-w-0 items-center gap-2.5 rounded-[4px] border border-control-border bg-control p-2 aria-invalid:!border-destructive/65"
|
|
127
|
+
aria-invalid={controlARIA["aria-invalid"]}
|
|
116
128
|
>
|
|
117
129
|
<button
|
|
118
130
|
id={field.id}
|
|
119
131
|
aria-label={i18n.t("plugin.seo:inspectImage")}
|
|
132
|
+
{...controlARIA}
|
|
120
133
|
type="button"
|
|
121
134
|
class="flex min-w-0 flex-1 items-center gap-2.5 rounded-[3px] text-start outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed"
|
|
122
|
-
disabled={target === undefined || ReferenceBrowser === undefined}
|
|
135
|
+
disabled={editingBlocked || target === undefined || ReferenceBrowser === undefined}
|
|
123
136
|
onclick={() => openBrowser("inspect")}
|
|
124
137
|
>
|
|
125
138
|
<span
|
|
@@ -142,14 +155,20 @@
|
|
|
142
155
|
</span>
|
|
143
156
|
</button>
|
|
144
157
|
{#if !field.admin.readOnly}<div class="flex shrink-0 gap-1">
|
|
145
|
-
<Button
|
|
158
|
+
<Button
|
|
159
|
+
variant="outline"
|
|
160
|
+
size="xs"
|
|
161
|
+
disabled={editingBlocked}
|
|
162
|
+
onclick={() => openBrowser("select")}
|
|
163
|
+
>
|
|
146
164
|
<SearchIcon aria-hidden="true" />
|
|
147
165
|
{i18n.t("plugin.seo:replaceImage")}
|
|
148
166
|
</Button>
|
|
149
167
|
<Button
|
|
150
168
|
variant="ghost"
|
|
151
169
|
size="icon-xs"
|
|
152
|
-
|
|
170
|
+
disabled={editingBlocked}
|
|
171
|
+
onclick={() => binding.set(null)}
|
|
153
172
|
aria-label={i18n.t("plugin.seo:removeImage")}
|
|
154
173
|
>
|
|
155
174
|
<XIcon />
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import { Button, FieldFrame, Input } from "@riducms/ui";
|
|
2
|
+
import type { PluginFieldProps } from "@riducms/plugin";
|
|
3
|
+
import { Button, FieldFrame, Input, fieldControlARIA } from "@riducms/ui";
|
|
4
4
|
|
|
5
5
|
import { GenerationController } from "@plugin-seo/generation-controller.svelte";
|
|
6
6
|
import LengthIndicator from "@plugin-seo/length-indicator.svelte";
|
|
7
|
-
import {
|
|
7
|
+
import type { LengthConfig } from "@plugin-seo/seo-config";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
field: binding,
|
|
11
|
+
form,
|
|
12
|
+
config,
|
|
13
|
+
i18n,
|
|
14
|
+
authoring,
|
|
15
|
+
}: PluginFieldProps<string, LengthConfig, "text"> = $props();
|
|
16
|
+
const field = $derived(binding.schema);
|
|
17
|
+
const editingBlocked = $derived(binding.readOnly);
|
|
8
18
|
|
|
9
|
-
let { field, form, i18n, authoring }: FieldComponentProps = $props();
|
|
10
|
-
const config = $derived(lengthConfig(field));
|
|
11
19
|
const minLength = $derived(field.text?.minLength ?? config.minLength);
|
|
12
20
|
const maxLength = $derived(field.text?.maxLength ?? config.maxLength);
|
|
13
|
-
const value = $derived(String(
|
|
14
|
-
const issues = $derived(
|
|
15
|
-
const
|
|
21
|
+
const value = $derived(String(binding.value ?? ""));
|
|
22
|
+
const issues = $derived(binding.issues);
|
|
23
|
+
const inputARIA = $derived(
|
|
24
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
25
|
+
);
|
|
16
26
|
const generation = new GenerationController();
|
|
17
27
|
|
|
18
|
-
$effect(() => form.register(field.path));
|
|
19
28
|
$effect(() => () => generation.cancel());
|
|
20
29
|
|
|
21
30
|
async function generate() {
|
|
22
31
|
const result = await generation.run(authoring, form, "generate-title");
|
|
23
|
-
if (result !== undefined)
|
|
32
|
+
if (result !== undefined) binding.set(result);
|
|
24
33
|
}
|
|
25
34
|
</script>
|
|
26
35
|
|
|
@@ -49,7 +58,7 @@
|
|
|
49
58
|
variant="link"
|
|
50
59
|
size="xs"
|
|
51
60
|
class="h-auto px-0"
|
|
52
|
-
disabled={
|
|
61
|
+
disabled={editingBlocked || generation.status === "pending"}
|
|
53
62
|
aria-busy={generation.status === "pending"}
|
|
54
63
|
onclick={generate}
|
|
55
64
|
>
|
|
@@ -61,14 +70,14 @@
|
|
|
61
70
|
id={field.id}
|
|
62
71
|
name={field.path}
|
|
63
72
|
required={field.required}
|
|
64
|
-
readonly={
|
|
73
|
+
readonly={editingBlocked}
|
|
65
74
|
minlength={field.text?.minLength}
|
|
66
75
|
maxlength={field.text?.maxLength}
|
|
67
|
-
aria-invalid={
|
|
68
|
-
aria-describedby={
|
|
69
|
-
aria-errormessage={
|
|
76
|
+
aria-invalid={inputARIA["aria-invalid"]}
|
|
77
|
+
aria-describedby={inputARIA["aria-describedby"]}
|
|
78
|
+
aria-errormessage={inputARIA["aria-errormessage"]}
|
|
70
79
|
{value}
|
|
71
|
-
oninput={(event) =>
|
|
80
|
+
oninput={(event) => binding.set(event.currentTarget.value)}
|
|
72
81
|
/>
|
|
73
82
|
<LengthIndicator text={value} {minLength} {maxLength} {i18n} />
|
|
74
83
|
{#if generation.status === "error"}<p class="text-[12px] text-destructive" role="alert">
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { PluginFieldProps } from "@riducms/plugin";
|
|
3
3
|
import CheckIcon from "~icons/lucide/check";
|
|
4
4
|
import XIcon from "~icons/lucide/x";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { OverviewConfig } from "@plugin-seo/seo-config";
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
field: binding,
|
|
10
|
+
form,
|
|
11
|
+
config,
|
|
12
|
+
i18n,
|
|
13
|
+
}: PluginFieldProps<undefined, OverviewConfig, "ui"> = $props();
|
|
14
|
+
const field = $derived(binding.schema);
|
|
7
15
|
|
|
8
|
-
let { field, form, i18n }: FieldComponentProps = $props();
|
|
9
|
-
const config = $derived(overviewConfig(field));
|
|
10
16
|
const title = $derived(form.get(config.titlePath));
|
|
11
17
|
const description = $derived(form.get(config.descriptionPath));
|
|
12
18
|
const image = $derived(form.get(config.imagePath));
|
package/src/preview-field.svelte
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { PluginFieldProps } from "@riducms/plugin";
|
|
3
3
|
|
|
4
4
|
import { GenerationController } from "@plugin-seo/generation-controller.svelte";
|
|
5
|
-
import {
|
|
5
|
+
import type { PreviewConfig } from "@plugin-seo/seo-config";
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
field: binding,
|
|
9
|
+
form,
|
|
10
|
+
config,
|
|
11
|
+
i18n,
|
|
12
|
+
authoring,
|
|
13
|
+
}: PluginFieldProps<undefined, PreviewConfig, "ui"> = $props();
|
|
14
|
+
const field = $derived(binding.schema);
|
|
6
15
|
|
|
7
|
-
let { field, form, i18n, authoring }: FieldComponentProps = $props();
|
|
8
|
-
const config = $derived(previewConfig(field));
|
|
9
16
|
const title = $derived(String(form.get(config.titlePath) ?? ""));
|
|
10
17
|
const description = $derived(String(form.get(config.descriptionPath) ?? ""));
|
|
11
18
|
let href = $state("");
|
package/src/seo-config.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { SchemaField } from "@riducms/protocol";
|
|
2
|
-
|
|
3
1
|
export interface LengthConfig {
|
|
4
2
|
generate: boolean;
|
|
5
3
|
minLength: number;
|
|
@@ -22,17 +20,25 @@ export interface PreviewConfig {
|
|
|
22
20
|
descriptionPath: string;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export function
|
|
26
|
-
const value = componentConfig(
|
|
23
|
+
export function decodeLengthConfig(raw: unknown, type: "text" | "textarea"): LengthConfig {
|
|
24
|
+
const value = componentConfig(raw, ["generate", "minLength", "maxLength"]);
|
|
27
25
|
return {
|
|
28
26
|
generate: value.generate === true,
|
|
29
|
-
minLength: positiveInteger(value.minLength,
|
|
30
|
-
maxLength: positiveInteger(value.maxLength,
|
|
27
|
+
minLength: positiveInteger(value.minLength, type === "textarea" ? 100 : 50),
|
|
28
|
+
maxLength: positiveInteger(value.maxLength, type === "textarea" ? 150 : 60),
|
|
31
29
|
};
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export function
|
|
35
|
-
const value = componentConfig(
|
|
32
|
+
export function decodeOverviewConfig(raw: unknown): OverviewConfig {
|
|
33
|
+
const value = componentConfig(raw, [
|
|
34
|
+
"titlePath",
|
|
35
|
+
"descriptionPath",
|
|
36
|
+
"imagePath",
|
|
37
|
+
"titleMin",
|
|
38
|
+
"titleMax",
|
|
39
|
+
"descriptionMin",
|
|
40
|
+
"descriptionMax",
|
|
41
|
+
]);
|
|
36
42
|
return {
|
|
37
43
|
titlePath: stringValue(value.titlePath, "meta.title"),
|
|
38
44
|
descriptionPath: stringValue(value.descriptionPath, "meta.description"),
|
|
@@ -44,8 +50,8 @@ export function overviewConfig(field: SchemaField): OverviewConfig {
|
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
export function
|
|
48
|
-
const value = componentConfig(
|
|
53
|
+
export function decodePreviewConfig(raw: unknown): PreviewConfig {
|
|
54
|
+
const value = componentConfig(raw, ["generate", "titlePath", "descriptionPath"]);
|
|
49
55
|
return {
|
|
50
56
|
generate: value.generate === true,
|
|
51
57
|
titlePath: stringValue(value.titlePath, "meta.title"),
|
|
@@ -53,21 +59,38 @@ export function previewConfig(field: SchemaField): PreviewConfig {
|
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
export function
|
|
57
|
-
return componentConfig(
|
|
62
|
+
export function decodeImageConfig(raw: unknown): { generate: boolean } {
|
|
63
|
+
return { generate: componentConfig(raw, ["generate"]).generate === true };
|
|
58
64
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
function componentConfig(raw: unknown, allowed: readonly string[]): Record<string, unknown> {
|
|
66
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
67
|
+
throw new Error("SEO config must be an object.");
|
|
68
|
+
const value = raw as Record<string, unknown>;
|
|
69
|
+
for (const key of Object.keys(value))
|
|
70
|
+
if (!allowed.includes(key)) throw new Error(`Unsupported SEO config key ${key}.`);
|
|
71
|
+
if (value.generate !== undefined && typeof value.generate !== "boolean")
|
|
72
|
+
throw new Error("generate must be a boolean.");
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
export function decodeString(value: unknown): string {
|
|
76
|
+
if (typeof value !== "string") throw new Error("Expected a string value.");
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
export function decodeUI(value: unknown): undefined {
|
|
80
|
+
if (value !== undefined) throw new Error("UI fields have no stored value.");
|
|
81
|
+
return undefined;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
function stringValue(value: unknown, fallback: string) {
|
|
68
|
-
|
|
85
|
+
if (value === undefined) return fallback;
|
|
86
|
+
if (typeof value !== "string" || !value.length)
|
|
87
|
+
throw new Error("Expected a nonempty string config value.");
|
|
88
|
+
return value;
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
function positiveInteger(value: unknown, fallback: number) {
|
|
72
|
-
|
|
92
|
+
if (value === undefined) return fallback;
|
|
93
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0)
|
|
94
|
+
throw new Error("Expected a positive integer config value.");
|
|
95
|
+
return value;
|
|
73
96
|
}
|