@jini-ai/admin 0.4.11 → 0.4.13
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/CHANGELOG.md +9 -0
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +74 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/ports/seo.d.ts +20 -0
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/adapters/http.js +5 -0
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/messages.en.d.ts +15 -10
- package/dist/seo/messages.en.js +15 -10
- package/dist/seo/models.d.ts +11 -0
- package/dist/seo/react/components/MediaRefField.d.ts +17 -5
- package/dist/seo/react/components/MediaRefField.js +31 -4
- package/dist/seo/react/hooks/MediaRefField.hooks.js +12 -2
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoPage.hooks.js +2 -2
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +1 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -2
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +13 -2
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +4 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -33
- package/dist/seo/react/index.d.ts +45 -30
- package/dist/seo/react/pages/SeoPage.d.ts +6 -4
- package/dist/seo/react/pages/SeoPage.js +51 -29
- package/dist/seo/rules.d.ts +91 -0
- package/dist/seo/rules.js +161 -6
- package/dist/seo/seo.module.d.ts +15 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +19 -10
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useWiredMediaRefField } from "../hooks/MediaRefField.hooks.js";
|
|
2
2
|
/**
|
|
3
|
-
* @file `MediaRefField` —
|
|
4
|
-
*
|
|
3
|
+
* @file `MediaRefField` — an `{assetId}:{transformName}` media reference (or an absolute URL),
|
|
4
|
+
* shown as the chosen file's thumbnail and name, plus a "Choose image" affordance that opens the existing
|
|
5
5
|
* `MediaPickerDialog`, a thumbnail preview of the current value, and a Remove control.
|
|
6
6
|
*
|
|
7
7
|
* Built for `react/pages/SeoPage.tsx`'s `defaultOgImage`/`ogImage`/`twitterImage` fields (SPEC intent: "make the OG
|
|
@@ -16,11 +16,15 @@ import { useWiredMediaRefField } from "../hooks/MediaRefField.hooks.js";
|
|
|
16
16
|
* 2026-10-08 polish: the original editable-input rationale above still applies to pasted URLs
|
|
17
17
|
* and empty fields. A selected library reference now shows plain copy with its preview; Remove
|
|
18
18
|
* returns to manual entry. A hidden input preserves the exact FormData value on defaults saves.
|
|
19
|
+
* 2026-10-15: superseded — the input is removed entirely (owner: the read-only box "looks
|
|
20
|
+
* useless"). An existing pasted URL still displays (by its file name) and still saves unchanged;
|
|
21
|
+
* a new URL can no longer be typed here, only chosen from the library.
|
|
19
22
|
*/
|
|
20
23
|
export interface MediaRefFieldProps {
|
|
21
24
|
locale: string;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
25
|
+
/** Base id: the caption is `{id}-label` and names the row's `role="group"`; the hidden input (when
|
|
26
|
+
* `name` is set) carries the id itself. Was an `<input id>`/`<label htmlFor>` pair until the text
|
|
27
|
+
* box was removed (2026-10-15). */
|
|
24
28
|
id: string;
|
|
25
29
|
/** Forwarded to the `<input name>` only when set — `defaultOgImage` needs this (its `<form>` reads
|
|
26
30
|
* `FormData` by name on submit); the per-entry `ogImage`/`twitterImage` fields do not (they save
|
|
@@ -32,9 +36,17 @@ export interface MediaRefFieldProps {
|
|
|
32
36
|
/** This field's own base handle — appended `-choose`/`-clear`/`-dialog` for its own controls, and
|
|
33
37
|
* forwarded as-is to the `<input>` itself. Omit to leave every element here untagged. */
|
|
34
38
|
agentHandle?: string;
|
|
39
|
+
/** Shown in the file slot while no image is set — for a field that inherits another field's
|
|
40
|
+
* image when empty (the X image falls back to the social preview image). */
|
|
41
|
+
emptyHint?: string;
|
|
42
|
+
/** The value comes from the site default or page content, with no SEO override of its own: the file
|
|
43
|
+
* shows muted as "Using site default: …" or "Using image from page content: …"; size and Remove
|
|
44
|
+
* are hidden — there is no SEO override to remove. "Choose image" is how an override starts
|
|
45
|
+
* (owner, 2026-10-15). */
|
|
46
|
+
inherited?: false | 'default' | 'content';
|
|
35
47
|
/** Injectable seam for the picker's own state — same convention as `MediaPickerDialog`'s own
|
|
36
48
|
* `useDialog` prop. Defaults to the real {@link useWiredMediaRefField}. */
|
|
37
49
|
useField?: typeof useWiredMediaRefField;
|
|
38
50
|
}
|
|
39
|
-
export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, useField, }: MediaRefFieldProps
|
|
51
|
+
export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited, useField, }: MediaRefFieldProps): import("react").JSX.Element;
|
|
40
52
|
//# sourceMappingURL=MediaRefField.d.ts.map
|
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { agentHandle } from "@jini-ai/agentic";
|
|
3
|
+
import { interpolate } from "@jini-ai/ui/i18n";
|
|
3
4
|
import { MediaPickerBridge } from "./MediaPickerBridge.js";
|
|
4
5
|
import { useSeoCopy } from "../hooks/SeoPorts.hooks.js";
|
|
5
6
|
import { useWiredMediaRefField, useMediaRefPresentation, handleSpread } from "../hooks/MediaRefField.hooks.js";
|
|
6
|
-
export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base,
|
|
7
|
+
export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited = false, useField = useWiredMediaRefField, }) {
|
|
7
8
|
const t = useSeoCopy();
|
|
8
|
-
const { Picker = MediaPickerBridge
|
|
9
|
-
const { pickerOpen, openPicker, closePicker, handleSelect, clear, previewUrl,
|
|
10
|
-
|
|
9
|
+
const { Picker = MediaPickerBridge } = useMediaRefPresentation({ value });
|
|
10
|
+
const { pickerOpen, openPicker, closePicker, handleSelect, clear, previewUrl, file, accept, onPreviewLoad, onPreviewError } = useField({ value, onChange });
|
|
11
|
+
const labelId = `${id}-label`;
|
|
12
|
+
/* One compact row: thumbnail, the file's name and size, then Choose/Remove kept together in their
|
|
13
|
+
own group so a narrow column wraps the pair as one unit instead of stranding Remove on a line of
|
|
14
|
+
its own (owner, 2026-10-15). The thumbnail used to sit under the row at 4rem, repeated per field.
|
|
15
|
+
`warning` (share-image size/byte/HTTP diagnostics) is still computed by the hook but no longer
|
|
16
|
+
painted: the owner called the yellow callout under every image "completely useless".
|
|
17
|
+
2026-10-15 (round 3): the text box is gone — for a library pick it only ever said a read-only
|
|
18
|
+
"Selected image" ("i can't change it and looks useless"), so the row names the file instead and
|
|
19
|
+
the picker is the one way in. The exact value still rides a hidden input for FormData saves. */
|
|
20
|
+
return (_jsxs("div", { className: "jini-field jini-media-ref-field", children: [_jsx("span", { className: "jini-field-label", id: labelId, children: label }), _jsxs("div", { className: "jini-media-ref-field-row", role: "group", "aria-labelledby": labelId, ...(base ? agentHandle({ handle: base }, { role: "region", label }) : {}), children: [_jsx(MediaRefFile, { file: file, emptyHint: emptyHint, label: label, locale: locale, t: t, inherited: inherited, previewUrl: previewUrl, onPreviewLoad: onPreviewLoad, onPreviewError: onPreviewError }), name ? _jsx("input", { type: "hidden", id: id, name: name, value: value }) : null, _jsx(MediaRefActions, { label: label, locale: locale, t: t, base: base, value: value, inherited: inherited, openPicker: openPicker, clear: clear })] }), pickerOpen ? (_jsx(Picker, { onSelect: handleSelect, onCancel: closePicker, accept: accept, agentHandle: base ? `${base}-dialog` : undefined })) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/** The chosen file: thumbnail, name (prefixed with its source when inherited) and, for an own
|
|
23
|
+
* override, its pixel size. With no file, the field's empty hint (if any) takes the slot. */
|
|
24
|
+
function MediaRefFile({ file, emptyHint, label, locale, t, inherited, previewUrl, onPreviewLoad, onPreviewError }) {
|
|
25
|
+
if (!file)
|
|
26
|
+
return emptyHint ? _jsx("span", { className: "jini-media-ref-field-empty", children: emptyHint }) : null;
|
|
27
|
+
return (_jsxs("span", { className: inherited ? "jini-media-ref-field-file jini-media-ref-field-file-inherited" : "jini-media-ref-field-file", children: [previewUrl ? _jsx("img", { className: "jini-media-ref-field-preview", src: previewUrl, alt: `${label} preview`, onLoad: onPreviewLoad, onError: onPreviewError }, previewUrl) : null, _jsx("span", { className: "jini-media-ref-field-name", title: file.name, children: inherited ? inheritedFileName({ name: file.name, inherited, locale, t }) : file.name }), file.size && !inherited ? _jsx("span", { className: "jini-media-ref-field-size", children: file.size }) : null] }));
|
|
28
|
+
}
|
|
29
|
+
/** "Using site default: {name}" or "Using image from page content: {name}", translated. */
|
|
30
|
+
function inheritedFileName({ name, inherited, locale, t }, _optional = {}) {
|
|
31
|
+
const key = inherited === "content" ? "Using image from page content: {name}" : "Using site default: {name}";
|
|
32
|
+
return interpolate({ template: t({ locale, key, vars: { name } }), vars: { name } });
|
|
33
|
+
}
|
|
34
|
+
/** Choose (always) and Remove (only for an own value), grouped so they wrap as one unit. */
|
|
35
|
+
function MediaRefActions({ label, locale, t, base, value, inherited, openPicker, clear }) {
|
|
36
|
+
const handleBase = base === undefined ? {} : { base };
|
|
37
|
+
return (_jsxs("span", { className: "jini-media-ref-field-actions", children: [_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: openPicker, ...handleSpread({ ...handleBase, suffix: "choose", label: `Choose an image for: ${label}` }), children: t({ locale, key: "Choose image" }) }), value && !inherited ? (_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: clear, ...handleSpread({ ...handleBase, suffix: "clear", label: `Remove the selected image for: ${label}` }), children: t({ locale, key: "Remove" }) })) : null] }));
|
|
11
38
|
}
|
|
12
39
|
//# sourceMappingURL=MediaRefField.js.map
|
|
@@ -3,16 +3,26 @@ import { useShareImageWarning } from './use-share-image-warning.hooks.js';
|
|
|
3
3
|
import { agentHandle } from '@jini-ai/agentic';
|
|
4
4
|
import { useController } from '../../../core/react/use-controller.js';
|
|
5
5
|
import { createMediaRefController } from '../../controllers/media-ref.controller.js';
|
|
6
|
-
import { isStoredMediaRef, resolveMediaRefPreviewUrl } from '../../rules.js';
|
|
6
|
+
import { describeMediaRef, isStoredMediaRef, resolveMediaRefPreviewUrl } from '../../rules.js';
|
|
7
|
+
import { useFetchQuery } from '@jini-ai/ui/fetch-query';
|
|
8
|
+
import { extensionForMimeType } from '@jini-ai/ui';
|
|
9
|
+
import { SEO_KEYS } from './SeoQuery.hooks.js';
|
|
7
10
|
import { useSeoPorts, useSeoOptions } from './SeoPorts.hooks.js';
|
|
8
11
|
export function useMediaRefField({ value, onChange, api }, { t } = {}) {
|
|
9
12
|
const change = useRef(onChange);
|
|
10
13
|
change.current = onChange;
|
|
11
14
|
const { controller, snapshot } = useController({ create: () => createMediaRefController({ onChange: value => change.current(value) }), dependencies: [] });
|
|
12
15
|
const previewUrl = api.mediaPreviewUrl ? api.mediaPreviewUrl({ value }) : resolveMediaRefPreviewUrl({ value, mediaOriginalUrl: id => api.mediaOriginalUrl({ id }) });
|
|
13
|
-
const diagnostics = useShareImageWarning({ value, previewUrl, api }, { t });
|
|
16
|
+
const { naturalWidth, naturalHeight, ...diagnostics } = useShareImageWarning({ value, previewUrl, api }, { t });
|
|
17
|
+
// One cached library read shared by every image field on the screen; only a library reference
|
|
18
|
+
// needs it (a URL names itself), and a host without `listSeoMedia` falls back to the slug.
|
|
19
|
+
const list = api.listSeoMedia;
|
|
20
|
+
// `fetch` runs only while the query is enabled, which requires `list`: the fetch-query adapters
|
|
21
|
+
// never call a disabled read's fetch (not on mount, focus or prefix invalidation).
|
|
22
|
+
const media = useFetchQuery({ key: SEO_KEYS.media, fetch: () => list.call(api, {}) }, { enabled: Boolean(list) && isStoredMediaRef({ value }) });
|
|
14
23
|
return {
|
|
15
24
|
...diagnostics,
|
|
25
|
+
file: describeMediaRef({ value, media: media.data }, { width: naturalWidth, height: naturalHeight, t, extensionFor: extensionForMimeType }),
|
|
16
26
|
pickerOpen: snapshot?.pickerOpen ?? false,
|
|
17
27
|
openPicker: () => controller?.openPicker({}), closePicker: () => controller?.closePicker({}),
|
|
18
28
|
handleSelect: item => controller?.handleSelect({ item }), clear: () => controller?.clear({}),
|
|
@@ -29,6 +29,7 @@ export declare function useAnalyzePanel({ analysis }: {
|
|
|
29
29
|
analysis: SeoEntryAnalysis;
|
|
30
30
|
}, _optional?: Record<string, never>): {
|
|
31
31
|
sortedIssues: import("../../../core/index.js").AdminSeoIssue[];
|
|
32
|
+
tone: "failure" | "warning" | "success";
|
|
32
33
|
};
|
|
33
34
|
/** All seven defaults remain in one FormData submission; tabs never split this form. */
|
|
34
35
|
export declare function useSeoDefaults({ save }: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { SeoDefaultsIcon, SeoSitemapIcon, SeoEntriesIcon } from '../components/SeoIcons.js';
|
|
3
|
-
import { buildSeoSettingsPatch, resolveSeoTabId, sortIssuesBySeverity } from '../../rules.js';
|
|
3
|
+
import { buildSeoSettingsPatch, resolveSeoTabId, sortIssuesBySeverity, scoreTone } from '../../rules.js';
|
|
4
4
|
import { useSeoOptions } from './SeoPorts.hooks.js';
|
|
5
5
|
export { buildSeoSettingsPatch, resolveSeoTabId } from '../../rules.js';
|
|
6
6
|
export function resolveSeoTabs(_required = {}, { t = key => key } = {}) {
|
|
@@ -20,7 +20,7 @@ export function useSeoTabs({ tabId }, _optional = {}) {
|
|
|
20
20
|
return { activeTabId: resolveSeoTabId({ tabId }), tabs: resolveSeoTabs({}, { t }) };
|
|
21
21
|
}
|
|
22
22
|
export function useAnalyzePanel({ analysis }, _optional = {}) {
|
|
23
|
-
return { sortedIssues: sortIssuesBySeverity({ issues: analysis.issues }) };
|
|
23
|
+
return { sortedIssues: sortIssuesBySeverity({ issues: analysis.issues }), tone: scoreTone({ score: analysis.score }) };
|
|
24
24
|
}
|
|
25
25
|
/** All seven defaults remain in one FormData submission; tabs never split this form. */
|
|
26
26
|
export function useSeoDefaults({ save }, _optional = {}) {
|
|
@@ -8,6 +8,7 @@ export declare const SEO_KEYS: {
|
|
|
8
8
|
}, _optional?: Record<string, never>) => readonly ["seo", "entry", string];
|
|
9
9
|
choices: readonly ["seo", "choices"];
|
|
10
10
|
sitemap: readonly ["seo", "sitemap"];
|
|
11
|
+
media: readonly ["seo", "media"];
|
|
11
12
|
};
|
|
12
13
|
/** Controllers keep their draft/error contracts; these injected ports share the provider cache.
|
|
13
14
|
* Reads omit a controller's abort signal because another mounted consumer can own the same read.
|
|
@@ -7,6 +7,7 @@ export const SEO_KEYS = {
|
|
|
7
7
|
entry: ({ entryId }, _optional = {}) => ['seo', 'entry', entryId],
|
|
8
8
|
choices: ['seo', 'choices'],
|
|
9
9
|
sitemap: ['seo', 'sitemap'],
|
|
10
|
+
media: ['seo', 'media'],
|
|
10
11
|
};
|
|
11
12
|
/** Controllers keep their draft/error contracts; these injected ports share the provider cache.
|
|
12
13
|
* Reads omit a controller's abort signal because another mounted consumer can own the same read.
|
|
@@ -2,13 +2,18 @@ import { describeSeoError } from '../../models.js';
|
|
|
2
2
|
import { useFetchQuery } from '@jini-ai/ui/fetch-query';
|
|
3
3
|
import { SEO_KEYS } from './SeoQuery.hooks.js';
|
|
4
4
|
import { initialEntryPickerState } from '../../models.js';
|
|
5
|
+
import { entryPickerOptions } from '../../rules.js';
|
|
5
6
|
import { useSeoOptions, useSeoPorts } from './SeoPorts.hooks.js';
|
|
6
7
|
export function useEntryPicker({ api }, { t, describeError } = {}) {
|
|
7
8
|
const query = useFetchQuery({ key: SEO_KEYS.choices, fetch: async () => {
|
|
8
9
|
const [posts, pages] = await Promise.all([api.listSeoPosts({}), api.listSeoPages({})]);
|
|
9
|
-
return
|
|
10
|
+
return { posts, pages };
|
|
10
11
|
} });
|
|
11
|
-
|
|
12
|
+
const data = query.data;
|
|
13
|
+
return { ...initialEntryPickerState,
|
|
14
|
+
entries: data ? [...data.posts, ...data.pages] : null,
|
|
15
|
+
options: data ? entryPickerOptions(data, { t }) : null,
|
|
16
|
+
error: query.error ? (describeError ?? describeSeoError)({ error: query.error, fallback: t?.('failed to load entries') ?? 'failed to load entries' }) : null };
|
|
12
17
|
}
|
|
13
18
|
export function useWiredEntryPicker(_required = {}, _optional = {}) {
|
|
14
19
|
const { seoApi: api } = useSeoPorts();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describeSeoError } from '../../models.js';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { useSeoEntryQuery } from './SeoQuery.hooks.js';
|
|
3
|
+
import { useSeoEntryQuery, SEO_KEYS } from './SeoQuery.hooks.js';
|
|
4
|
+
import { useFetchQuery } from '@jini-ai/ui/fetch-query';
|
|
4
5
|
import { useController } from '../../../core/react/use-controller.js';
|
|
5
6
|
import { createSeoEntryController } from '../../controllers/seo-entry.controller.js';
|
|
6
7
|
import { initialSeoEntryState } from '../../models.js';
|
|
8
|
+
import { inheritedXFor, ogImageInherited } from '../../rules.js';
|
|
7
9
|
import { useSeoOptions, useSeoPorts } from './SeoPorts.hooks.js';
|
|
8
10
|
export function useSeoEntryPanel({ api, entryId }, { t, describeError } = {}) {
|
|
9
11
|
const cached = useSeoEntryQuery({ api, entryId });
|
|
@@ -17,11 +19,20 @@ export function useSeoEntryPanel({ api, entryId }, { t, describeError } = {}) {
|
|
|
17
19
|
if (controller && cached.query.data)
|
|
18
20
|
controller.set({ patch: cached.query.data });
|
|
19
21
|
}, [controller, cached.query.data]);
|
|
22
|
+
// The site defaults read the Site defaults tab already caches; only the default image is used here.
|
|
23
|
+
const settings = useFetchQuery({ key: SEO_KEYS.settings, fetch: () => api.getSeoSettings({}) });
|
|
20
24
|
const state = snapshot ?? initialSeoEntryState;
|
|
21
25
|
return {
|
|
22
26
|
...state,
|
|
27
|
+
ogImageInherited: state.resolved ? ogImageInherited({ meta: state.resolved, defaultOgImage: settings.data?.defaultOgImage, touched: state.touched }) : false,
|
|
23
28
|
fieldValue: (key, resolvedValue) => controller ? controller.fieldValue({ key, resolvedValue }) : resolvedValue,
|
|
24
|
-
setField: (key, value) =>
|
|
29
|
+
setField: (key, value) => {
|
|
30
|
+
// An edit to the social preview also clears an X field that was following it (rules.ts).
|
|
31
|
+
const xKey = state.resolved ? inheritedXFor({ key, meta: state.resolved, touched: state.touched }) : null;
|
|
32
|
+
controller?.setField({ key, value });
|
|
33
|
+
if (xKey)
|
|
34
|
+
controller?.setField({ key: xKey, value: '' });
|
|
35
|
+
},
|
|
25
36
|
save: async () => { await controller?.save({}); },
|
|
26
37
|
};
|
|
27
38
|
}
|
|
@@ -15,5 +15,8 @@ export declare function useShareImageWarning({ value, previewUrl, api }: {
|
|
|
15
15
|
api: AdminSeoPort;
|
|
16
16
|
}, { t }?: {
|
|
17
17
|
t?: SeoTranslator | undefined;
|
|
18
|
-
}): Pick<MediaRefFieldController, 'warning' | 'onPreviewLoad' | 'onPreviewError'
|
|
18
|
+
}): Pick<MediaRefFieldController, 'warning' | 'onPreviewLoad' | 'onPreviewError'> & {
|
|
19
|
+
naturalWidth?: number | undefined;
|
|
20
|
+
naturalHeight?: number | undefined;
|
|
21
|
+
};
|
|
19
22
|
//# sourceMappingURL=use-share-image-warning.hooks.d.ts.map
|
|
@@ -17,20 +17,8 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
|
|
|
17
17
|
const [inspection, setInspection] = useState(null);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
const inspect = api.inspectShareImage;
|
|
20
|
-
if (!previewUrl || !inspect)
|
|
20
|
+
if (!previewUrl || !inspect || !isBrowserSameOrigin({ url: previewUrl }))
|
|
21
21
|
return;
|
|
22
|
-
// Browser origin, rather than the public site's alias: a cross-origin HEAD cannot establish
|
|
23
|
-
// that an otherwise loadable external image is missing when the server omits CORS headers.
|
|
24
|
-
const location = globalThis.location;
|
|
25
|
-
if (!location)
|
|
26
|
-
return;
|
|
27
|
-
try {
|
|
28
|
-
if (new URL(previewUrl, location.href).origin !== location.origin)
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
22
|
const controller = new AbortController();
|
|
35
23
|
let active = true;
|
|
36
24
|
const timeout = setTimeout(() => {
|
|
@@ -39,17 +27,8 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
|
|
|
39
27
|
setInspection({ url: previewUrl, error: { key: 'The share image check timed out after 10 seconds.' } });
|
|
40
28
|
}, 10_000);
|
|
41
29
|
inspect.call(api, { url: previewUrl }, { signal: controller.signal }).then(({ status, byteSize }) => {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
if (status === 404 || status === 410) {
|
|
45
|
-
setInspection({ url: previewUrl, error: { key: 'The share image is missing (HTTP {status}).', vars: { status } } });
|
|
46
|
-
}
|
|
47
|
-
else if (status < 200 || status >= 300) {
|
|
48
|
-
setInspection({ url: previewUrl, error: { key: 'The share image could not be checked (HTTP {status}).', vars: { status } } });
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
setInspection({ url: previewUrl, byteSize });
|
|
52
|
-
}
|
|
30
|
+
if (active)
|
|
31
|
+
setInspection(inspectionFromResponse({ url: previewUrl, status, byteSize }));
|
|
53
32
|
}).catch((error) => {
|
|
54
33
|
if (!active)
|
|
55
34
|
return;
|
|
@@ -58,22 +37,58 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
|
|
|
58
37
|
}).finally(() => { clearTimeout(timeout); });
|
|
59
38
|
return () => { active = false; clearTimeout(timeout); controller.abort(); };
|
|
60
39
|
}, [previewUrl, api]);
|
|
61
|
-
const source =
|
|
62
|
-
const checked =
|
|
63
|
-
const error = checked?.error;
|
|
40
|
+
const source = stateForUrl({ state: decoded, url: previewUrl });
|
|
41
|
+
const checked = stateForUrl({ state: inspection, url: previewUrl });
|
|
64
42
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
naturalWidth: source.width, naturalHeight: source.height,
|
|
44
|
+
warning: shareImageWarning({ value, previewUrl, width: source.width, height: source.height, previewFailed: source.failed, byteSize: checked.byteSize,
|
|
45
|
+
error: translatedInspectionError({ error: checked.error }, { t }) }, { t }),
|
|
67
46
|
onPreviewLoad: ({ currentTarget }) => {
|
|
68
|
-
const url = currentTarget.
|
|
69
|
-
if (url
|
|
47
|
+
const url = currentPreviewSrc({ image: currentTarget, currentUrl: currentUrl.current });
|
|
48
|
+
if (url)
|
|
70
49
|
setDecoded({ url, width: currentTarget.naturalWidth, height: currentTarget.naturalHeight });
|
|
71
50
|
},
|
|
72
51
|
onPreviewError: ({ currentTarget }) => {
|
|
73
|
-
const url = currentTarget.
|
|
74
|
-
if (url
|
|
52
|
+
const url = currentPreviewSrc({ image: currentTarget, currentUrl: currentUrl.current });
|
|
53
|
+
if (url)
|
|
75
54
|
setDecoded({ url, failed: true });
|
|
76
55
|
},
|
|
77
56
|
};
|
|
78
57
|
}
|
|
58
|
+
/** URL-keyed state for the current preview only; a prior choice's state reads as nothing known. */
|
|
59
|
+
function stateForUrl({ state, url }, _optional = {}) {
|
|
60
|
+
return state?.url === url ? state : {};
|
|
61
|
+
}
|
|
62
|
+
/** Whether `url` resolves to the browser's own origin; `false` outside a browser or for an unparseable URL. */
|
|
63
|
+
function isBrowserSameOrigin({ url }, _optional = {}) {
|
|
64
|
+
// Browser origin, rather than the public site's alias: a cross-origin HEAD cannot establish
|
|
65
|
+
// that an otherwise loadable external image is missing when the server omits CORS headers.
|
|
66
|
+
const location = globalThis.location;
|
|
67
|
+
if (!location)
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
return new URL(url, location.href).origin === location.origin;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** A HEAD response as inspection state: 404/410 is a missing file, any other non-2xx an unchecked
|
|
77
|
+
* one, and a 2xx carries the reported byte size. */
|
|
78
|
+
function inspectionFromResponse({ url, status, byteSize }, _optional = {}) {
|
|
79
|
+
if (status === 404 || status === 410)
|
|
80
|
+
return { url, error: { key: 'The share image is missing (HTTP {status}).', vars: { status } } };
|
|
81
|
+
if (status < 200 || status >= 300)
|
|
82
|
+
return { url, error: { key: 'The share image could not be checked (HTTP {status}).', vars: { status } } };
|
|
83
|
+
return { url, byteSize };
|
|
84
|
+
}
|
|
85
|
+
/** The inspection error's copy, translated at render so a language switch re-renders it. */
|
|
86
|
+
function translatedInspectionError({ error }, { t }) {
|
|
87
|
+
return error ? interpolate({ template: t(error.key, error.vars), vars: error.vars ?? {} }) : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** The preview image's `src` when it is still the current selection's; `null` for a stale event. */
|
|
90
|
+
function currentPreviewSrc({ image, currentUrl }, _optional = {}) {
|
|
91
|
+
const url = image.getAttribute('src');
|
|
92
|
+
return url && url === currentUrl ? url : null;
|
|
93
|
+
}
|
|
79
94
|
//# sourceMappingURL=use-share-image-warning.hooks.js.map
|
|
@@ -36,21 +36,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
36
36
|
readonly "Canonical URL": "Canonical URL";
|
|
37
37
|
readonly Noindex: "Noindex";
|
|
38
38
|
readonly Nofollow: "Nofollow";
|
|
39
|
-
readonly "
|
|
40
|
-
readonly "
|
|
41
|
-
readonly "
|
|
42
|
-
readonly "Twitter title": "Twitter title";
|
|
43
|
-
readonly "Twitter description": "Twitter description";
|
|
44
|
-
readonly "Twitter image": "Twitter image";
|
|
39
|
+
readonly "Preview title": "Preview title";
|
|
40
|
+
readonly "Preview description": "Preview description";
|
|
41
|
+
readonly "Preview image": "Preview image";
|
|
42
|
+
readonly "X/Twitter title": "X/Twitter title";
|
|
43
|
+
readonly "X/Twitter description": "X/Twitter description";
|
|
44
|
+
readonly "X/Twitter image": "X/Twitter image";
|
|
45
|
+
readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
|
|
46
|
+
readonly "Same as the preview image": "Same as the preview image";
|
|
47
|
+
readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
|
|
45
48
|
readonly "Saving\u2026": "Saving…";
|
|
46
49
|
readonly "Save overrides": "Save overrides";
|
|
47
50
|
readonly "Per-entry SEO": "Per-entry SEO";
|
|
48
51
|
readonly "Search appearance": "Search appearance";
|
|
49
52
|
readonly "Title template (must contain %s)": "Title template (must contain %s)";
|
|
50
53
|
readonly "Default meta description": "Default meta description";
|
|
51
|
-
readonly "Social
|
|
52
|
-
readonly "Default
|
|
53
|
-
readonly "Twitter @
|
|
54
|
+
readonly "Social preview": "Social preview";
|
|
55
|
+
readonly "Default preview image": "Default preview image";
|
|
56
|
+
readonly "X/Twitter @handle": "X/Twitter @handle";
|
|
54
57
|
readonly Crawling: "Crawling";
|
|
55
58
|
readonly "Default noindex": "Default noindex";
|
|
56
59
|
readonly "Default nofollow": "Default nofollow";
|
|
@@ -63,7 +66,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
63
66
|
readonly "View sitemap": "View sitemap";
|
|
64
67
|
readonly "Loading SEO settings\u2026": "Loading SEO settings…";
|
|
65
68
|
readonly Marketing: "Marketing";
|
|
66
|
-
readonly "Site-wide defaults for meta titles, descriptions,
|
|
69
|
+
readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
|
|
67
70
|
readonly "Choose image": "Choose image";
|
|
68
71
|
readonly Remove: "Remove";
|
|
69
72
|
readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
|
|
@@ -91,6 +94,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
91
94
|
readonly "failed to load the sitemap": "failed to load the sitemap";
|
|
92
95
|
readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
|
|
93
96
|
readonly "Selected image": "Selected image";
|
|
97
|
+
readonly "Using site default: {name}": "Using site default: {name}";
|
|
98
|
+
readonly "Using image from page content: {name}": "Using image from page content: {name}";
|
|
94
99
|
};
|
|
95
100
|
pages: {
|
|
96
101
|
readonly settings: {
|
|
@@ -158,21 +163,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
158
163
|
readonly "Canonical URL": "Canonical URL";
|
|
159
164
|
readonly Noindex: "Noindex";
|
|
160
165
|
readonly Nofollow: "Nofollow";
|
|
161
|
-
readonly "
|
|
162
|
-
readonly "
|
|
163
|
-
readonly "
|
|
164
|
-
readonly "Twitter title": "Twitter title";
|
|
165
|
-
readonly "Twitter description": "Twitter description";
|
|
166
|
-
readonly "Twitter image": "Twitter image";
|
|
166
|
+
readonly "Preview title": "Preview title";
|
|
167
|
+
readonly "Preview description": "Preview description";
|
|
168
|
+
readonly "Preview image": "Preview image";
|
|
169
|
+
readonly "X/Twitter title": "X/Twitter title";
|
|
170
|
+
readonly "X/Twitter description": "X/Twitter description";
|
|
171
|
+
readonly "X/Twitter image": "X/Twitter image";
|
|
172
|
+
readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
|
|
173
|
+
readonly "Same as the preview image": "Same as the preview image";
|
|
174
|
+
readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
|
|
167
175
|
readonly "Saving\u2026": "Saving…";
|
|
168
176
|
readonly "Save overrides": "Save overrides";
|
|
169
177
|
readonly "Per-entry SEO": "Per-entry SEO";
|
|
170
178
|
readonly "Search appearance": "Search appearance";
|
|
171
179
|
readonly "Title template (must contain %s)": "Title template (must contain %s)";
|
|
172
180
|
readonly "Default meta description": "Default meta description";
|
|
173
|
-
readonly "Social
|
|
174
|
-
readonly "Default
|
|
175
|
-
readonly "Twitter @
|
|
181
|
+
readonly "Social preview": "Social preview";
|
|
182
|
+
readonly "Default preview image": "Default preview image";
|
|
183
|
+
readonly "X/Twitter @handle": "X/Twitter @handle";
|
|
176
184
|
readonly Crawling: "Crawling";
|
|
177
185
|
readonly "Default noindex": "Default noindex";
|
|
178
186
|
readonly "Default nofollow": "Default nofollow";
|
|
@@ -185,7 +193,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
185
193
|
readonly "View sitemap": "View sitemap";
|
|
186
194
|
readonly "Loading SEO settings\u2026": "Loading SEO settings…";
|
|
187
195
|
readonly Marketing: "Marketing";
|
|
188
|
-
readonly "Site-wide defaults for meta titles, descriptions,
|
|
196
|
+
readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
|
|
189
197
|
readonly "Choose image": "Choose image";
|
|
190
198
|
readonly Remove: "Remove";
|
|
191
199
|
readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
|
|
@@ -213,6 +221,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
213
221
|
readonly "failed to load the sitemap": "failed to load the sitemap";
|
|
214
222
|
readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
|
|
215
223
|
readonly "Selected image": "Selected image";
|
|
224
|
+
readonly "Using site default: {name}": "Using site default: {name}";
|
|
225
|
+
readonly "Using image from page content: {name}": "Using image from page content: {name}";
|
|
216
226
|
};
|
|
217
227
|
readonly pages: {
|
|
218
228
|
readonly settings: {
|
|
@@ -281,21 +291,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
281
291
|
readonly "Canonical URL": "Canonical URL";
|
|
282
292
|
readonly Noindex: "Noindex";
|
|
283
293
|
readonly Nofollow: "Nofollow";
|
|
284
|
-
readonly "
|
|
285
|
-
readonly "
|
|
286
|
-
readonly "
|
|
287
|
-
readonly "Twitter title": "Twitter title";
|
|
288
|
-
readonly "Twitter description": "Twitter description";
|
|
289
|
-
readonly "Twitter image": "Twitter image";
|
|
294
|
+
readonly "Preview title": "Preview title";
|
|
295
|
+
readonly "Preview description": "Preview description";
|
|
296
|
+
readonly "Preview image": "Preview image";
|
|
297
|
+
readonly "X/Twitter title": "X/Twitter title";
|
|
298
|
+
readonly "X/Twitter description": "X/Twitter description";
|
|
299
|
+
readonly "X/Twitter image": "X/Twitter image";
|
|
300
|
+
readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
|
|
301
|
+
readonly "Same as the preview image": "Same as the preview image";
|
|
302
|
+
readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
|
|
290
303
|
readonly "Saving\u2026": "Saving…";
|
|
291
304
|
readonly "Save overrides": "Save overrides";
|
|
292
305
|
readonly "Per-entry SEO": "Per-entry SEO";
|
|
293
306
|
readonly "Search appearance": "Search appearance";
|
|
294
307
|
readonly "Title template (must contain %s)": "Title template (must contain %s)";
|
|
295
308
|
readonly "Default meta description": "Default meta description";
|
|
296
|
-
readonly "Social
|
|
297
|
-
readonly "Default
|
|
298
|
-
readonly "Twitter @
|
|
309
|
+
readonly "Social preview": "Social preview";
|
|
310
|
+
readonly "Default preview image": "Default preview image";
|
|
311
|
+
readonly "X/Twitter @handle": "X/Twitter @handle";
|
|
299
312
|
readonly Crawling: "Crawling";
|
|
300
313
|
readonly "Default noindex": "Default noindex";
|
|
301
314
|
readonly "Default nofollow": "Default nofollow";
|
|
@@ -308,7 +321,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
308
321
|
readonly "View sitemap": "View sitemap";
|
|
309
322
|
readonly "Loading SEO settings\u2026": "Loading SEO settings…";
|
|
310
323
|
readonly Marketing: "Marketing";
|
|
311
|
-
readonly "Site-wide defaults for meta titles, descriptions,
|
|
324
|
+
readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
|
|
312
325
|
readonly "Choose image": "Choose image";
|
|
313
326
|
readonly Remove: "Remove";
|
|
314
327
|
readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
|
|
@@ -336,6 +349,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
|
|
|
336
349
|
readonly "failed to load the sitemap": "failed to load the sitemap";
|
|
337
350
|
readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
|
|
338
351
|
readonly "Selected image": "Selected image";
|
|
352
|
+
readonly "Using site default: {name}": "Using site default: {name}";
|
|
353
|
+
readonly "Using image from page content: {name}": "Using image from page content: {name}";
|
|
339
354
|
};
|
|
340
355
|
readonly pages: {
|
|
341
356
|
readonly settings: {
|
|
@@ -65,6 +65,8 @@ import { useWiredSeo } from "../hooks/use-seo.hooks.js";
|
|
|
65
65
|
*/
|
|
66
66
|
export interface SeoSectionProps {
|
|
67
67
|
title: string;
|
|
68
|
+
/** Optional ⓘ explanation beside the title (explanations are tips, never loose paragraphs). */
|
|
69
|
+
tip?: string;
|
|
68
70
|
children: ReactNode;
|
|
69
71
|
}
|
|
70
72
|
export interface EntryPickerProps {
|
|
@@ -94,7 +96,7 @@ export interface SeoEntrySectionProps {
|
|
|
94
96
|
* and nothing else — the exact regression `deployment/HistoryTab.tsx`'s own comment records
|
|
95
97
|
* measuring live on its tab. It is also not a duplicate: the tab reads "Pages & posts" (what you
|
|
96
98
|
* pick from), the heading reads "Per-entry SEO" (what you are editing). */
|
|
97
|
-
export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoEntrySectionProps
|
|
99
|
+
export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoEntrySectionProps): import("react").JSX.Element;
|
|
98
100
|
/** What the two site-wide tab panels need from `useWiredSeo()`. Named as one object rather than
|
|
99
101
|
* passed as six loose props for the same reason `Roles.tsx`'s `RoleCreateFormController` documents:
|
|
100
102
|
* none of these is meaningful without the others, so naming the group is what lets a test hand a
|
|
@@ -116,7 +118,7 @@ export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoE
|
|
|
116
118
|
*/
|
|
117
119
|
export declare function SeoDefaultsTab({ controller }: {
|
|
118
120
|
controller: SeoTabController;
|
|
119
|
-
}
|
|
121
|
+
}): import("react").JSX.Element;
|
|
120
122
|
/**
|
|
121
123
|
* The "Sitemap" tab — the two sitemap actions, moved here verbatim, plus one line this screen never
|
|
122
124
|
* had: whether a sitemap is being published at all.
|
|
@@ -136,7 +138,7 @@ export declare function SeoDefaultsTab({ controller }: {
|
|
|
136
138
|
*/
|
|
137
139
|
export declare function SeoSitemapTab({ controller }: {
|
|
138
140
|
controller: SeoTabController;
|
|
139
|
-
}
|
|
141
|
+
}): import("react").JSX.Element;
|
|
140
142
|
/** Dispatches the one active tab's panel as a flat if-chain — a plain function rather than a
|
|
141
143
|
* ternary written directly in `Seo`'s JSX, which would be counted against that component's own
|
|
142
144
|
* complexity. Same split `Deployment.tsx`'s `deploymentTabPanel` and `Sites.tsx`'s `sitesTabPanel`
|
|
@@ -155,7 +157,7 @@ export interface SeoProps {
|
|
|
155
157
|
tab: string;
|
|
156
158
|
}, optional?: Record<string, never>) => void) | undefined;
|
|
157
159
|
}
|
|
158
|
-
export declare function Seo({ tabId, onTabChange, useSeoHook }?: SeoProps
|
|
160
|
+
export declare function Seo({ tabId, onTabChange, useSeoHook }?: SeoProps): import("react").JSX.Element;
|
|
159
161
|
export { Seo as SeoPage };
|
|
160
162
|
export default Seo;
|
|
161
163
|
//# sourceMappingURL=SeoPage.d.ts.map
|