@webx-ui/module-seo 0.1.0 → 0.2.1
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/dist/SeoAliasesPage.d.ts +20 -0
- package/dist/SeoCard.d.ts +1 -1
- package/dist/SeoLayout.d.ts +4 -4
- package/dist/SeoUrlsPage.d.ts +1 -1
- package/dist/api.d.ts +3 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +934 -753
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types.d.ts +47 -0
- package/package.json +4 -4
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/messages.ts","../src/i18n.ts","../src/types.ts","../src/SeoCard.vue","../src/SeoLayout.vue","../src/api.ts","../src/SeoRedirectDialog.vue","../src/TestUrlDialog.vue","../src/SeoRedirectsPage.vue","../src/rule.ts","../src/SeoUrlDialog.vue","../src/SeoUrlsPage.vue","../src/module.ts"],"sourcesContent":["import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * What this package says, in English — the same keys `webx-ui/module-seo` ships as\n * `lang/en/*.php`, so a key never reaches the screen when a translation has not arrived yet.\n *\n * The server's dictionary wins over these. There is no second dictionary inside this package:\n * a module translated into ten languages with an English dialog in the middle of it is worse\n * than one that is not translated at all.\n *\n * Placeholders are Laravel's, `:like-this` — the same line is read from a PHP file and from\n * here, and only one of the two spellings can be right.\n */\nexport const seoMessages: Record<string, Messages> = {\n module: {\n title: 'SEO',\n },\n\n page: {\n rules: 'Rules',\n redirects: 'Redirects',\n test: 'Check an address',\n\n 'new-rule': 'New rule',\n 'new-redirect': 'New redirect',\n 'search-rules': 'Search by address',\n 'search-redirects': 'Search by address or destination',\n empty: 'Nothing here yet.',\n\n address: 'Address',\n kind: 'Kind',\n priority: 'Priority',\n state: 'State',\n active: 'Active',\n inactive: 'Off',\n title: 'Title',\n target: 'Destination',\n status: 'Code',\n hits: 'Hits',\n 'last-hit': 'Last used',\n never: 'never',\n loop: 'Points at itself',\n\n 'any-kind': 'Any kind',\n exact: 'Exact',\n mask: 'Mask',\n regex: 'Regular expression',\n\n rule: 'Rule',\n redirect: 'Redirect',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n saved: 'Saved.',\n deleted: 'Deleted.',\n failed: 'Some values were not accepted. Check the highlighted fields.',\n 'delete-title': 'Delete :pattern?',\n 'delete-text': 'This cannot be undone.',\n\n 'address-help':\n 'A path with its query string. In a mask, * is one segment and ** is any number of them; a regular expression is stored as written, delimiters and all.',\n 'target-help':\n 'A path on this site, or a full address elsewhere. $1 puts back what a mask caught.',\n 'priority-help': 'Among rules of the same kind, the higher number wins.',\n\n 'test-placeholder': '/catalog/shoes?page=2',\n 'test-run': 'Check',\n 'test-empty': 'Type an address to see what the site will say about it.',\n 'test-matched': 'Matched rule',\n 'test-none': 'No rule matches this address.',\n 'test-redirected': 'This address is redirected to :target with a :status.',\n 'test-result': 'What the page will say',\n 'test-chain': 'Where each part came from',\n },\n\n card: {\n 'section-page': 'The page',\n 'section-share': 'Sharing',\n 'section-advanced': 'Advanced',\n\n title: 'Title',\n h1: 'Heading',\n description: 'Description',\n keywords: 'Keywords',\n canonical: 'Canonical address',\n 'canonical-help': 'The address this page should be indexed under, when it is not its own.',\n\n robots: 'Indexing',\n 'robots-kept': 'Also kept, as written: :directives',\n noindex: 'Keep out of the index',\n nofollow: 'Do not follow the links',\n noarchive: 'No cached copy',\n nosnippet: 'No snippet',\n noimageindex: 'Do not index the images',\n\n 'og-title': 'Share title',\n 'og-description': 'Share description',\n 'og-image': 'Share image',\n 'og-help': 'Left empty, a share preview uses the title and description above.',\n\n 'json-ld': 'Structured data',\n 'json-ld-help':\n 'A JSON-LD object, or a list of them. What the site says about itself comes from the settings; this is for a page that needs markup of its own.',\n 'json-ld-invalid': 'This is not valid JSON.',\n\n preview: 'In search results',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { seoMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useSeoMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-seo', seoMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","import type { LocalizedValue } from '@webx-ui/core'\n\n/** The shapes `webx-ui/module-seo` answers with, and what its forms send back. */\n\n/** How a pattern is compared with an address. */\nexport type MatchType = 'exact' | 'mask' | 'regex'\n\n/** What `wx-media` stores, plus the address the server works out on every read. */\nexport interface SeoImage {\n path: string\n alt?: string\n title?: string\n url?: string | null\n}\n\n/**\n * What a page says about itself.\n *\n * The text fields are language maps — the same `{ en: '…', ru: '…' }` every localized field in\n * the panel uses. The picture is not: there are no per-language images anywhere yet, and a\n * column that already holds an object would read `path` as a language code.\n */\nexport interface SeoFields {\n title: LocalizedValue\n h1: LocalizedValue\n description: LocalizedValue\n keywords: LocalizedValue\n og_title: LocalizedValue\n og_description: LocalizedValue\n og_image: SeoImage | null\n canonical: string | null\n robots: string | null\n /** A JSON-LD object, a list of them, or null. */\n json_ld: unknown\n}\n\n/**\n * The value of a `wx-seo` field: the same fields, none of them required.\n *\n * A record that has never been given any SEO stores nothing rather than a shape full of nulls.\n */\nexport type SeoValue = Partial<SeoFields>\n\nexport interface SeoUrlRule extends SeoFields {\n id: number\n match_type: MatchType\n pattern: string\n priority: number\n is_active: boolean\n created_at: string | null\n updated_at: string | null\n /**\n * What a table row is, as far as `WxTable` is concerned. Without it this type does not\n * extend `TableRow` and every cell slot hands back `unknown`.\n */\n [key: string]: unknown\n}\n\n/** A `PUT` replaces the whole rule: the form edits every field at once. */\nexport interface SeoUrlInput extends SeoValue {\n match_type: MatchType\n pattern: string\n priority?: number\n is_active?: boolean\n}\n\nexport interface SeoRedirect {\n id: number\n match_type: MatchType\n pattern: string\n target: string\n status: number\n is_active: boolean\n hits: number\n last_hit_at: string | null\n /** The server says so rather than refusing the row: a loop is skipped, not rejected. */\n is_loop: boolean\n created_at: string | null\n updated_at: string | null\n [key: string]: unknown\n}\n\nexport interface SeoRedirectInput {\n match_type: MatchType\n pattern: string\n target: string\n status?: number\n is_active?: boolean\n}\n\n/**\n * A page, flat — what `->paginate()` serialises and `WxTable` reads as it arrives. A resource\n * collection nests the same numbers under `meta`; the client flattens them once, here.\n */\nexport interface SeoPage<T> {\n data: T[]\n current_page: number\n last_page: number\n per_page: number\n total: number\n from: number | null\n to: number | null\n}\n\nexport interface SeoUrlQuery {\n q?: string\n match_type?: MatchType | null\n is_active?: boolean | null\n page?: number\n per_page?: number\n}\n\nexport interface SeoRedirectQuery extends SeoUrlQuery {\n sort?: string\n}\n\n/** What a page ends up saying, every source merged. */\nexport interface SeoResolved {\n title: string | null\n h1: string | null\n description: string | null\n keywords: string | null\n canonical: string | null\n robots: string | null\n og: Record<string, string>\n json_ld: Record<string, unknown>[]\n}\n\n/** One source's contribution, before the merge. */\nexport interface SeoChainStep {\n source: string\n priority: number\n data: SeoResolved\n}\n\n/** The answer to \"why does this page say that\". */\nexport interface SeoTestResult {\n url: string\n /** Said first because it happens first: a redirected address never reaches the rules. */\n redirect: SeoRedirect | null\n matched: SeoUrlRule | null\n chain: SeoChainStep[]\n seo: SeoResolved\n}\n\n/** The meta directives the card offers as checkboxes. Anything else is kept as written. */\nexport const robotsDirectives = [\n 'noindex',\n 'nofollow',\n 'noarchive',\n 'nosnippet',\n 'noimageindex',\n] as const\n\nexport type RobotsDirective = (typeof robotsDirectives)[number]\n","<script setup lang=\"ts\">\nimport { computed, ref, type Component } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport {\n localizedValue,\n useLocales,\n WxCheckboxGroup,\n WxCodeEditor,\n WxFormItem,\n WxInput,\n WxTabs,\n WxTab,\n WxText,\n WxTextarea,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport { useSeoMessages } from './i18n'\nimport { robotsDirectives, type SeoImage, type SeoValue } from './types'\n\n/**\n * What a page says about itself, as a field.\n *\n * Registered as `wx-seo`, so it can be dropped into any screen by a patch, and used directly by\n * the rule form in this package. The value is one object — the same shape the server stores —\n * rather than a dozen loose keys, because SEO is edited and reasoned about as a unit.\n *\n * The text fields are language maps and say so: the same chip every other localized field in the\n * panel grows, moving together with them.\n */\ndefineOptions({ name: 'WxSeo' })\n\nconst props = withDefaults(\n defineProps<{\n /**\n * The field that picks the share image — `WxMediaField` in a panel that has the library.\n *\n * Handed in rather than imported: this package does not depend on the media one, and a panel\n * without a library still edits everything else about a page.\n */\n mediaField?: Component\n disabled?: boolean\n /**\n * Soft limits, for the counters only. Long is not wrong — search engines shorten what they\n * shorten — so nothing here refuses a longer line, it only says how long it is.\n */\n titleLimit?: number\n descriptionLimit?: number\n /** What the snippet preview shows above the title. */\n site?: string\n /** Hidden when a screen has no use for them. */\n structuredData?: boolean\n }>(),\n {\n mediaField: undefined,\n disabled: false,\n titleLimit: 60,\n descriptionLimit: 160,\n site: undefined,\n structuredData: true,\n },\n)\n\nconst value = defineModel<SeoValue | null>({ default: null })\n\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\nconst locales = useLocales()\n\nconst jsonLdText = ref<string | null>(null)\nconst jsonLdBroken = ref(false)\n\n/** One field of the value, without rebuilding the object in every handler. */\nfunction field<K extends keyof SeoValue>(key: K) {\n return computed({\n get: () => value.value?.[key],\n set: (next: SeoValue[K]) => {\n value.value = { ...(value.value ?? {}), [key]: next }\n },\n })\n}\n\nconst title = field('title')\nconst h1 = field('h1')\nconst description = field('description')\nconst keywords = field('keywords')\nconst ogTitle = field('og_title')\nconst ogDescription = field('og_description')\n\nconst canonical = computed<string>({\n get: () => value.value?.canonical ?? '',\n set: (next: string) => {\n value.value = { ...(value.value ?? {}), canonical: next.trim() === '' ? null : next }\n },\n})\n\nconst image = computed({\n get: () => value.value?.og_image ?? null,\n set: (next: SeoImage | null) => {\n value.value = { ...(value.value ?? {}), og_image: next }\n },\n})\n\n/* The directives the checkboxes cover. Anything else somebody wrote — `max-snippet:20` and its\n kind — is kept and shown, because silently dropping a line a person typed is worse than not\n offering a box for it. */\nconst directives = computed(() => (value.value?.robots ?? '').split(',').map((one) => one.trim()))\n\nconst kept = computed(() =>\n directives.value.filter((one) => one !== '' && !robotsDirectives.includes(one as never)),\n)\n\nconst known = computed({\n get: () => directives.value.filter((one) => robotsDirectives.includes(one as never)),\n set: (next: readonly string[]) => {\n const merged = [...next, ...kept.value].join(', ')\n value.value = { ...(value.value ?? {}), robots: merged === '' ? null : merged }\n },\n})\n\nconst robotsOptions = computed(() =>\n robotsDirectives.map((one) => ({ value: one, label: t(`card.${one}`) })),\n)\n\nconst jsonLd = computed<string>({\n get: () => {\n if (jsonLdText.value !== null) return jsonLdText.value\n\n const stored = value.value?.json_ld\n\n return stored === null || stored === undefined ? '' : JSON.stringify(stored, null, 2)\n },\n set: (next: string) => {\n /* The text is kept as typed while it is being typed: reformatting somebody's JSON the\n moment it happens to parse moves the caret out from under them. */\n jsonLdText.value = next\n\n if (next.trim() === '') {\n jsonLdBroken.value = false\n value.value = { ...(value.value ?? {}), json_ld: null }\n\n return\n }\n\n try {\n value.value = { ...(value.value ?? {}), json_ld: JSON.parse(next) }\n jsonLdBroken.value = false\n } catch {\n jsonLdBroken.value = true\n }\n },\n})\n\n/** What the counters and the preview read: the language being edited. */\nfunction inLocale(map: LocalizedValue | undefined): string {\n return localizedValue(map, locales.active.value)\n}\n\nconst counted = computed(() => ({\n title: inLocale(title.value as LocalizedValue | undefined).length,\n description: inLocale(description.value as LocalizedValue | undefined).length,\n}))\n\nconst preview = computed(() => ({\n title: inLocale(title.value as LocalizedValue | undefined),\n description: inLocale(description.value as LocalizedValue | undefined),\n address: canonical.value || (props.site ?? ''),\n}))\n\nconst hasPreview = computed(() => preview.value.title !== '' || preview.value.description !== '')\n\nconst keptHelp = computed(() =>\n kept.value.length === 0\n ? undefined\n : t('card.robots-kept', { directives: kept.value.join(', ') }),\n)\n\nfunction tone(count: number, limit: number): 'muted' | 'warning' {\n return count > limit ? 'warning' : 'muted'\n}\n</script>\n\n<template>\n <div class=\"wx-seo\">\n <wx-tabs keep-alive>\n <wx-tab :label=\"t('card.section-page')\" value=\"page\">\n <div class=\"wx-seo__fields\">\n <wx-form-item :label=\"t('card.title')\" :disabled=\"disabled\">\n <wx-input v-model=\"title\" localized />\n <div class=\"wx-seo__count\">\n <wx-text size=\"sm\" :tone=\"tone(counted.title, titleLimit)\">\n {{ counted.title }} / {{ titleLimit }}\n </wx-text>\n </div>\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.h1')\" :disabled=\"disabled\">\n <wx-input v-model=\"h1\" localized />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.description')\" :disabled=\"disabled\">\n <wx-textarea v-model=\"description\" localized :rows=\"3\" />\n <div class=\"wx-seo__count\">\n <wx-text size=\"sm\" :tone=\"tone(counted.description, descriptionLimit)\">\n {{ counted.description }} / {{ descriptionLimit }}\n </wx-text>\n </div>\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.keywords')\" :disabled=\"disabled\">\n <wx-input v-model=\"keywords\" localized />\n </wx-form-item>\n\n <div v-if=\"hasPreview\" class=\"wx-seo__preview\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('card.preview') }}</wx-text>\n <div class=\"wx-seo__snippet\">\n <div v-if=\"preview.address\" class=\"wx-seo__snippet-address\">\n {{ preview.address }}\n </div>\n <div class=\"wx-seo__snippet-title\">{{ preview.title }}</div>\n <div class=\"wx-seo__snippet-text\">{{ preview.description }}</div>\n </div>\n </div>\n </div>\n </wx-tab>\n\n <wx-tab :label=\"t('card.section-share')\" value=\"share\">\n <div class=\"wx-seo__fields\">\n <component\n :is=\"mediaField\"\n v-if=\"mediaField\"\n v-model=\"image\"\n :label=\"t('card.og-image')\"\n aspect=\"16/9\"\n accept=\"image\"\n :disabled=\"disabled\"\n />\n\n <wx-form-item :label=\"t('card.og-title')\" :help=\"t('card.og-help')\" :disabled=\"disabled\">\n <wx-input v-model=\"ogTitle\" localized />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.og-description')\" :disabled=\"disabled\">\n <wx-textarea v-model=\"ogDescription\" localized :rows=\"2\" />\n </wx-form-item>\n </div>\n </wx-tab>\n\n <wx-tab :label=\"t('card.section-advanced')\" value=\"advanced\">\n <div class=\"wx-seo__fields\">\n <wx-form-item\n :label=\"t('card.canonical')\"\n :help=\"t('card.canonical-help')\"\n :disabled=\"disabled\"\n >\n <wx-input v-model=\"canonical\" placeholder=\"https://example.com/page\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.robots')\" :help=\"keptHelp\" :disabled=\"disabled\">\n <wx-checkbox-group v-model=\"known\" :options=\"robotsOptions\" />\n </wx-form-item>\n\n <wx-form-item\n v-if=\"structuredData\"\n :label=\"t('card.json-ld')\"\n :help=\"t('card.json-ld-help')\"\n :error=\"jsonLdBroken ? t('card.json-ld-invalid') : undefined\"\n :disabled=\"disabled\"\n >\n <wx-code-editor\n v-model=\"jsonLd\"\n language=\"json\"\n lint\n min-height=\"120px\"\n max-height=\"420px\"\n />\n </wx-form-item>\n </div>\n </wx-tab>\n </wx-tabs>\n </div>\n</template>\n\n<style scoped>\n.wx-seo {\n container-type: inline-size;\n}\n\n.wx-seo__fields {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n padding-block-start: var(--wx-space-12);\n}\n\n.wx-seo__count {\n display: flex;\n justify-content: flex-end;\n}\n\n.wx-seo__preview {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-4);\n}\n\n/* Not a copy of any one search engine's styling — a reminder of how much of a line survives. */\n.wx-seo__snippet {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-2);\n padding: var(--wx-space-12);\n border: 1px solid var(--wx-color-border);\n border-radius: var(--wx-radius-md);\n background: var(--wx-color-surface-sunken);\n}\n\n.wx-seo__snippet-address {\n color: var(--wx-color-text-muted);\n font-size: var(--wx-font-size-sm);\n overflow-wrap: anywhere;\n}\n\n.wx-seo__snippet-title {\n color: var(--wx-color-primary);\n font-size: var(--wx-font-size-lg);\n overflow-wrap: anywhere;\n}\n\n.wx-seo__snippet-text {\n color: var(--wx-color-text);\n font-size: var(--wx-font-size-sm);\n overflow-wrap: anywhere;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport { WxButton, WxHeading, WxSegmented, WxSpace } from '@webx-ui/core'\nimport { useSeoMessages } from './i18n'\n\n/**\n * The head of the section: its name, the switch between what it holds, and the one tool that\n * belongs to the whole of it.\n *\n * Rules and redirects are two screens rather than two tabs of one, because they are two tables\n * with their own paging and their own search — and a tab that quietly resets both when you come\n * back to it is worse than a second address.\n */\nconst props = defineProps<{ base: string; current: 'rules' | 'redirects' }>()\n\nconst emit = defineEmits<{ test: [] }>()\n\nconst context = useAdmin()\nconst router = useRouter()\nconst route = useRoute()\n\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\n/** The section's name as the server translated it; the built-in English until it arrives. */\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'seo')?.title ??\n t('module.title'),\n)\n\nconst options = computed(() => [\n { value: 'rules', label: t('page.rules') },\n { value: 'redirects', label: t('page.redirects') },\n])\n\nconst where = computed({\n get: () => props.current,\n set: (next: string | number) => {\n const path = next === 'redirects' ? `${props.base}/redirects` : props.base\n\n if (path !== route.path) void router.push(path)\n },\n})\n</script>\n\n<template>\n <div class=\"wx-seo-layout\">\n <div class=\"wx-seo-layout__head\">\n <wx-heading :level=\"2\">{{ title }}</wx-heading>\n\n <wx-space size=\"sm\">\n <wx-segmented v-model=\"where\" :options=\"options\" size=\"sm\" />\n <wx-button variant=\"outline\" icon=\"search\" @click=\"emit('test')\">\n {{ t('page.test') }}\n </wx-button>\n </wx-space>\n </div>\n\n <slot />\n </div>\n</template>\n\n<style scoped>\n.wx-seo-layout {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n container-type: inline-size;\n}\n\n.wx-seo-layout__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--wx-space-12);\n flex-wrap: wrap;\n}\n\n/* On a phone the heading takes the first line and the controls the second, full width, rather\n than three things elbowing each other on one. */\n@container (max-width: 560px) {\n .wx-seo-layout__head > * {\n flex: 1 1 100%;\n }\n}\n</style>\n","import type { AdminContext } from '@webx-ui/module-admin'\nimport type {\n SeoPage,\n SeoRedirect,\n SeoRedirectInput,\n SeoRedirectQuery,\n SeoTestResult,\n SeoUrlInput,\n SeoUrlQuery,\n SeoUrlRule,\n} from './types'\n\nexport interface SeoApi {\n /** The rules, in the order the site tries them. */\n urls(query?: SeoUrlQuery): Promise<SeoPage<SeoUrlRule>>\n url(id: number): Promise<SeoUrlRule>\n createUrl(input: SeoUrlInput): Promise<SeoUrlRule>\n updateUrl(id: number, input: SeoUrlInput): Promise<SeoUrlRule>\n removeUrl(id: number): Promise<void>\n\n redirects(query?: SeoRedirectQuery): Promise<SeoPage<SeoRedirect>>\n createRedirect(input: SeoRedirectInput): Promise<SeoRedirect>\n updateRedirect(id: number, input: SeoRedirectInput): Promise<SeoRedirect>\n removeRedirect(id: number): Promise<void>\n\n /** What an address ends up saying, and where every part of it came from. */\n test(url: string, locale?: string | null): Promise<SeoTestResult>\n}\n\n/** Everything under `/seo`, below the panel's API path. */\nexport function createSeoApi(admin: AdminContext): SeoApi {\n const base = `${admin.apiPath}/seo`\n const data = <T>(body: { data: T }): T => body.data\n\n /* A paginated list arrives as a resource collection — rows under `data`, numbers under\n `meta`. The table wants them on one object, so they are joined here once rather than in\n every screen. */\n const page = <T>(body: { data: T[]; meta: Omit<SeoPage<T>, 'data'> }): SeoPage<T> => ({\n ...body.meta,\n data: body.data,\n })\n\n /* `undefined` drops the parameter; `null` and `''` would be sent as words and read as a\n filter nobody asked for. */\n const listQuery = (\n query: SeoRedirectQuery,\n ): Record<string, string | number | boolean | null | undefined> => ({\n q: query.q || undefined,\n match_type: query.match_type ?? undefined,\n is_active: query.is_active ?? undefined,\n sort: query.sort || undefined,\n page: query.page,\n per_page: query.per_page,\n })\n\n return {\n urls: (query = {}) =>\n admin.http\n .get<{ data: SeoUrlRule[]; meta: Omit<SeoPage<SeoUrlRule>, 'data'> }>(`${base}/urls`, {\n query: listQuery(query),\n })\n .then(page),\n\n url: (id) => admin.http.get<{ data: SeoUrlRule }>(`${base}/urls/${id}`).then(data),\n\n createUrl: (input) => admin.http.post<{ data: SeoUrlRule }>(`${base}/urls`, input).then(data),\n\n updateUrl: (id, input) =>\n admin.http.put<{ data: SeoUrlRule }>(`${base}/urls/${id}`, input).then(data),\n\n removeUrl: (id) => admin.http.delete<void>(`${base}/urls/${id}`),\n\n redirects: (query = {}) =>\n admin.http\n .get<{ data: SeoRedirect[]; meta: Omit<SeoPage<SeoRedirect>, 'data'> }>(\n `${base}/redirects`,\n { query: listQuery(query) },\n )\n .then(page),\n\n createRedirect: (input) =>\n admin.http.post<{ data: SeoRedirect }>(`${base}/redirects`, input).then(data),\n\n updateRedirect: (id, input) =>\n admin.http.put<{ data: SeoRedirect }>(`${base}/redirects/${id}`, input).then(data),\n\n removeRedirect: (id) => admin.http.delete<void>(`${base}/redirects/${id}`),\n\n // A POST because it carries an address in its body, and an address in a query string is an\n // address somebody has to escape twice.\n test: (url, locale = null) =>\n admin.http\n .post<{ data: SeoTestResult }>(`${base}/test-url`, { url, locale: locale ?? undefined })\n .then(data),\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n toast,\n useModal,\n WxButton,\n WxDialog,\n WxFormItem,\n WxInput,\n WxSelect,\n WxSpace,\n WxSwitch,\n} from '@webx-ui/core'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { MatchType, SeoRedirect } from './types'\n\n/** One address that has moved, and where to. Four fields, so four fields. */\nconst props = withDefaults(defineProps<{ redirect?: SeoRedirect | null }>(), { redirect: null })\n\nconst { resolve, dismiss, open } = useModal<SeoRedirect>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst form = ref({\n match_type: 'exact' as MatchType,\n pattern: '',\n target: '',\n status: 301,\n is_active: true,\n})\n\nconst editing = computed(() => props.redirect !== null)\n\nconst kindOptions = computed(() => [\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\n/* Numbers, not words: 301 and 302 are what a person types into a search when they want to know\n what one is, and a label would only stand between them and that. */\nconst statusOptions = [\n { value: 301, label: '301' },\n { value: 302, label: '302' },\n]\n\nwatch(\n () => props.redirect,\n (redirect) => {\n form.value = {\n match_type: redirect?.match_type ?? 'exact',\n pattern: redirect?.pattern ?? '',\n target: redirect?.target ?? '',\n status: redirect?.status ?? 301,\n is_active: redirect?.is_active ?? true,\n }\n errors.value = {}\n },\n { immediate: true },\n)\n\nfunction errorOf(field: string): string | undefined {\n return errors.value[field]?.[0]\n}\n\nasync function save(): Promise<void> {\n saving.value = true\n errors.value = {}\n\n try {\n const saved = props.redirect\n ? await api.updateRedirect(props.redirect.id, form.value)\n : await api.createRedirect(form.value)\n\n toast.success(t('page.saved'))\n resolve(saved)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]>; message?: string } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('page.failed'))\n } else {\n toast.danger(body?.message ?? t('page.failed'))\n }\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.redirect')\" :width=\"640\">\n <div class=\"wx-seo-redirect\">\n <wx-form-item :label=\"t('page.kind')\">\n <wx-select v-model=\"form.match_type\" :options=\"kindOptions\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('page.address')\"\n :help=\"t('page.address-help')\"\n :error=\"errorOf('pattern')\"\n >\n <wx-input v-model=\"form.pattern\" placeholder=\"/old-address\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('page.target')\"\n :help=\"t('page.target-help')\"\n :error=\"errorOf('target')\"\n >\n <wx-input v-model=\"form.target\" placeholder=\"/new-address\" />\n </wx-form-item>\n\n <div class=\"wx-seo-redirect__row\">\n <wx-form-item :label=\"t('page.status')\">\n <wx-select v-model=\"form.status\" :options=\"statusOptions\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.state')\">\n <wx-switch v-model=\"form.is_active\" />\n </wx-form-item>\n </div>\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('page.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" @click=\"save\">\n {{ editing ? t('page.save') : t('page.new-redirect') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-redirect {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n}\n\n.wx-seo-redirect__row {\n display: grid;\n grid-template-columns: 140px 1fr;\n gap: var(--wx-space-12);\n align-items: start;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n WxAlert,\n WxBadge,\n WxButton,\n WxDescriptions,\n WxDescriptionsItem,\n WxDialog,\n WxEmpty,\n WxInput,\n WxSpace,\n WxText,\n useModal,\n} from '@webx-ui/core'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { SeoTestResult } from './types'\n\n/**\n * \"Why does this page have the wrong title?\"\n *\n * The most common question this section gets, and the reason it should take one call to answer\n * rather than a reading of the code: which redirect catches the address, which rule matches it,\n * what every source contributed, and what the page ends up saying.\n */\nconst { open } = useModal<void>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst url = ref('')\nconst running = ref(false)\nconst result = ref<SeoTestResult | null>(null)\n\nconst fields = computed(() => {\n const seo = result.value?.seo\n\n if (!seo) return []\n\n return [\n { label: t('card.title'), value: seo.title },\n { label: t('card.h1'), value: seo.h1 },\n { label: t('card.description'), value: seo.description },\n { label: t('card.keywords'), value: seo.keywords },\n { label: t('card.canonical'), value: seo.canonical },\n { label: t('card.robots'), value: seo.robots },\n ].filter((one) => one.value)\n})\n\nasync function run(): Promise<void> {\n if (url.value.trim() === '') return\n\n running.value = true\n\n try {\n result.value = await api.test(url.value)\n } finally {\n running.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.test')\" :width=\"720\">\n <div class=\"wx-seo-test\">\n <wx-space size=\"sm\" class=\"wx-seo-test__ask\">\n <wx-input\n v-model=\"url\"\n :placeholder=\"t('page.test-placeholder')\"\n class=\"wx-seo-test__input\"\n @keyup.enter=\"run\"\n />\n <wx-button type=\"primary\" :loading=\"running\" @click=\"run\">\n {{ t('page.test-run') }}\n </wx-button>\n </wx-space>\n\n <template v-if=\"result\">\n <!-- First, because it happens first: a redirected address never reaches the rules. -->\n <wx-alert\n v-if=\"result.redirect\"\n type=\"warning\"\n :description=\"\n t('page.test-redirected', {\n target: result.redirect.target,\n status: result.redirect.status,\n })\n \"\n />\n\n <wx-alert v-if=\"!result.matched\" type=\"info\" :description=\"t('page.test-none')\" />\n <div v-else class=\"wx-seo-test__matched\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-matched') }}</wx-text>\n <wx-space size=\"sm\" align=\"center\">\n <wx-badge>{{ t(`page.${result.matched.match_type}`) }}</wx-badge>\n <wx-text mono size=\"sm\">{{ result.matched.pattern }}</wx-text>\n </wx-space>\n </div>\n\n <div v-if=\"fields.length > 0\" class=\"wx-seo-test__result\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-result') }}</wx-text>\n <wx-descriptions :columns=\"1\" bordered size=\"sm\">\n <wx-descriptions-item v-for=\"one in fields\" :key=\"one.label\" :label=\"one.label\">\n {{ one.value }}\n </wx-descriptions-item>\n </wx-descriptions>\n </div>\n\n <div v-if=\"result.chain.length > 0\" class=\"wx-seo-test__chain\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-chain') }}</wx-text>\n <wx-space size=\"xs\" wrap>\n <wx-badge v-for=\"step in result.chain\" :key=\"step.source\">\n {{ step.source }} · {{ step.priority }}\n </wx-badge>\n </wx-space>\n </div>\n </template>\n\n <wx-empty v-else-if=\"!running\" icon=\"search\" :description=\"t('page.test-empty')\" />\n </div>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-test {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n}\n\n.wx-seo-test__ask {\n width: 100%;\n}\n\n.wx-seo-test__input {\n flex: 1 1 auto;\n}\n\n.wx-seo-test__matched,\n.wx-seo-test__result,\n.wx-seo-test__chain {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-6);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAction,\n WxActions,\n WxBadge,\n WxButton,\n WxCard,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport SeoLayout from './SeoLayout.vue'\nimport SeoRedirectDialog from './SeoRedirectDialog.vue'\nimport TestUrlDialog from './TestUrlDialog.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { SeoPage, SeoRedirect } from './types'\n\n/**\n * The addresses that have moved.\n *\n * Busiest first by default, which is the order this list is worked on: a redirect nobody has\n * followed since the site was rebuilt is the one worth deleting, and it sits at the bottom.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/seo' })\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst page = ref<SeoPage<SeoRedirect> | null>(null)\nconst loading = ref(false)\n\nlet last: TableState = { page: 1, perPage: 20, sort: null, search: '' }\n\nconst canManage = context.can('seo.manage')\n\nconst edit = createModal<SeoRedirect, { redirect: SeoRedirect | null }>(SeoRedirectDialog)\nconst test = createModal<void, Record<string, never>>(TestUrlDialog)\n\nconst columns = computed<TableColumn<SeoRedirect>[]>(() => [\n { key: 'pattern', label: t('page.address'), sortable: true },\n { key: 'target', label: t('page.target') },\n { key: 'status', label: t('page.status'), align: 'center', hideBelow: 560 },\n { key: 'hits', label: t('page.hits'), align: 'center', sortable: true, hideBelow: 760 },\n {\n key: 'last_hit_at',\n label: t('page.last-hit'),\n sortable: true,\n hideBelow: 900,\n hideOnCards: true,\n },\n { key: 'is_active', label: t('page.state'), align: 'center', hideBelow: 660 },\n {\n key: 'actions',\n label: '',\n width: 56,\n align: 'right',\n hidden: !canManage,\n hideOnCards: true,\n },\n])\n\nasync function load(state: TableState): Promise<void> {\n last = state\n loading.value = true\n\n try {\n page.value = await api.redirects({\n q: state.search,\n sort: state.sort ? `${state.sort.order === 'desc' ? '-' : ''}${state.sort.key}` : '-hits',\n page: state.page,\n per_page: state.perPage,\n })\n } finally {\n loading.value = false\n }\n}\n\nasync function open(redirect: SeoRedirect | null): Promise<void> {\n const saved = await edit({ redirect })\n\n if (saved) void load(last)\n}\n\nasync function remove(redirect: SeoRedirect): Promise<void> {\n const agreed = await confirm({\n title: t('page.delete-title', { pattern: redirect.pattern }),\n message: t('page.delete-text'),\n confirmText: t('page.delete'),\n cancelText: t('page.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeRedirect(redirect.id)\n toast.success(t('page.deleted'))\n void load(last)\n } catch (error) {\n toast.danger((error as { body?: { message?: string } }).body?.message ?? String(error))\n }\n}\n</script>\n\n<template>\n <seo-layout :base=\"props.base\" current=\"redirects\" @test=\"test({})\">\n <wx-card>\n <template #header>{{ t('page.redirects') }}</template>\n\n <template v-if=\"canManage\" #extra>\n <wx-button type=\"primary\" icon=\"add\" @click=\"open(null)\">\n {{ t('page.new-redirect') }}\n </wx-button>\n </template>\n\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('page.search-redirects')\"\n :empty-text=\"t('page.empty')\"\n @state-change=\"load\"\n @row-click=\"canManage ? open($event) : undefined\"\n >\n <template #cell-pattern=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.pattern }}</wx-text>\n <!-- Said out loud rather than refused on save: the middleware steps over it, and a\n row that quietly does nothing is a row nobody ever fixes. -->\n <wx-badge v-if=\"row.is_loop\" type=\"warning\">{{ t('page.loop') }}</wx-badge>\n </template>\n\n <template #cell-target=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.target }}</wx-text>\n </template>\n\n <template #cell-last_hit_at=\"{ row }\">\n <wx-text size=\"sm\" :tone=\"row.last_hit_at ? 'default' : 'muted'\">\n {{ row.last_hit_at ? new Date(row.last_hit_at).toLocaleString() : t('page.never') }}\n </wx-text>\n </template>\n\n <template #cell-is_active=\"{ row }\">\n <wx-badge :type=\"row.is_active ? 'success' : 'default'\" dot>\n {{ row.is_active ? t('page.active') : t('page.inactive') }}\n </wx-badge>\n </template>\n\n <template #card-actions=\"{ row }\">\n <wx-actions v-if=\"canManage\" size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <wx-actions size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n </wx-table>\n </wx-card>\n </seo-layout>\n</template>\n","import { localizedValue, type LocalizedValue } from '@webx-ui/core'\nimport type { SeoUrlInput, SeoUrlRule, SeoValue } from './types'\n\n/** The fields the card owns, as opposed to the ones the rule form owns. */\nconst SEO_FIELDS = [\n 'title',\n 'h1',\n 'description',\n 'keywords',\n 'og_title',\n 'og_description',\n 'og_image',\n 'canonical',\n 'robots',\n 'json_ld',\n] as const\n\n/**\n * What a rule is called in a table.\n *\n * Any language rather than the current one: a record half translated is the normal state of a\n * site being worked on, and a blank cell says less than the language that does have the words.\n */\nexport function ruleTitle(rule: SeoUrlRule, locale?: string): string {\n return localizedValue(rule.title as LocalizedValue | undefined, locale)\n}\n\n/** The SEO half of a rule, for the card to edit. */\nexport function seoOf(rule: SeoUrlRule | null): SeoValue {\n if (!rule) return {}\n\n const value: SeoValue = {}\n\n for (const key of SEO_FIELDS) {\n const one = rule[key]\n\n if (one !== null && one !== undefined) {\n Object.assign(value, { [key]: one })\n }\n }\n\n return value\n}\n\n/**\n * What the form sends: the rule's own fields, with the card's value spread back over them.\n *\n * Every field of the card goes, present or not — a `PUT` replaces the whole rule, so a\n * description somebody cleared has to arrive as nothing rather than not arrive at all.\n */\nexport function ruleInput(\n own: Pick<SeoUrlInput, 'match_type' | 'pattern' | 'priority' | 'is_active'>,\n seo: SeoValue,\n): SeoUrlInput {\n const input: SeoUrlInput = { ...own }\n\n for (const key of SEO_FIELDS) {\n Object.assign(input, { [key]: seo[key] ?? null })\n }\n\n return input\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watch, type Component } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n toast,\n useModal,\n WxButton,\n WxDialog,\n WxFormItem,\n WxInput,\n WxInputNumber,\n WxSelect,\n WxSpace,\n WxSwitch,\n} from '@webx-ui/core'\nimport SeoCard from './SeoCard.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport { ruleInput, seoOf } from './rule'\nimport type { MatchType, SeoUrlRule, SeoValue } from './types'\n\n/**\n * One rule: which addresses it covers, and what it says about them.\n *\n * A dialog rather than a second route, and a template rather than a described screen — the same\n * decision `admins.form` is on, until the screens mechanism has been round the block a few more\n * times. What it edits below the address is `WxSeo`, the same card a content module will get by\n * patching it into its own form.\n */\nconst props = withDefaults(defineProps<{ rule?: SeoUrlRule | null; mediaField?: Component }>(), {\n rule: null,\n mediaField: undefined,\n})\n\nconst { resolve, dismiss, open } = useModal<SeoUrlRule>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst form = ref({\n match_type: 'exact' as MatchType,\n pattern: '',\n priority: 0,\n is_active: true,\n})\n\nconst seo = ref<SeoValue>({})\n\nconst editing = computed(() => props.rule !== null)\n\nconst kindOptions = computed(() => [\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\nwatch(\n () => props.rule,\n (rule) => {\n form.value = {\n match_type: rule?.match_type ?? 'exact',\n pattern: rule?.pattern ?? '',\n priority: rule?.priority ?? 0,\n is_active: rule?.is_active ?? true,\n }\n seo.value = seoOf(rule)\n errors.value = {}\n },\n { immediate: true },\n)\n\nfunction errorOf(field: string): string | undefined {\n return errors.value[field]?.[0]\n}\n\nasync function save(): Promise<void> {\n saving.value = true\n errors.value = {}\n\n const input = ruleInput(form.value, seo.value)\n\n try {\n const saved = props.rule\n ? await api.updateUrl(props.rule.id, input)\n : await api.createUrl(input)\n\n toast.success(t('page.saved'))\n resolve(saved)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]>; message?: string } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('page.failed'))\n } else {\n toast.danger(body?.message ?? t('page.failed'))\n }\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.rule')\" :width=\"860\">\n <div class=\"wx-seo-rule\">\n <div class=\"wx-seo-rule__where\">\n <wx-form-item :label=\"t('page.kind')\">\n <wx-select v-model=\"form.match_type\" :options=\"kindOptions\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('page.address')\"\n :help=\"t('page.address-help')\"\n :error=\"errorOf('pattern')\"\n class=\"wx-seo-rule__address\"\n >\n <wx-input v-model=\"form.pattern\" placeholder=\"/catalog/shoes\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.priority')\" :help=\"t('page.priority-help')\">\n <wx-input-number v-model=\"form.priority\" :step=\"10\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.state')\">\n <wx-switch v-model=\"form.is_active\" />\n </wx-form-item>\n </div>\n\n <seo-card v-model=\"seo\" :media-field=\"props.mediaField\" />\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('page.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" @click=\"save\">\n {{ editing ? t('page.save') : t('page.new-rule') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-rule {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n container-type: inline-size;\n}\n\n/* The address and its kind on one line: they are one decision, and reading them apart is\n reading half of it. */\n.wx-seo-rule__where {\n display: grid;\n grid-template-columns: 180px 1fr 140px 100px;\n gap: var(--wx-space-12);\n align-items: start;\n}\n\n@container (max-width: 720px) {\n .wx-seo-rule__where {\n grid-template-columns: 1fr 1fr;\n }\n\n .wx-seo-rule__address {\n grid-column: 1 / -1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, type Component } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAction,\n WxActions,\n WxBadge,\n WxButton,\n WxCard,\n WxSelect,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport SeoUrlDialog from './SeoUrlDialog.vue'\nimport TestUrlDialog from './TestUrlDialog.vue'\nimport SeoLayout from './SeoLayout.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport { ruleTitle } from './rule'\nimport type { SeoPage, SeoUrlRule } from './types'\n\n/**\n * The rules written for addresses.\n *\n * Listed in the order the site tries them — exact, then mask, then regular expression, by\n * priority inside each group — so that reading the table top to bottom is reading what will\n * happen. Sorting it any other way would hide the one thing a person comes here to work out.\n */\nconst props = withDefaults(defineProps<{ base?: string; mediaField?: Component }>(), {\n base: '/seo',\n mediaField: undefined,\n})\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst page = ref<SeoPage<SeoUrlRule> | null>(null)\nconst loading = ref(false)\n/* A word rather than an empty string: a select reads \"\" as nothing chosen and shows a blank\n control where the option said \"any kind\". */\nconst kind = ref('any')\n\nlet last: TableState = { page: 1, perPage: 20, sort: null, search: '' }\n\nconst canManage = context.can('seo.manage')\n\nconst edit = createModal<SeoUrlRule, { rule: SeoUrlRule | null; mediaField?: Component }>(\n SeoUrlDialog,\n)\nconst test = createModal<void, Record<string, never>>(TestUrlDialog)\n\nconst kindOptions = computed(() => [\n { value: 'any', label: t('page.any-kind') },\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\nconst columns = computed<TableColumn<SeoUrlRule>[]>(() => [\n { key: 'pattern', label: t('page.address') },\n { key: 'match_type', label: t('page.kind'), hideBelow: 560 },\n { key: 'title', label: t('page.title'), hideBelow: 900 },\n { key: 'priority', label: t('page.priority'), align: 'center', hideBelow: 760 },\n { key: 'is_active', label: t('page.state'), align: 'center', hideBelow: 660 },\n {\n key: 'actions',\n label: '',\n width: 56,\n align: 'right',\n hidden: !canManage,\n hideOnCards: true,\n },\n])\n\nasync function load(state: TableState): Promise<void> {\n last = state\n loading.value = true\n\n try {\n page.value = await api.urls({\n q: state.search,\n match_type: kind.value === 'any' ? null : (kind.value as 'exact' | 'mask' | 'regex'),\n page: state.page,\n per_page: state.perPage,\n })\n } finally {\n loading.value = false\n }\n}\n\nasync function open(rule: SeoUrlRule | null): Promise<void> {\n const saved = await edit({ rule, mediaField: props.mediaField })\n\n if (saved) void load(last)\n}\n\nasync function remove(rule: SeoUrlRule): Promise<void> {\n const agreed = await confirm({\n title: t('page.delete-title', { pattern: rule.pattern }),\n message: t('page.delete-text'),\n confirmText: t('page.delete'),\n cancelText: t('page.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeUrl(rule.id)\n toast.success(t('page.deleted'))\n void load(last)\n } catch (error) {\n toast.danger((error as { body?: { message?: string } }).body?.message ?? String(error))\n }\n}\n</script>\n\n<template>\n <seo-layout :base=\"props.base\" current=\"rules\" @test=\"test({})\">\n <wx-card>\n <template #header>{{ t('page.rules') }}</template>\n\n <template v-if=\"canManage\" #extra>\n <wx-button type=\"primary\" icon=\"add\" @click=\"open(null)\">\n {{ t('page.new-rule') }}\n </wx-button>\n </template>\n\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('page.search-rules')\"\n :empty-text=\"t('page.empty')\"\n @state-change=\"load\"\n @row-click=\"canManage ? open($event) : undefined\"\n >\n <template #actions>\n <wx-select v-model=\"kind\" :options=\"kindOptions\" class=\"wx-seo-urls__kind\" />\n </template>\n\n <template #cell-pattern=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.pattern }}</wx-text>\n </template>\n\n <template #cell-match_type=\"{ row }\">\n <wx-badge>{{ t(`page.${row.match_type}`) }}</wx-badge>\n </template>\n\n <template #cell-title=\"{ row }\">\n <wx-text v-if=\"ruleTitle(row)\" size=\"sm\">{{ ruleTitle(row) }}</wx-text>\n <wx-text v-else size=\"sm\" tone=\"muted\">—</wx-text>\n </template>\n\n <template #cell-is_active=\"{ row }\">\n <wx-badge :type=\"row.is_active ? 'success' : 'default'\" dot>\n {{ row.is_active ? t('page.active') : t('page.inactive') }}\n </wx-badge>\n </template>\n\n <template #card-actions=\"{ row }\">\n <wx-actions v-if=\"canManage\" size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <!-- `.stop`: the row opens the rule, and deleting one is not opening it. -->\n <wx-actions size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n </wx-table>\n </wx-card>\n </seo-layout>\n</template>\n\n<style scoped>\n.wx-seo-urls__kind {\n width: 200px;\n}\n\n/* Once the rows are cards the filters are a column, and a fixed width leaves this one short. */\n.wx-table--cards .wx-seo-urls__kind {\n width: 100%;\n}\n</style>\n","import { markRaw, type Component } from 'vue'\nimport type { AdminModule } from '@webx-ui/module-admin'\nimport SeoCard from './SeoCard.vue'\nimport SeoRedirectsPage from './SeoRedirectsPage.vue'\nimport SeoUrlsPage from './SeoUrlsPage.vue'\n\nexport interface SeoOptions {\n /** Where the section lives inside the panel. */\n path?: string\n /**\n * The field that picks a share image — `WxMediaField` from `@webx-ui/module-media`.\n *\n * Handed in rather than imported, so this package does not depend on the library being\n * installed. Without it every other SEO field still works; only the picture is missing.\n */\n mediaField?: Component\n}\n\n/**\n * SEO as a section of the panel, and `wx-seo` as a field any screen can use.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed, and stays out of the way when\n * only one is.\n */\nexport function seo(options: SeoOptions = {}): AdminModule {\n const path = options.path ?? '/seo'\n /* A component kept in an options object arrives as a reactive proxy; Vue warns about one and\n `h()` wants the raw thing anyway. */\n const mediaField = options.mediaField ? markRaw(options.mediaField) : undefined\n\n return {\n id: 'seo',\n path,\n routes: [\n // The section's own path travels as a prop, so a panel that mounted it somewhere else\n // still links between its two screens correctly.\n {\n path,\n name: 'webx.seo',\n component: SeoUrlsPage,\n props: { base: path, mediaField },\n },\n {\n path: `${path}/redirects`,\n name: 'webx.seo.redirects',\n component: SeoRedirectsPage,\n props: { base: path },\n },\n ],\n // What a screen means by `wx-seo`: everything a page says about itself, as one value. The\n // server registers the same name for what it stores.\n types: {\n 'wx-seo': {\n component: SeoCard,\n kind: 'field',\n bind: () => ({ mediaField }),\n },\n },\n }\n}\n"],"names":["seoMessages","seeded","useSeoMessages","i18n","useAdmin","robotsDirectives","props","__props","value","_useModel","t","useTranslate","locales","useLocales","jsonLdText","ref","jsonLdBroken","field","key","computed","next","title","h1","description","keywords","ogTitle","ogDescription","canonical","image","directives","one","kept","known","merged","robotsOptions","jsonLd","stored","inLocale","map","localizedValue","counted","preview","hasPreview","keptHelp","tone","count","limit","_openBlock","_createElementBlock","_hoisted_1","_createVNode","_unref","WxTabs","WxTab","_createElementVNode","_hoisted_2","WxFormItem","WxInput","$event","_hoisted_3","WxText","_createTextVNode","_toDisplayString","WxTextarea","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_createBlock","_resolveDynamicComponent","_hoisted_11","WxCheckboxGroup","WxCodeEditor","emit","__emit","context","router","useRouter","route","useRoute","module","options","where","path","WxHeading","WxSpace","WxSegmented","WxButton","_renderSlot","_ctx","createSeoApi","admin","base","data","body","page","listQuery","query","id","input","url","locale","resolve","dismiss","open","useModal","api","saving","errors","form","editing","kindOptions","statusOptions","watch","redirect","errorOf","save","saved","toast","error","WxDialog","WxSelect","_cache","WxSwitch","running","result","fields","seo","run","_Fragment","WxAlert","WxBadge","WxDescriptions","_renderList","WxDescriptionsItem","step","WxEmpty","loading","last","canManage","edit","createModal","SeoRedirectDialog","test","TestUrlDialog","columns","load","state","remove","confirm","SeoLayout","WxCard","_createSlots","WxTable","_withCtx","row","WxActions","WxAction","SEO_FIELDS","ruleTitle","rule","seoOf","ruleInput","own","WxInputNumber","SeoCard","kind","SeoUrlDialog","mediaField","markRaw","SeoUrlsPage","SeoRedirectsPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/messages.ts","../src/i18n.ts","../src/SeoLayout.vue","../src/api.ts","../src/TestUrlDialog.vue","../src/SeoAliasesPage.vue","../src/types.ts","../src/SeoCard.vue","../src/SeoRedirectDialog.vue","../src/SeoRedirectsPage.vue","../src/rule.ts","../src/SeoUrlDialog.vue","../src/SeoUrlsPage.vue","../src/module.ts"],"sourcesContent":["import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * What this package says, in English — the same keys `webx-ui/module-seo` ships as\n * `lang/en/*.php`, so a key never reaches the screen when a translation has not arrived yet.\n *\n * The server's dictionary wins over these. There is no second dictionary inside this package:\n * a module translated into ten languages with an English dialog in the middle of it is worse\n * than one that is not translated at all.\n *\n * Placeholders are Laravel's, `:like-this` — the same line is read from a PHP file and from\n * here, and only one of the two spellings can be right.\n */\nexport const seoMessages: Record<string, Messages> = {\n module: {\n title: 'SEO',\n },\n\n page: {\n rules: 'Rules',\n redirects: 'Redirects',\n test: 'Check an address',\n\n 'new-rule': 'New rule',\n 'new-redirect': 'New redirect',\n 'search-rules': 'Search by address',\n 'search-redirects': 'Search by address or destination',\n empty: 'Nothing here yet.',\n\n address: 'Address',\n kind: 'Kind',\n priority: 'Priority',\n state: 'State',\n active: 'Active',\n inactive: 'Off',\n title: 'Title',\n target: 'Destination',\n status: 'Code',\n hits: 'Hits',\n 'last-hit': 'Last used',\n never: 'never',\n loop: 'Points at itself',\n\n 'any-kind': 'Any kind',\n exact: 'Exact',\n mask: 'Mask',\n regex: 'Regular expression',\n\n rule: 'Rule',\n redirect: 'Redirect',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n saved: 'Saved.',\n deleted: 'Deleted.',\n failed: 'Some values were not accepted. Check the highlighted fields.',\n 'delete-title': 'Delete :pattern?',\n 'delete-text': 'This cannot be undone.',\n\n 'address-help':\n 'A path with its query string. In a mask, * is one segment and ** is any number of them; a regular expression is stored as written, delimiters and all.',\n 'target-help':\n 'A path on this site, or a full address elsewhere. $1 puts back what a mask caught.',\n 'priority-help': 'Among rules of the same kind, the higher number wins.',\n\n 'test-placeholder': '/catalog/shoes?page=2',\n 'test-run': 'Check',\n 'test-empty': 'Type an address to see what the site will say about it.',\n 'test-matched': 'Matched rule',\n 'test-none': 'No rule matches this address.',\n 'test-redirected': 'This address is redirected to :target with a :status.',\n 'test-result': 'What the page will say',\n 'test-chain': 'Where each part came from',\n\n automatic: 'Automatic',\n 'search-aliases': 'Search by address or destination',\n 'aliases-empty': 'Nothing has moved yet.',\n 'aliases-help':\n 'The site writes these itself: renaming or moving a page leaves its old address behind answering 301, so nothing that linked to it dies. They cannot be edited here — a redirect of your own is tried first and wins.',\n language: 'Language',\n 'moved-at': 'Moved',\n gone: 'Leads nowhere',\n occupied:\n 'A page of the site answers at :path. A redirect is tried before it, so the page stops being reachable there.',\n 'occupied-alias':\n 'This address already leads to :target — what a move left behind. A redirect written here is tried first.',\n },\n\n card: {\n 'section-page': 'The page',\n 'section-share': 'Sharing',\n 'section-advanced': 'Advanced',\n\n title: 'Title',\n h1: 'Heading',\n description: 'Description',\n keywords: 'Keywords',\n canonical: 'Canonical address',\n 'canonical-help': 'The address this page should be indexed under, when it is not its own.',\n\n robots: 'Indexing',\n 'robots-kept': 'Also kept, as written: :directives',\n noindex: 'Keep out of the index',\n nofollow: 'Do not follow the links',\n noarchive: 'No cached copy',\n nosnippet: 'No snippet',\n noimageindex: 'Do not index the images',\n\n 'og-title': 'Share title',\n 'og-description': 'Share description',\n 'og-image': 'Share image',\n 'og-help': 'Left empty, a share preview uses the title and description above.',\n\n 'json-ld': 'Structured data',\n 'json-ld-help':\n 'A JSON-LD object, or a list of them. What the site says about itself comes from the settings; this is for a page that needs markup of its own.',\n 'json-ld-invalid': 'This is not valid JSON.',\n\n preview: 'In search results',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { seoMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useSeoMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-seo', seoMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport { WxButton, WxHeading, WxSegmented, WxSpace } from '@webx-ui/core'\nimport { useSeoMessages } from './i18n'\n\n/**\n * The head of the section: its name, the switch between what it holds, and the one tool that\n * belongs to the whole of it.\n *\n * Rules, redirects and the trail of renames are three screens rather than three tabs of one,\n * because they are three tables with their own paging and their own search — and a tab that\n * quietly resets both when you come back to it is worse than a second address.\n */\nconst props = defineProps<{ base: string; current: 'rules' | 'redirects' | 'aliases' }>()\n\nconst emit = defineEmits<{ test: [] }>()\n\nconst context = useAdmin()\nconst router = useRouter()\nconst route = useRoute()\n\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\n/** The section's name as the server translated it; the built-in English until it arrives. */\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'seo')?.title ??\n t('module.title'),\n)\n\nconst options = computed(() => [\n { value: 'rules', label: t('page.rules') },\n { value: 'redirects', label: t('page.redirects') },\n { value: 'aliases', label: t('page.automatic') },\n])\n\nconst where = computed({\n get: () => props.current,\n set: (next: string | number) => {\n const path = next === 'rules' ? props.base : `${props.base}/${String(next)}`\n\n if (path !== route.path) void router.push(path)\n },\n})\n</script>\n\n<template>\n <div class=\"wx-seo-layout\">\n <div class=\"wx-seo-layout__head\">\n <wx-heading :level=\"2\">{{ title }}</wx-heading>\n\n <wx-space size=\"sm\">\n <wx-segmented v-model=\"where\" :options=\"options\" size=\"sm\" />\n <wx-button variant=\"outline\" icon=\"search\" @click=\"emit('test')\">\n {{ t('page.test') }}\n </wx-button>\n </wx-space>\n </div>\n\n <slot />\n </div>\n</template>\n\n<style scoped>\n.wx-seo-layout {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n container-type: inline-size;\n}\n\n.wx-seo-layout__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--wx-space-12);\n flex-wrap: wrap;\n}\n\n/* On a phone the heading takes the first line and the controls the second, full width, rather\n than three things elbowing each other on one. */\n@container (max-width: 560px) {\n .wx-seo-layout__head > * {\n flex: 1 1 100%;\n }\n}\n</style>\n","import type { AdminContext } from '@webx-ui/module-admin'\nimport type {\n SeoAlias,\n SeoAliasQuery,\n SeoPage,\n SeoRedirect,\n SeoRedirectInput,\n SeoRedirectQuery,\n SeoTestResult,\n SeoUrlInput,\n SeoUrlQuery,\n SeoUrlRule,\n} from './types'\n\nexport interface SeoApi {\n /** The rules, in the order the site tries them. */\n urls(query?: SeoUrlQuery): Promise<SeoPage<SeoUrlRule>>\n url(id: number): Promise<SeoUrlRule>\n createUrl(input: SeoUrlInput): Promise<SeoUrlRule>\n updateUrl(id: number, input: SeoUrlInput): Promise<SeoUrlRule>\n removeUrl(id: number): Promise<void>\n\n redirects(query?: SeoRedirectQuery): Promise<SeoPage<SeoRedirect>>\n createRedirect(input: SeoRedirectInput): Promise<SeoRedirect>\n updateRedirect(id: number, input: SeoRedirectInput): Promise<SeoRedirect>\n removeRedirect(id: number): Promise<void>\n\n /** The redirects nobody wrote: what the address registry kept after a rename. Read only. */\n aliases(query?: SeoAliasQuery): Promise<SeoPage<SeoAlias>>\n\n /** What an address ends up saying, and where every part of it came from. */\n test(url: string, locale?: string | null): Promise<SeoTestResult>\n}\n\n/** Everything under `/seo`, below the panel's API path. */\nexport function createSeoApi(admin: AdminContext): SeoApi {\n const base = `${admin.apiPath}/seo`\n const data = <T>(body: { data: T }): T => body.data\n\n /* A paginated list arrives as a resource collection — rows under `data`, numbers under\n `meta`. The table wants them on one object, so they are joined here once rather than in\n every screen. */\n const page = <T>(body: { data: T[]; meta: Omit<SeoPage<T>, 'data'> }): SeoPage<T> => ({\n ...body.meta,\n data: body.data,\n })\n\n /* `undefined` drops the parameter; `null` and `''` would be sent as words and read as a\n filter nobody asked for. */\n const listQuery = (\n query: SeoRedirectQuery,\n ): Record<string, string | number | boolean | null | undefined> => ({\n q: query.q || undefined,\n match_type: query.match_type ?? undefined,\n is_active: query.is_active ?? undefined,\n sort: query.sort || undefined,\n page: query.page,\n per_page: query.per_page,\n })\n\n return {\n urls: (query = {}) =>\n admin.http\n .get<{ data: SeoUrlRule[]; meta: Omit<SeoPage<SeoUrlRule>, 'data'> }>(`${base}/urls`, {\n query: listQuery(query),\n })\n .then(page),\n\n url: (id) => admin.http.get<{ data: SeoUrlRule }>(`${base}/urls/${id}`).then(data),\n\n createUrl: (input) => admin.http.post<{ data: SeoUrlRule }>(`${base}/urls`, input).then(data),\n\n updateUrl: (id, input) =>\n admin.http.put<{ data: SeoUrlRule }>(`${base}/urls/${id}`, input).then(data),\n\n removeUrl: (id) => admin.http.delete<void>(`${base}/urls/${id}`),\n\n redirects: (query = {}) =>\n admin.http\n .get<{ data: SeoRedirect[]; meta: Omit<SeoPage<SeoRedirect>, 'data'> }>(\n `${base}/redirects`,\n { query: listQuery(query) },\n )\n .then(page),\n\n createRedirect: (input) =>\n admin.http.post<{ data: SeoRedirect }>(`${base}/redirects`, input).then(data),\n\n updateRedirect: (id, input) =>\n admin.http.put<{ data: SeoRedirect }>(`${base}/redirects/${id}`, input).then(data),\n\n removeRedirect: (id) => admin.http.delete<void>(`${base}/redirects/${id}`),\n\n aliases: (query = {}) =>\n admin.http\n .get<{ data: SeoAlias[]; meta: Omit<SeoPage<SeoAlias>, 'data'> }>(`${base}/aliases`, {\n query: {\n q: query.q || undefined,\n locale: query.locale ?? undefined,\n page: query.page,\n per_page: query.per_page,\n },\n })\n .then(page),\n\n // A POST because it carries an address in its body, and an address in a query string is an\n // address somebody has to escape twice.\n test: (url, locale = null) =>\n admin.http\n .post<{ data: SeoTestResult }>(`${base}/test-url`, { url, locale: locale ?? undefined })\n .then(data),\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n WxAlert,\n WxBadge,\n WxButton,\n WxDescriptions,\n WxDescriptionsItem,\n WxDialog,\n WxEmpty,\n WxInput,\n WxSpace,\n WxText,\n useModal,\n} from '@webx-ui/core'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { SeoTestResult } from './types'\n\n/**\n * \"Why does this page have the wrong title?\"\n *\n * The most common question this section gets, and the reason it should take one call to answer\n * rather than a reading of the code: which redirect catches the address, which rule matches it,\n * what every source contributed, and what the page ends up saying.\n */\nconst { open } = useModal<void>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst url = ref('')\nconst running = ref(false)\nconst result = ref<SeoTestResult | null>(null)\n\nconst fields = computed(() => {\n const seo = result.value?.seo\n\n if (!seo) return []\n\n return [\n { label: t('card.title'), value: seo.title },\n { label: t('card.h1'), value: seo.h1 },\n { label: t('card.description'), value: seo.description },\n { label: t('card.keywords'), value: seo.keywords },\n { label: t('card.canonical'), value: seo.canonical },\n { label: t('card.robots'), value: seo.robots },\n ].filter((one) => one.value)\n})\n\nasync function run(): Promise<void> {\n if (url.value.trim() === '') return\n\n running.value = true\n\n try {\n result.value = await api.test(url.value)\n } finally {\n running.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.test')\" :width=\"720\">\n <div class=\"wx-seo-test\">\n <wx-space size=\"sm\" class=\"wx-seo-test__ask\">\n <wx-input\n v-model=\"url\"\n :placeholder=\"t('page.test-placeholder')\"\n class=\"wx-seo-test__input\"\n @keyup.enter=\"run\"\n />\n <wx-button type=\"primary\" :loading=\"running\" @click=\"run\">\n {{ t('page.test-run') }}\n </wx-button>\n </wx-space>\n\n <template v-if=\"result\">\n <!-- First, because it happens first: a redirected address never reaches the rules. -->\n <wx-alert\n v-if=\"result.redirect\"\n type=\"warning\"\n :description=\"\n t('page.test-redirected', {\n target: result.redirect.target,\n status: result.redirect.status,\n })\n \"\n />\n\n <!-- Who answers at this address at all. A rule of the site's own comes before any of\n this, so an address can be redirected away from a page that still exists. -->\n <wx-alert\n v-if=\"result.route\"\n type=\"info\"\n :description=\"\n result.route.kind === 'alias'\n ? t('page.occupied-alias', { target: result.route.target ?? result.route.path })\n : t('page.occupied', { path: result.route.path })\n \"\n />\n\n <wx-alert v-if=\"!result.matched\" type=\"info\" :description=\"t('page.test-none')\" />\n <div v-else class=\"wx-seo-test__matched\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-matched') }}</wx-text>\n <wx-space size=\"sm\" align=\"center\">\n <wx-badge>{{ t(`page.${result.matched.match_type}`) }}</wx-badge>\n <wx-text mono size=\"sm\">{{ result.matched.pattern }}</wx-text>\n </wx-space>\n </div>\n\n <div v-if=\"fields.length > 0\" class=\"wx-seo-test__result\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-result') }}</wx-text>\n <wx-descriptions :columns=\"1\" bordered size=\"sm\">\n <wx-descriptions-item v-for=\"one in fields\" :key=\"one.label\" :label=\"one.label\">\n {{ one.value }}\n </wx-descriptions-item>\n </wx-descriptions>\n </div>\n\n <div v-if=\"result.chain.length > 0\" class=\"wx-seo-test__chain\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('page.test-chain') }}</wx-text>\n <wx-space size=\"xs\" wrap>\n <wx-badge v-for=\"step in result.chain\" :key=\"step.source\">\n {{ step.source }} · {{ step.priority }}\n </wx-badge>\n </wx-space>\n </div>\n </template>\n\n <wx-empty v-else-if=\"!running\" icon=\"search\" :description=\"t('page.test-empty')\" />\n </div>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-test {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n}\n\n.wx-seo-test__ask {\n width: 100%;\n}\n\n.wx-seo-test__input {\n flex: 1 1 auto;\n}\n\n.wx-seo-test__matched,\n.wx-seo-test__result,\n.wx-seo-test__chain {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-6);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useTranslate, useAdmin } from '@webx-ui/module-admin'\nimport {\n createModal,\n WxAlert,\n WxBadge,\n WxCard,\n WxLink,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport SeoLayout from './SeoLayout.vue'\nimport TestUrlDialog from './TestUrlDialog.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { SeoAlias, SeoPage } from './types'\n\n/**\n * The redirects nobody wrote.\n *\n * Renaming a page or moving a branch leaves its old address behind, answering 301 — which is\n * the only reason an external link, a bookmark or a search result survives an edit in the panel.\n * They are shown here because a reader who lands on a dead address does not care which half of\n * the system made the redirect, and an editor chasing one should not have to.\n *\n * Nothing on this screen writes. These rows belong to the entity that moved: it makes them, and\n * deleting it takes them with it. An editor who wants a different answer for an old address\n * writes a rule of their own on the next tab — those are tried first, and this one stops\n * mattering.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/seo' })\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst page = ref<SeoPage<SeoAlias> | null>(null)\nconst loading = ref(false)\n\nconst test = createModal<void, Record<string, never>>(TestUrlDialog)\n\n/** More than one language on the site is the only reason to say which one a row is in. */\nconst multilingual = computed(() => (context.state.manifest?.locales?.length ?? 1) > 1)\n\nconst columns = computed<TableColumn<SeoAlias>[]>(() => [\n { key: 'pattern', label: t('page.address'), sortable: false },\n { key: 'target', label: t('page.target') },\n {\n key: 'locale',\n label: t('page.language'),\n align: 'center',\n hidden: !multilingual.value,\n hideBelow: 760,\n },\n { key: 'created_at', label: t('page.moved-at'), hideBelow: 900, hideOnCards: true },\n])\n\nasync function load(state: TableState): Promise<void> {\n loading.value = true\n\n try {\n page.value = await api.aliases({\n q: state.search,\n page: state.page,\n per_page: state.perPage,\n })\n } finally {\n loading.value = false\n }\n}\n</script>\n\n<template>\n <seo-layout :base=\"props.base\" current=\"aliases\" @test=\"test({})\">\n <wx-card>\n <template #header>{{ t('page.automatic') }}</template>\n\n <wx-alert\n type=\"info\"\n variant=\"soft\"\n :description=\"t('page.aliases-help')\"\n class=\"wx-seo-aliases__note\"\n />\n\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('page.search-aliases')\"\n :empty-text=\"t('page.aliases-empty')\"\n @state-change=\"load\"\n >\n <template #cell-pattern=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.pattern }}</wx-text>\n </template>\n\n <!-- The address it leads to now, as a link: this is the one place in the panel where an\n editor can walk from a dead address to the live page without knowing the section it\n lives in. -->\n <template #cell-target=\"{ row }\">\n <wx-link v-if=\"row.target && row.target_url\" :href=\"row.target_url\" external size=\"sm\">\n <wx-text mono size=\"sm\">{{ row.target }}</wx-text>\n </wx-link>\n <wx-badge v-else type=\"warning\">{{ t('page.gone') }}</wx-badge>\n </template>\n\n <template #cell-locale=\"{ row }\">\n <wx-badge>{{ row.locale }}</wx-badge>\n </template>\n\n <template #cell-created_at=\"{ row }\">\n <wx-text size=\"sm\" tone=\"muted\">\n {{ row.created_at ? new Date(row.created_at).toLocaleString() : '' }}\n </wx-text>\n </template>\n </wx-table>\n </wx-card>\n </seo-layout>\n</template>\n\n<style scoped>\n.wx-seo-aliases__note {\n margin-bottom: var(--wx-space-12);\n}\n</style>\n","import type { LocalizedValue } from '@webx-ui/core'\n\n/** The shapes `webx-ui/module-seo` answers with, and what its forms send back. */\n\n/** How a pattern is compared with an address. */\nexport type MatchType = 'exact' | 'mask' | 'regex'\n\n/** What `wx-media` stores, plus the address the server works out on every read. */\nexport interface SeoImage {\n path: string\n alt?: string\n title?: string\n url?: string | null\n}\n\n/**\n * What a page says about itself.\n *\n * The text fields are language maps — the same `{ en: '…', ru: '…' }` every localized field in\n * the panel uses. The picture is not: there are no per-language images anywhere yet, and a\n * column that already holds an object would read `path` as a language code.\n */\nexport interface SeoFields {\n title: LocalizedValue\n h1: LocalizedValue\n description: LocalizedValue\n keywords: LocalizedValue\n og_title: LocalizedValue\n og_description: LocalizedValue\n og_image: SeoImage | null\n canonical: string | null\n robots: string | null\n /** A JSON-LD object, a list of them, or null. */\n json_ld: unknown\n}\n\n/**\n * The value of a `wx-seo` field: the same fields, none of them required.\n *\n * A record that has never been given any SEO stores nothing rather than a shape full of nulls.\n */\nexport type SeoValue = Partial<SeoFields>\n\nexport interface SeoUrlRule extends SeoFields {\n id: number\n match_type: MatchType\n pattern: string\n priority: number\n is_active: boolean\n created_at: string | null\n updated_at: string | null\n /**\n * What a table row is, as far as `WxTable` is concerned. Without it this type does not\n * extend `TableRow` and every cell slot hands back `unknown`.\n */\n [key: string]: unknown\n}\n\n/** A `PUT` replaces the whole rule: the form edits every field at once. */\nexport interface SeoUrlInput extends SeoValue {\n match_type: MatchType\n pattern: string\n priority?: number\n is_active?: boolean\n}\n\nexport interface SeoRedirect {\n id: number\n match_type: MatchType\n pattern: string\n target: string\n status: number\n is_active: boolean\n hits: number\n last_hit_at: string | null\n /** The server says so rather than refusing the row: a loop is skipped, not rejected. */\n is_loop: boolean\n created_at: string | null\n updated_at: string | null\n [key: string]: unknown\n}\n\n/**\n * An address a rename left behind, from `webx-ui/routing`.\n *\n * The same words a manual redirect uses where the two overlap, so one screen can show both\n * without learning two vocabularies. What it has instead of the rest is an entity: these are\n * written by whatever moved, always exact, always 301, and never edited here.\n */\nexport interface SeoAlias {\n id: number\n locale: string\n /** The old address. */\n pattern: string\n /** The address it leads to now; null when the row it pointed at is gone. */\n target: string | null\n url: string\n target_url: string | null\n /** `page`, `article`, `product` — what the content module called itself. */\n entity_type: string\n entity_id: number\n created_at: string | null\n [key: string]: unknown\n}\n\nexport interface SeoAliasQuery {\n q?: string\n locale?: string | null\n page?: number\n per_page?: number\n}\n\n/**\n * What the address registry holds at an address, when something does.\n *\n * A rule written by hand is tried before any of it — that is deliberate — so this is a warning\n * and never a refusal: the page at this address is about to stop being reachable.\n */\nexport interface SeoRoute {\n path: string\n url: string\n kind: 'canonical' | 'alias'\n /** Where an old address leads now; null on a live page, which has nowhere to lead. */\n target: string | null\n entity_type: string\n entity_id: number\n /** False when a shorter address matched: the page at `/parts` answering for `/parts/bobcat`. */\n exact: boolean\n}\n\nexport interface SeoRedirectInput {\n match_type: MatchType\n pattern: string\n target: string\n status?: number\n is_active?: boolean\n}\n\n/**\n * A page, flat — what `->paginate()` serialises and `WxTable` reads as it arrives. A resource\n * collection nests the same numbers under `meta`; the client flattens them once, here.\n */\nexport interface SeoPage<T> {\n data: T[]\n current_page: number\n last_page: number\n per_page: number\n total: number\n from: number | null\n to: number | null\n}\n\nexport interface SeoUrlQuery {\n q?: string\n match_type?: MatchType | null\n is_active?: boolean | null\n page?: number\n per_page?: number\n}\n\nexport interface SeoRedirectQuery extends SeoUrlQuery {\n sort?: string\n}\n\n/** What a page ends up saying, every source merged. */\nexport interface SeoResolved {\n title: string | null\n h1: string | null\n description: string | null\n keywords: string | null\n canonical: string | null\n robots: string | null\n og: Record<string, string>\n json_ld: Record<string, unknown>[]\n}\n\n/** One source's contribution, before the merge. */\nexport interface SeoChainStep {\n source: string\n priority: number\n data: SeoResolved\n}\n\n/** The answer to \"why does this page say that\". */\nexport interface SeoTestResult {\n url: string\n /** Said first because it happens first: a redirected address never reaches the rules. */\n redirect: SeoRedirect | null\n /** What the address registry has here — a live page, or the trail of one that moved. */\n route: SeoRoute | null\n matched: SeoUrlRule | null\n chain: SeoChainStep[]\n seo: SeoResolved\n}\n\n/** The meta directives the card offers as checkboxes. Anything else is kept as written. */\nexport const robotsDirectives = [\n 'noindex',\n 'nofollow',\n 'noarchive',\n 'nosnippet',\n 'noimageindex',\n] as const\n\nexport type RobotsDirective = (typeof robotsDirectives)[number]\n","<script setup lang=\"ts\">\nimport { computed, ref, type Component } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport {\n localizedValue,\n useLocales,\n WxCheckboxGroup,\n WxCodeEditor,\n WxFormItem,\n WxInput,\n WxTabs,\n WxTab,\n WxText,\n WxTextarea,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport { useSeoMessages } from './i18n'\nimport { robotsDirectives, type SeoImage, type SeoValue } from './types'\n\n/**\n * What a page says about itself, as a field.\n *\n * Registered as `wx-seo`, so it can be dropped into any screen by a patch, and used directly by\n * the rule form in this package. The value is one object — the same shape the server stores —\n * rather than a dozen loose keys, because SEO is edited and reasoned about as a unit.\n *\n * The text fields are language maps and say so: the same chip every other localized field in the\n * panel grows, moving together with them.\n */\ndefineOptions({ name: 'WxSeo' })\n\nconst props = withDefaults(\n defineProps<{\n /**\n * The field that picks the share image — `WxMediaField` in a panel that has the library.\n *\n * Handed in rather than imported: this package does not depend on the media one, and a panel\n * without a library still edits everything else about a page.\n */\n mediaField?: Component\n disabled?: boolean\n /**\n * Soft limits, for the counters only. Long is not wrong — search engines shorten what they\n * shorten — so nothing here refuses a longer line, it only says how long it is.\n */\n titleLimit?: number\n descriptionLimit?: number\n /** What the snippet preview shows above the title. */\n site?: string\n /** Hidden when a screen has no use for them. */\n structuredData?: boolean\n }>(),\n {\n mediaField: undefined,\n disabled: false,\n titleLimit: 60,\n descriptionLimit: 160,\n site: undefined,\n structuredData: true,\n },\n)\n\nconst value = defineModel<SeoValue | null>({ default: null })\n\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\nconst locales = useLocales()\n\nconst jsonLdText = ref<string | null>(null)\nconst jsonLdBroken = ref(false)\n\n/** One field of the value, without rebuilding the object in every handler. */\nfunction field<K extends keyof SeoValue>(key: K) {\n return computed({\n get: () => value.value?.[key],\n set: (next: SeoValue[K]) => {\n value.value = { ...(value.value ?? {}), [key]: next }\n },\n })\n}\n\nconst title = field('title')\nconst h1 = field('h1')\nconst description = field('description')\nconst keywords = field('keywords')\nconst ogTitle = field('og_title')\nconst ogDescription = field('og_description')\n\nconst canonical = computed<string>({\n get: () => value.value?.canonical ?? '',\n set: (next: string) => {\n value.value = { ...(value.value ?? {}), canonical: next.trim() === '' ? null : next }\n },\n})\n\nconst image = computed({\n get: () => value.value?.og_image ?? null,\n set: (next: SeoImage | null) => {\n value.value = { ...(value.value ?? {}), og_image: next }\n },\n})\n\n/* The directives the checkboxes cover. Anything else somebody wrote — `max-snippet:20` and its\n kind — is kept and shown, because silently dropping a line a person typed is worse than not\n offering a box for it. */\nconst directives = computed(() => (value.value?.robots ?? '').split(',').map((one) => one.trim()))\n\nconst kept = computed(() =>\n directives.value.filter((one) => one !== '' && !robotsDirectives.includes(one as never)),\n)\n\nconst known = computed({\n get: () => directives.value.filter((one) => robotsDirectives.includes(one as never)),\n set: (next: readonly string[]) => {\n const merged = [...next, ...kept.value].join(', ')\n value.value = { ...(value.value ?? {}), robots: merged === '' ? null : merged }\n },\n})\n\nconst robotsOptions = computed(() =>\n robotsDirectives.map((one) => ({ value: one, label: t(`card.${one}`) })),\n)\n\nconst jsonLd = computed<string>({\n get: () => {\n if (jsonLdText.value !== null) return jsonLdText.value\n\n const stored = value.value?.json_ld\n\n return stored === null || stored === undefined ? '' : JSON.stringify(stored, null, 2)\n },\n set: (next: string) => {\n /* The text is kept as typed while it is being typed: reformatting somebody's JSON the\n moment it happens to parse moves the caret out from under them. */\n jsonLdText.value = next\n\n if (next.trim() === '') {\n jsonLdBroken.value = false\n value.value = { ...(value.value ?? {}), json_ld: null }\n\n return\n }\n\n try {\n value.value = { ...(value.value ?? {}), json_ld: JSON.parse(next) }\n jsonLdBroken.value = false\n } catch {\n jsonLdBroken.value = true\n }\n },\n})\n\n/** What the counters and the preview read: the language being edited. */\nfunction inLocale(map: LocalizedValue | undefined): string {\n return localizedValue(map, locales.active.value)\n}\n\nconst counted = computed(() => ({\n title: inLocale(title.value as LocalizedValue | undefined).length,\n description: inLocale(description.value as LocalizedValue | undefined).length,\n}))\n\nconst preview = computed(() => ({\n title: inLocale(title.value as LocalizedValue | undefined),\n description: inLocale(description.value as LocalizedValue | undefined),\n address: canonical.value || (props.site ?? ''),\n}))\n\nconst hasPreview = computed(() => preview.value.title !== '' || preview.value.description !== '')\n\nconst keptHelp = computed(() =>\n kept.value.length === 0\n ? undefined\n : t('card.robots-kept', { directives: kept.value.join(', ') }),\n)\n\nfunction tone(count: number, limit: number): 'muted' | 'warning' {\n return count > limit ? 'warning' : 'muted'\n}\n</script>\n\n<template>\n <div class=\"wx-seo\">\n <wx-tabs keep-alive>\n <wx-tab :label=\"t('card.section-page')\" value=\"page\">\n <div class=\"wx-seo__fields\">\n <wx-form-item :label=\"t('card.title')\" :disabled=\"disabled\">\n <wx-input v-model=\"title\" localized />\n <div class=\"wx-seo__count\">\n <wx-text size=\"sm\" :tone=\"tone(counted.title, titleLimit)\">\n {{ counted.title }} / {{ titleLimit }}\n </wx-text>\n </div>\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.h1')\" :disabled=\"disabled\">\n <wx-input v-model=\"h1\" localized />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.description')\" :disabled=\"disabled\">\n <wx-textarea v-model=\"description\" localized :rows=\"3\" />\n <div class=\"wx-seo__count\">\n <wx-text size=\"sm\" :tone=\"tone(counted.description, descriptionLimit)\">\n {{ counted.description }} / {{ descriptionLimit }}\n </wx-text>\n </div>\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.keywords')\" :disabled=\"disabled\">\n <wx-input v-model=\"keywords\" localized />\n </wx-form-item>\n\n <div v-if=\"hasPreview\" class=\"wx-seo__preview\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('card.preview') }}</wx-text>\n <div class=\"wx-seo__snippet\">\n <div v-if=\"preview.address\" class=\"wx-seo__snippet-address\">\n {{ preview.address }}\n </div>\n <div class=\"wx-seo__snippet-title\">{{ preview.title }}</div>\n <div class=\"wx-seo__snippet-text\">{{ preview.description }}</div>\n </div>\n </div>\n </div>\n </wx-tab>\n\n <wx-tab :label=\"t('card.section-share')\" value=\"share\">\n <div class=\"wx-seo__fields\">\n <component\n :is=\"mediaField\"\n v-if=\"mediaField\"\n v-model=\"image\"\n :label=\"t('card.og-image')\"\n aspect=\"16/9\"\n accept=\"image\"\n :disabled=\"disabled\"\n />\n\n <wx-form-item :label=\"t('card.og-title')\" :help=\"t('card.og-help')\" :disabled=\"disabled\">\n <wx-input v-model=\"ogTitle\" localized />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.og-description')\" :disabled=\"disabled\">\n <wx-textarea v-model=\"ogDescription\" localized :rows=\"2\" />\n </wx-form-item>\n </div>\n </wx-tab>\n\n <wx-tab :label=\"t('card.section-advanced')\" value=\"advanced\">\n <div class=\"wx-seo__fields\">\n <wx-form-item\n :label=\"t('card.canonical')\"\n :help=\"t('card.canonical-help')\"\n :disabled=\"disabled\"\n >\n <wx-input v-model=\"canonical\" placeholder=\"https://example.com/page\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('card.robots')\" :help=\"keptHelp\" :disabled=\"disabled\">\n <wx-checkbox-group v-model=\"known\" :options=\"robotsOptions\" />\n </wx-form-item>\n\n <wx-form-item\n v-if=\"structuredData\"\n :label=\"t('card.json-ld')\"\n :help=\"t('card.json-ld-help')\"\n :error=\"jsonLdBroken ? t('card.json-ld-invalid') : undefined\"\n :disabled=\"disabled\"\n >\n <wx-code-editor\n v-model=\"jsonLd\"\n language=\"json\"\n lint\n min-height=\"120px\"\n max-height=\"420px\"\n />\n </wx-form-item>\n </div>\n </wx-tab>\n </wx-tabs>\n </div>\n</template>\n\n<style scoped>\n.wx-seo {\n container-type: inline-size;\n}\n\n.wx-seo__fields {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n padding-block-start: var(--wx-space-12);\n}\n\n.wx-seo__count {\n display: flex;\n justify-content: flex-end;\n}\n\n.wx-seo__preview {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-4);\n}\n\n/* Not a copy of any one search engine's styling — a reminder of how much of a line survives. */\n.wx-seo__snippet {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-2);\n padding: var(--wx-space-12);\n border: 1px solid var(--wx-color-border);\n border-radius: var(--wx-radius-md);\n background: var(--wx-color-surface-sunken);\n}\n\n.wx-seo__snippet-address {\n color: var(--wx-color-text-muted);\n font-size: var(--wx-font-size-sm);\n overflow-wrap: anywhere;\n}\n\n.wx-seo__snippet-title {\n color: var(--wx-color-primary);\n font-size: var(--wx-font-size-lg);\n overflow-wrap: anywhere;\n}\n\n.wx-seo__snippet-text {\n color: var(--wx-color-text);\n font-size: var(--wx-font-size-sm);\n overflow-wrap: anywhere;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n toast,\n useModal,\n WxAlert,\n WxButton,\n WxDialog,\n WxFormItem,\n WxInput,\n WxSelect,\n WxSpace,\n WxSwitch,\n} from '@webx-ui/core'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { MatchType, SeoRedirect, SeoRoute } from './types'\n\n/** One address that has moved, and where to. Four fields, so four fields. */\nconst props = withDefaults(defineProps<{ redirect?: SeoRedirect | null }>(), { redirect: null })\n\nconst { resolve, dismiss, open } = useModal<SeoRedirect>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst occupant = ref<SeoRoute | null>(null)\n\nconst form = ref({\n match_type: 'exact' as MatchType,\n pattern: '',\n target: '',\n status: 301,\n is_active: true,\n})\n\nconst editing = computed(() => props.redirect !== null)\n\nconst kindOptions = computed(() => [\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\n/* Numbers, not words: 301 and 302 are what a person types into a search when they want to know\n what one is, and a label would only stand between them and that. */\nconst statusOptions = [\n { value: 301, label: '301' },\n { value: 302, label: '302' },\n]\n\nwatch(\n () => props.redirect,\n (redirect) => {\n form.value = {\n match_type: redirect?.match_type ?? 'exact',\n pattern: redirect?.pattern ?? '',\n target: redirect?.target ?? '',\n status: redirect?.status ?? 301,\n is_active: redirect?.is_active ?? true,\n }\n errors.value = {}\n occupant.value = null\n void shadowed()\n },\n { immediate: true },\n)\n\n/**\n * Whether a live page is standing at the address this rule is about to take over.\n *\n * A rule wins: `RedirectRequests` runs before routing, deliberately, so that an editor can\n * redirect an address the site still answers at. Which is exactly why this has to be said out\n * loud — the page does not disappear from the panel, it only stops being reachable, and the\n * person who finds out otherwise is a reader.\n *\n * Only for an exact address. A mask covers addresses that do not exist yet and a regular\n * expression covers ones nobody can enumerate; guessing at either would produce a warning that\n * is wrong often enough to be ignored, and a warning that is ignored is worse than none.\n */\nasync function shadowed(): Promise<void> {\n const pattern = form.value.pattern.trim()\n\n if (form.value.match_type !== 'exact' || pattern === '') {\n occupant.value = null\n\n return\n }\n\n try {\n occupant.value = (await api.test(pattern)).route\n } catch {\n // The check is a courtesy; a rule is not held up because it could not be made.\n occupant.value = null\n }\n}\n\nfunction errorOf(field: string): string | undefined {\n return errors.value[field]?.[0]\n}\n\nasync function save(): Promise<void> {\n saving.value = true\n errors.value = {}\n\n try {\n const saved = props.redirect\n ? await api.updateRedirect(props.redirect.id, form.value)\n : await api.createRedirect(form.value)\n\n toast.success(t('page.saved'))\n resolve(saved)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]>; message?: string } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('page.failed'))\n } else {\n toast.danger(body?.message ?? t('page.failed'))\n }\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.redirect')\" :width=\"640\">\n <div class=\"wx-seo-redirect\">\n <wx-form-item :label=\"t('page.kind')\">\n <wx-select v-model=\"form.match_type\" :options=\"kindOptions\" @change=\"shadowed\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('page.address')\"\n :help=\"t('page.address-help')\"\n :error=\"errorOf('pattern')\"\n >\n <wx-input v-model=\"form.pattern\" placeholder=\"/old-address\" @blur=\"shadowed\" />\n </wx-form-item>\n\n <!-- Said, not refused: the rule is allowed to shadow the page, and whoever writes it\n should know that is what they are doing. -->\n <wx-alert\n v-if=\"occupant\"\n type=\"warning\"\n variant=\"soft\"\n :description=\"\n occupant.kind === 'alias'\n ? t('page.occupied-alias', { target: occupant.target ?? occupant.path })\n : t('page.occupied', { path: occupant.path })\n \"\n />\n\n <wx-form-item\n :label=\"t('page.target')\"\n :help=\"t('page.target-help')\"\n :error=\"errorOf('target')\"\n >\n <wx-input v-model=\"form.target\" placeholder=\"/new-address\" />\n </wx-form-item>\n\n <div class=\"wx-seo-redirect__row\">\n <wx-form-item :label=\"t('page.status')\">\n <wx-select v-model=\"form.status\" :options=\"statusOptions\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.state')\">\n <wx-switch v-model=\"form.is_active\" />\n </wx-form-item>\n </div>\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('page.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" @click=\"save\">\n {{ editing ? t('page.save') : t('page.new-redirect') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-redirect {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n}\n\n.wx-seo-redirect__row {\n display: grid;\n grid-template-columns: 140px 1fr;\n gap: var(--wx-space-12);\n align-items: start;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAction,\n WxActions,\n WxBadge,\n WxButton,\n WxCard,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport SeoLayout from './SeoLayout.vue'\nimport SeoRedirectDialog from './SeoRedirectDialog.vue'\nimport TestUrlDialog from './TestUrlDialog.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport type { SeoPage, SeoRedirect } from './types'\n\n/**\n * The addresses that have moved.\n *\n * Busiest first by default, which is the order this list is worked on: a redirect nobody has\n * followed since the site was rebuilt is the one worth deleting, and it sits at the bottom.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/seo' })\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst page = ref<SeoPage<SeoRedirect> | null>(null)\nconst loading = ref(false)\n\nlet last: TableState = { page: 1, perPage: 20, sort: null, search: '' }\n\nconst canManage = context.can('seo.manage')\n\nconst edit = createModal<SeoRedirect, { redirect: SeoRedirect | null }>(SeoRedirectDialog)\nconst test = createModal<void, Record<string, never>>(TestUrlDialog)\n\nconst columns = computed<TableColumn<SeoRedirect>[]>(() => [\n { key: 'pattern', label: t('page.address'), sortable: true },\n { key: 'target', label: t('page.target') },\n { key: 'status', label: t('page.status'), align: 'center', hideBelow: 560 },\n { key: 'hits', label: t('page.hits'), align: 'center', sortable: true, hideBelow: 760 },\n {\n key: 'last_hit_at',\n label: t('page.last-hit'),\n sortable: true,\n hideBelow: 900,\n hideOnCards: true,\n },\n { key: 'is_active', label: t('page.state'), align: 'center', hideBelow: 660 },\n {\n key: 'actions',\n label: '',\n width: 56,\n align: 'right',\n hidden: !canManage,\n hideOnCards: true,\n },\n])\n\nasync function load(state: TableState): Promise<void> {\n last = state\n loading.value = true\n\n try {\n page.value = await api.redirects({\n q: state.search,\n sort: state.sort ? `${state.sort.order === 'desc' ? '-' : ''}${state.sort.key}` : '-hits',\n page: state.page,\n per_page: state.perPage,\n })\n } finally {\n loading.value = false\n }\n}\n\nasync function open(redirect: SeoRedirect | null): Promise<void> {\n const saved = await edit({ redirect })\n\n if (saved) void load(last)\n}\n\nasync function remove(redirect: SeoRedirect): Promise<void> {\n const agreed = await confirm({\n title: t('page.delete-title', { pattern: redirect.pattern }),\n message: t('page.delete-text'),\n confirmText: t('page.delete'),\n cancelText: t('page.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeRedirect(redirect.id)\n toast.success(t('page.deleted'))\n void load(last)\n } catch (error) {\n toast.danger((error as { body?: { message?: string } }).body?.message ?? String(error))\n }\n}\n</script>\n\n<template>\n <seo-layout :base=\"props.base\" current=\"redirects\" @test=\"test({})\">\n <wx-card>\n <template #header>{{ t('page.redirects') }}</template>\n\n <template v-if=\"canManage\" #extra>\n <wx-button type=\"primary\" icon=\"add\" @click=\"open(null)\">\n {{ t('page.new-redirect') }}\n </wx-button>\n </template>\n\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('page.search-redirects')\"\n :empty-text=\"t('page.empty')\"\n @state-change=\"load\"\n @row-click=\"canManage ? open($event) : undefined\"\n >\n <template #cell-pattern=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.pattern }}</wx-text>\n <!-- Said out loud rather than refused on save: the middleware steps over it, and a\n row that quietly does nothing is a row nobody ever fixes. -->\n <wx-badge v-if=\"row.is_loop\" type=\"warning\">{{ t('page.loop') }}</wx-badge>\n </template>\n\n <template #cell-target=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.target }}</wx-text>\n </template>\n\n <template #cell-last_hit_at=\"{ row }\">\n <wx-text size=\"sm\" :tone=\"row.last_hit_at ? 'default' : 'muted'\">\n {{ row.last_hit_at ? new Date(row.last_hit_at).toLocaleString() : t('page.never') }}\n </wx-text>\n </template>\n\n <template #cell-is_active=\"{ row }\">\n <wx-badge :type=\"row.is_active ? 'success' : 'default'\" dot>\n {{ row.is_active ? t('page.active') : t('page.inactive') }}\n </wx-badge>\n </template>\n\n <template #card-actions=\"{ row }\">\n <wx-actions v-if=\"canManage\" size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <wx-actions size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n </wx-table>\n </wx-card>\n </seo-layout>\n</template>\n","import { localizedValue, type LocalizedValue } from '@webx-ui/core'\nimport type { SeoUrlInput, SeoUrlRule, SeoValue } from './types'\n\n/** The fields the card owns, as opposed to the ones the rule form owns. */\nconst SEO_FIELDS = [\n 'title',\n 'h1',\n 'description',\n 'keywords',\n 'og_title',\n 'og_description',\n 'og_image',\n 'canonical',\n 'robots',\n 'json_ld',\n] as const\n\n/**\n * What a rule is called in a table.\n *\n * Any language rather than the current one: a record half translated is the normal state of a\n * site being worked on, and a blank cell says less than the language that does have the words.\n */\nexport function ruleTitle(rule: SeoUrlRule, locale?: string): string {\n return localizedValue(rule.title as LocalizedValue | undefined, locale)\n}\n\n/** The SEO half of a rule, for the card to edit. */\nexport function seoOf(rule: SeoUrlRule | null): SeoValue {\n if (!rule) return {}\n\n const value: SeoValue = {}\n\n for (const key of SEO_FIELDS) {\n const one = rule[key]\n\n if (one !== null && one !== undefined) {\n Object.assign(value, { [key]: one })\n }\n }\n\n return value\n}\n\n/**\n * What the form sends: the rule's own fields, with the card's value spread back over them.\n *\n * Every field of the card goes, present or not — a `PUT` replaces the whole rule, so a\n * description somebody cleared has to arrive as nothing rather than not arrive at all.\n */\nexport function ruleInput(\n own: Pick<SeoUrlInput, 'match_type' | 'pattern' | 'priority' | 'is_active'>,\n seo: SeoValue,\n): SeoUrlInput {\n const input: SeoUrlInput = { ...own }\n\n for (const key of SEO_FIELDS) {\n Object.assign(input, { [key]: seo[key] ?? null })\n }\n\n return input\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watch, type Component } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n toast,\n useModal,\n WxButton,\n WxDialog,\n WxFormItem,\n WxInput,\n WxInputNumber,\n WxSelect,\n WxSpace,\n WxSwitch,\n} from '@webx-ui/core'\nimport SeoCard from './SeoCard.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport { ruleInput, seoOf } from './rule'\nimport type { MatchType, SeoUrlRule, SeoValue } from './types'\n\n/**\n * One rule: which addresses it covers, and what it says about them.\n *\n * A dialog rather than a second route, and a template rather than a described screen — the same\n * decision `admins.form` is on, until the screens mechanism has been round the block a few more\n * times. What it edits below the address is `WxSeo`, the same card a content module will get by\n * patching it into its own form.\n */\nconst props = withDefaults(defineProps<{ rule?: SeoUrlRule | null; mediaField?: Component }>(), {\n rule: null,\n mediaField: undefined,\n})\n\nconst { resolve, dismiss, open } = useModal<SeoUrlRule>()\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst form = ref({\n match_type: 'exact' as MatchType,\n pattern: '',\n priority: 0,\n is_active: true,\n})\n\nconst seo = ref<SeoValue>({})\n\nconst editing = computed(() => props.rule !== null)\n\nconst kindOptions = computed(() => [\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\nwatch(\n () => props.rule,\n (rule) => {\n form.value = {\n match_type: rule?.match_type ?? 'exact',\n pattern: rule?.pattern ?? '',\n priority: rule?.priority ?? 0,\n is_active: rule?.is_active ?? true,\n }\n seo.value = seoOf(rule)\n errors.value = {}\n },\n { immediate: true },\n)\n\nfunction errorOf(field: string): string | undefined {\n return errors.value[field]?.[0]\n}\n\nasync function save(): Promise<void> {\n saving.value = true\n errors.value = {}\n\n const input = ruleInput(form.value, seo.value)\n\n try {\n const saved = props.rule\n ? await api.updateUrl(props.rule.id, input)\n : await api.createUrl(input)\n\n toast.success(t('page.saved'))\n resolve(saved)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]>; message?: string } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('page.failed'))\n } else {\n toast.danger(body?.message ?? t('page.failed'))\n }\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('page.rule')\" :width=\"860\">\n <div class=\"wx-seo-rule\">\n <div class=\"wx-seo-rule__where\">\n <wx-form-item :label=\"t('page.kind')\">\n <wx-select v-model=\"form.match_type\" :options=\"kindOptions\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('page.address')\"\n :help=\"t('page.address-help')\"\n :error=\"errorOf('pattern')\"\n class=\"wx-seo-rule__address\"\n >\n <wx-input v-model=\"form.pattern\" placeholder=\"/catalog/shoes\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.priority')\" :help=\"t('page.priority-help')\">\n <wx-input-number v-model=\"form.priority\" :step=\"10\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('page.state')\">\n <wx-switch v-model=\"form.is_active\" />\n </wx-form-item>\n </div>\n\n <seo-card v-model=\"seo\" :media-field=\"props.mediaField\" />\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('page.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" @click=\"save\">\n {{ editing ? t('page.save') : t('page.new-rule') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n\n<style scoped>\n.wx-seo-rule {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-16);\n container-type: inline-size;\n}\n\n/* The address and its kind on one line: they are one decision, and reading them apart is\n reading half of it. */\n.wx-seo-rule__where {\n display: grid;\n grid-template-columns: 180px 1fr 140px 100px;\n gap: var(--wx-space-12);\n align-items: start;\n}\n\n@container (max-width: 720px) {\n .wx-seo-rule__where {\n grid-template-columns: 1fr 1fr;\n }\n\n .wx-seo-rule__address {\n grid-column: 1 / -1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, type Component } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAction,\n WxActions,\n WxBadge,\n WxButton,\n WxCard,\n WxSelect,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport SeoUrlDialog from './SeoUrlDialog.vue'\nimport TestUrlDialog from './TestUrlDialog.vue'\nimport SeoLayout from './SeoLayout.vue'\nimport { createSeoApi } from './api'\nimport { useSeoMessages } from './i18n'\nimport { ruleTitle } from './rule'\nimport type { SeoPage, SeoUrlRule } from './types'\n\n/**\n * The rules written for addresses.\n *\n * Listed in the order the site tries them — exact, then mask, then regular expression, by\n * priority inside each group — so that reading the table top to bottom is reading what will\n * happen. Sorting it any other way would hide the one thing a person comes here to work out.\n */\nconst props = withDefaults(defineProps<{ base?: string; mediaField?: Component }>(), {\n base: '/seo',\n mediaField: undefined,\n})\n\nconst context = useAdmin()\nconst api = createSeoApi(context)\nuseSeoMessages()\n\nconst t = useTranslate('webx-seo')\n\nconst page = ref<SeoPage<SeoUrlRule> | null>(null)\nconst loading = ref(false)\n/* A word rather than an empty string: a select reads \"\" as nothing chosen and shows a blank\n control where the option said \"any kind\". */\nconst kind = ref('any')\n\nlet last: TableState = { page: 1, perPage: 20, sort: null, search: '' }\n\nconst canManage = context.can('seo.manage')\n\nconst edit = createModal<SeoUrlRule, { rule: SeoUrlRule | null; mediaField?: Component }>(\n SeoUrlDialog,\n)\nconst test = createModal<void, Record<string, never>>(TestUrlDialog)\n\nconst kindOptions = computed(() => [\n { value: 'any', label: t('page.any-kind') },\n { value: 'exact', label: t('page.exact') },\n { value: 'mask', label: t('page.mask') },\n { value: 'regex', label: t('page.regex') },\n])\n\nconst columns = computed<TableColumn<SeoUrlRule>[]>(() => [\n { key: 'pattern', label: t('page.address') },\n { key: 'match_type', label: t('page.kind'), hideBelow: 560 },\n { key: 'title', label: t('page.title'), hideBelow: 900 },\n { key: 'priority', label: t('page.priority'), align: 'center', hideBelow: 760 },\n { key: 'is_active', label: t('page.state'), align: 'center', hideBelow: 660 },\n {\n key: 'actions',\n label: '',\n width: 56,\n align: 'right',\n hidden: !canManage,\n hideOnCards: true,\n },\n])\n\nasync function load(state: TableState): Promise<void> {\n last = state\n loading.value = true\n\n try {\n page.value = await api.urls({\n q: state.search,\n match_type: kind.value === 'any' ? null : (kind.value as 'exact' | 'mask' | 'regex'),\n page: state.page,\n per_page: state.perPage,\n })\n } finally {\n loading.value = false\n }\n}\n\nasync function open(rule: SeoUrlRule | null): Promise<void> {\n const saved = await edit({ rule, mediaField: props.mediaField })\n\n if (saved) void load(last)\n}\n\nasync function remove(rule: SeoUrlRule): Promise<void> {\n const agreed = await confirm({\n title: t('page.delete-title', { pattern: rule.pattern }),\n message: t('page.delete-text'),\n confirmText: t('page.delete'),\n cancelText: t('page.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeUrl(rule.id)\n toast.success(t('page.deleted'))\n void load(last)\n } catch (error) {\n toast.danger((error as { body?: { message?: string } }).body?.message ?? String(error))\n }\n}\n</script>\n\n<template>\n <seo-layout :base=\"props.base\" current=\"rules\" @test=\"test({})\">\n <wx-card>\n <template #header>{{ t('page.rules') }}</template>\n\n <template v-if=\"canManage\" #extra>\n <wx-button type=\"primary\" icon=\"add\" @click=\"open(null)\">\n {{ t('page.new-rule') }}\n </wx-button>\n </template>\n\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('page.search-rules')\"\n :empty-text=\"t('page.empty')\"\n @state-change=\"load\"\n @row-click=\"canManage ? open($event) : undefined\"\n >\n <template #actions>\n <wx-select v-model=\"kind\" :options=\"kindOptions\" class=\"wx-seo-urls__kind\" />\n </template>\n\n <template #cell-pattern=\"{ row }\">\n <wx-text mono size=\"sm\">{{ row.pattern }}</wx-text>\n </template>\n\n <template #cell-match_type=\"{ row }\">\n <wx-badge>{{ t(`page.${row.match_type}`) }}</wx-badge>\n </template>\n\n <template #cell-title=\"{ row }\">\n <wx-text v-if=\"ruleTitle(row)\" size=\"sm\">{{ ruleTitle(row) }}</wx-text>\n <wx-text v-else size=\"sm\" tone=\"muted\">—</wx-text>\n </template>\n\n <template #cell-is_active=\"{ row }\">\n <wx-badge :type=\"row.is_active ? 'success' : 'default'\" dot>\n {{ row.is_active ? t('page.active') : t('page.inactive') }}\n </wx-badge>\n </template>\n\n <template #card-actions=\"{ row }\">\n <wx-actions v-if=\"canManage\" size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <!-- `.stop`: the row opens the rule, and deleting one is not opening it. -->\n <wx-actions size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('page.delete')\" @click=\"remove(row)\" />\n </wx-actions>\n </template>\n </wx-table>\n </wx-card>\n </seo-layout>\n</template>\n\n<style scoped>\n.wx-seo-urls__kind {\n width: 200px;\n}\n\n/* Once the rows are cards the filters are a column, and a fixed width leaves this one short. */\n.wx-table--cards .wx-seo-urls__kind {\n width: 100%;\n}\n</style>\n","import { markRaw, type Component } from 'vue'\nimport type { AdminModule } from '@webx-ui/module-admin'\nimport SeoAliasesPage from './SeoAliasesPage.vue'\nimport SeoCard from './SeoCard.vue'\nimport SeoRedirectsPage from './SeoRedirectsPage.vue'\nimport SeoUrlsPage from './SeoUrlsPage.vue'\n\nexport interface SeoOptions {\n /** Where the section lives inside the panel. */\n path?: string\n /**\n * The field that picks a share image — `WxMediaField` from `@webx-ui/module-media`.\n *\n * Handed in rather than imported, so this package does not depend on the library being\n * installed. Without it every other SEO field still works; only the picture is missing.\n */\n mediaField?: Component\n}\n\n/**\n * SEO as a section of the panel, and `wx-seo` as a field any screen can use.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed, and stays out of the way when\n * only one is.\n */\nexport function seo(options: SeoOptions = {}): AdminModule {\n const path = options.path ?? '/seo'\n /* A component kept in an options object arrives as a reactive proxy; Vue warns about one and\n `h()` wants the raw thing anyway. */\n const mediaField = options.mediaField ? markRaw(options.mediaField) : undefined\n\n return {\n id: 'seo',\n path,\n routes: [\n // The section's own path travels as a prop, so a panel that mounted it somewhere else\n // still links between its two screens correctly.\n {\n path,\n name: 'webx.seo',\n component: SeoUrlsPage,\n props: { base: path, mediaField },\n },\n {\n path: `${path}/redirects`,\n name: 'webx.seo.redirects',\n component: SeoRedirectsPage,\n props: { base: path },\n },\n // The trail of renames, from `webx-ui/routing`. The screen is here whether or not any\n // content module has registered a type yet: without one the table is simply empty, and an\n // empty table is a better answer than a tab that appears one day without explanation.\n {\n path: `${path}/aliases`,\n name: 'webx.seo.aliases',\n component: SeoAliasesPage,\n props: { base: path },\n },\n ],\n // What a screen means by `wx-seo`: everything a page says about itself, as one value. The\n // server registers the same name for what it stores.\n types: {\n 'wx-seo': {\n component: SeoCard,\n kind: 'field',\n bind: () => ({ mediaField }),\n },\n },\n }\n}\n"],"names":["seoMessages","seeded","useSeoMessages","i18n","useAdmin","props","__props","emit","__emit","context","router","useRouter","route","useRoute","t","useTranslate","title","computed","module","options","where","next","path","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_createVNode","_unref","WxHeading","WxSpace","WxSegmented","$event","WxButton","_renderSlot","_ctx","createSeoApi","admin","base","data","body","page","listQuery","query","id","input","url","locale","open","useModal","api","ref","running","result","fields","seo","one","run","_createBlock","WxDialog","WxInput","_Fragment","WxAlert","WxText","WxBadge","_createTextVNode","_toDisplayString","_hoisted_3","WxDescriptions","_renderList","WxDescriptionsItem","_hoisted_4","step","WxEmpty","loading","test","createModal","TestUrlDialog","multilingual","columns","load","state","SeoLayout","WxCard","WxTable","_withCtx","row","WxLink","robotsDirectives","value","_useModel","locales","useLocales","jsonLdText","jsonLdBroken","field","key","h1","description","keywords","ogTitle","ogDescription","canonical","image","directives","kept","known","merged","robotsOptions","jsonLd","stored","inLocale","map","localizedValue","counted","preview","hasPreview","keptHelp","tone","count","limit","WxTabs","WxTab","WxFormItem","WxTextarea","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_resolveDynamicComponent","_hoisted_11","WxCheckboxGroup","WxCodeEditor","resolve","dismiss","saving","errors","occupant","form","editing","kindOptions","statusOptions","watch","redirect","shadowed","pattern","errorOf","save","saved","toast","error","WxSelect","_cache","WxSwitch","last","canManage","edit","SeoRedirectDialog","remove","confirm","_createSlots","WxActions","WxAction","SEO_FIELDS","ruleTitle","rule","seoOf","ruleInput","own","WxInputNumber","SeoCard","kind","SeoUrlDialog","mediaField","markRaw","SeoUrlsPage","SeoRedirectsPage","SeoAliasesPage"],"mappings":";;;;AAaO,MAAMA,KAAwC;AAAA,EACnD,QAAQ;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAGT,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,IAEN,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,IACpB,OAAO;AAAA,IAEP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,MAAM;AAAA,IACN,OAAO;AAAA,IAEP,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,eAAe;AAAA,IAEf,gBACE;AAAA,IACF,eACE;AAAA,IACF,iBAAiB;AAAA,IAEjB,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,cAAc;AAAA,IAEd,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,gBACE;AAAA,IACF,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,UACE;AAAA,IACF,kBACE;AAAA,EAAA;AAAA,EAGJ,MAAM;AAAA,IACJ,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,IAEpB,OAAO;AAAA,IACP,IAAI;AAAA,IACJ,aAAa;AAAA,IACb,UAAU;AAAA,IACV,WAAW;AAAA,IACX,kBAAkB;AAAA,IAElB,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,SAAS;AAAA,IACT,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,cAAc;AAAA,IAEd,YAAY;AAAA,IACZ,kBAAkB;AAAA,IAClB,YAAY;AAAA,IACZ,WAAW;AAAA,IAEX,WAAW;AAAA,IACX,gBACE;AAAA,IACF,mBAAmB;AAAA,IAEnB,SAAS;AAAA,EAAA;AAEb,GCrHMC,yBAAa,QAAA;AAGZ,SAASC,IAAuB;AACrC,MAAI;AACF,UAAM,EAAE,MAAAC,EAAA,IAASC,EAAA;AAEjB,IAAKH,GAAO,IAAIE,CAAI,MAClBA,EAAK,SAAS,YAAYH,EAAW,GACrCC,GAAO,IAAIE,CAAI;AAAA,EAEnB,QAAQ;AAAA,EAER;AACF;;;;;;;;;ACFA,UAAME,IAAQC,GAERC,IAAOC,GAEPC,IAAUL,EAAA,GACVM,IAASC,GAAA,GACTC,IAAQC,GAAA;AAEd,IAAAX,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAG3BC,IAAQC;AAAA,MACZ,MACER,EAAQ,MAAM,UAAU,QAAQ,KAAK,CAACS,MAAWA,EAAO,OAAO,KAAK,GAAG,SACvEJ,EAAE,cAAc;AAAA,IAAA,GAGdK,IAAUF,EAAS,MAAM;AAAA,MAC7B,EAAE,OAAO,SAAS,OAAOH,EAAE,YAAY,EAAA;AAAA,MACvC,EAAE,OAAO,aAAa,OAAOA,EAAE,gBAAgB,EAAA;AAAA,MAC/C,EAAE,OAAO,WAAW,OAAOA,EAAE,gBAAgB,EAAA;AAAA,IAAE,CAChD,GAEKM,IAAQH,EAAS;AAAA,MACrB,KAAK,MAAMZ,EAAM;AAAA,MACjB,KAAK,CAACgB,MAA0B;AAC9B,cAAMC,IAAOD,MAAS,UAAUhB,EAAM,OAAO,GAAGA,EAAM,IAAI,IAAI,OAAOgB,CAAI,CAAC;AAE1E,QAAIC,MAASV,EAAM,QAAWF,EAAO,KAAKY,CAAI;AAAA,MAChD;AAAA,IAAA,CACD;sBAICC,EAAA,GAAAC,EAaM,OAbNC,IAaM;AAAA,MAZJC,EASM,OATNC,IASM;AAAA,QARJC,EAA+CC,EAAAC,EAAA,GAAA,EAAlC,OAAO,KAAC;AAAA,qBAAE,MAAW;AAAA,gBAARd,EAAA,KAAK,GAAA,CAAA;AAAA,UAAA;;;QAE/BY,EAKWC,EAAAE,CAAA,GAAA,EALD,MAAK,QAAI;AAAA,qBACjB,MAA6D;AAAA,YAA7DH,EAA6DC,EAAAG,EAAA,GAAA;AAAA,0BAAtCZ,EAAA;AAAA,4DAAAA,EAAK,QAAAa;AAAA,cAAG,SAASd,EAAA;AAAA,cAAS,MAAK;AAAA,YAAA;YACtDS,EAEYC,EAAAK,CAAA,GAAA;AAAA,cAFD,SAAQ;AAAA,cAAU,MAAK;AAAA,cAAU,gCAAO3B,EAAI,MAAA;AAAA,YAAA;yBACrD,MAAoB;AAAA,oBAAjBsB,EAAAf,CAAA,EAAC,WAAA,CAAA,GAAA,CAAA;AAAA,cAAA;;;;;;;MAKVqB,GAAQC,EAAA,QAAA,WAAA,CAAA,GAAA,QAAA,EAAA;AAAA,IAAA;;;;;;;;AC5BL,SAASC,EAAaC,GAA6B;AACxD,QAAMC,IAAO,GAAGD,EAAM,OAAO,QACvBE,IAAO,CAAIC,MAAyBA,EAAK,MAKzCC,IAAO,CAAID,OAAqE;AAAA,IACpF,GAAGA,EAAK;AAAA,IACR,MAAMA,EAAK;AAAA,EAAA,IAKPE,IAAY,CAChBC,OACkE;AAAA,IAClE,GAAGA,EAAM,KAAK;AAAA,IACd,YAAYA,EAAM,cAAc;AAAA,IAChC,WAAWA,EAAM,aAAa;AAAA,IAC9B,MAAMA,EAAM,QAAQ;AAAA,IACpB,MAAMA,EAAM;AAAA,IACZ,UAAUA,EAAM;AAAA,EAAA;AAGlB,SAAO;AAAA,IACL,MAAM,CAACA,IAAQ,CAAA,MACbN,EAAM,KACH,IAAqE,GAAGC,CAAI,SAAS;AAAA,MACpF,OAAOI,EAAUC,CAAK;AAAA,IAAA,CACvB,EACA,KAAKF,CAAI;AAAA,IAEd,KAAK,CAACG,MAAOP,EAAM,KAAK,IAA0B,GAAGC,CAAI,SAASM,CAAE,EAAE,EAAE,KAAKL,CAAI;AAAA,IAEjF,WAAW,CAACM,MAAUR,EAAM,KAAK,KAA2B,GAAGC,CAAI,SAASO,CAAK,EAAE,KAAKN,CAAI;AAAA,IAE5F,WAAW,CAACK,GAAIC,MACdR,EAAM,KAAK,IAA0B,GAAGC,CAAI,SAASM,CAAE,IAAIC,CAAK,EAAE,KAAKN,CAAI;AAAA,IAE7E,WAAW,CAACK,MAAOP,EAAM,KAAK,OAAa,GAAGC,CAAI,SAASM,CAAE,EAAE;AAAA,IAE/D,WAAW,CAACD,IAAQ,CAAA,MAClBN,EAAM,KACH;AAAA,MACC,GAAGC,CAAI;AAAA,MACP,EAAE,OAAOI,EAAUC,CAAK,EAAA;AAAA,IAAE,EAE3B,KAAKF,CAAI;AAAA,IAEd,gBAAgB,CAACI,MACfR,EAAM,KAAK,KAA4B,GAAGC,CAAI,cAAcO,CAAK,EAAE,KAAKN,CAAI;AAAA,IAE9E,gBAAgB,CAACK,GAAIC,MACnBR,EAAM,KAAK,IAA2B,GAAGC,CAAI,cAAcM,CAAE,IAAIC,CAAK,EAAE,KAAKN,CAAI;AAAA,IAEnF,gBAAgB,CAACK,MAAOP,EAAM,KAAK,OAAa,GAAGC,CAAI,cAAcM,CAAE,EAAE;AAAA,IAEzE,SAAS,CAACD,IAAQ,CAAA,MAChBN,EAAM,KACH,IAAiE,GAAGC,CAAI,YAAY;AAAA,MACnF,OAAO;AAAA,QACL,GAAGK,EAAM,KAAK;AAAA,QACd,QAAQA,EAAM,UAAU;AAAA,QACxB,MAAMA,EAAM;AAAA,QACZ,UAAUA,EAAM;AAAA,MAAA;AAAA,IAClB,CACD,EACA,KAAKF,CAAI;AAAA;AAAA;AAAA,IAId,MAAM,CAACK,GAAKC,IAAS,SACnBV,EAAM,KACH,KAA8B,GAAGC,CAAI,aAAa,EAAE,KAAAQ,GAAK,QAAQC,KAAU,QAAW,EACtF,KAAKR,CAAI;AAAA,EAAA;AAElB;;;;;;;;;;;;;ACrFA,UAAM,EAAE,MAAAS,EAAA,IAASC,GAAA,GAEXzC,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3BgC,IAAMK,EAAI,EAAE,GACZC,IAAUD,EAAI,EAAK,GACnBE,IAASF,EAA0B,IAAI,GAEvCG,IAAStC,EAAS,MAAM;AAC5B,YAAMuC,IAAMF,EAAO,OAAO;AAE1B,aAAKE,IAEE;AAAA,QACL,EAAE,OAAO1C,EAAE,YAAY,GAAG,OAAO0C,EAAI,MAAA;AAAA,QACrC,EAAE,OAAO1C,EAAE,SAAS,GAAG,OAAO0C,EAAI,GAAA;AAAA,QAClC,EAAE,OAAO1C,EAAE,kBAAkB,GAAG,OAAO0C,EAAI,YAAA;AAAA,QAC3C,EAAE,OAAO1C,EAAE,eAAe,GAAG,OAAO0C,EAAI,SAAA;AAAA,QACxC,EAAE,OAAO1C,EAAE,gBAAgB,GAAG,OAAO0C,EAAI,UAAA;AAAA,QACzC,EAAE,OAAO1C,EAAE,aAAa,GAAG,OAAO0C,EAAI,OAAA;AAAA,MAAO,EAC7C,OAAO,CAACC,MAAQA,EAAI,KAAK,IATV,CAAA;AAAA,IAUnB,CAAC;AAED,mBAAeC,IAAqB;AAClC,UAAIX,EAAI,MAAM,KAAA,MAAW,IAEzB;AAAA,QAAAM,EAAQ,QAAQ;AAEhB,YAAI;AACF,UAAAC,EAAO,QAAQ,MAAMH,EAAI,KAAKJ,EAAI,KAAK;AAAA,QACzC,UAAA;AACE,UAAAM,EAAQ,QAAQ;AAAA,QAClB;AAAA;AAAA,IACF;2BAIEM,EAqEY9B,EAAA+B,EAAA,GAAA;AAAA,MArEO,MAAM/B,EAAAoB,CAAA;AAAA,qDAAAA,EAAI,QAAAhB,IAAA;AAAA,MAAG,OAAOJ,EAAAf,CAAA,EAAC,WAAA;AAAA,MAAgB,OAAO;AAAA,IAAA;iBAC7D,MAmEM;AAAA,QAnENY,EAmEM,OAnEND,IAmEM;AAAA,UAlEJG,EAUWC,EAAAE,CAAA,GAAA;AAAA,YAVD,MAAK;AAAA,YAAK,OAAM;AAAA,UAAA;uBACxB,MAKE;AAAA,cALFH,EAKEC,EAAAgC,CAAA,GAAA;AAAA,4BAJSd,EAAA;AAAA,8DAAAA,EAAG,QAAAd;AAAA,gBACX,aAAaJ,EAAAf,CAAA,EAAC,uBAAA;AAAA,gBACf,OAAM;AAAA,gBACL,YAAa4C,GAAG,CAAA,OAAA,CAAA;AAAA,cAAA;cAEnB9B,EAEYC,EAAAK,CAAA,GAAA;AAAA,gBAFD,MAAK;AAAA,gBAAW,SAASmB,EAAA;AAAA,gBAAU,SAAOK;AAAA,cAAA;2BACnD,MAAwB;AAAA,sBAArB7B,EAAAf,CAAA,EAAC,eAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;;;;UAIQwC,EAAA,cAAhB9B,EAmDWsC,IAAA,EAAA,KAAA,KAAA;AAAA,YAhDDR,EAAA,MAAO,iBADfK,EASE9B,EAAAkC,CAAA,GAAA;AAAA;cAPA,MAAK;AAAA,cACJ,aAA0BlC,EAAAf,CAAA,EAAC,wBAAA;AAAA,wBAAiDwC,EAAA,MAAO,SAAS;AAAA,wBAA8BA,EAAA,MAAO,SAAS;AAAA,cAAA;;YAWrIA,EAAA,MAAO,cADfK,EAQE9B,EAAAkC,CAAA,GAAA;AAAA;cANA,MAAK;AAAA,cACJ,aAA0BT,EAAA,MAAO,MAAM,SAAI,UAA6BzB,EAAAf,CAAA,EAAC,uBAAA,EAAA,QAAkCwC,EAAA,MAAO,MAAM,UAAUA,EAAA,MAAO,MAAM,KAAA,CAAI,IAAoBzB,EAAAf,CAAA,EAAC,iBAAA,EAAA,MAA0BwC,EAAA,MAAO,MAAM,MAAI;AAAA,YAAA;YAOrMA,EAAA,MAAO,WACxB/B,KAAAC,EAMM,OANNG,IAMM;AAAA,cALJC,EAAsEC,EAAAmC,CAAA,GAAA;AAAA,gBAA7D,MAAK;AAAA,gBAAK,MAAK;AAAA,cAAA;2BAAQ,MAA4B;AAAA,sBAAzBnC,EAAAf,CAAA,EAAC,mBAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;cACpCc,EAGWC,EAAAE,CAAA,GAAA;AAAA,gBAHD,MAAK;AAAA,gBAAK,OAAM;AAAA,cAAA;2BACxB,MAAiE;AAAA,kBAAjEH,EAAiEC,EAAAoC,CAAA,GAAA,MAAA;AAAA,+BAAvD,MAA4C;AAAA,sBAAzCC,EAAAC,EAAAtC,EAAAf,CAAA,EAAC,QAASwC,EAAA,MAAO,QAAQ,UAAU,EAAA,CAAA,GAAA,CAAA;AAAA,oBAAA;;;kBAChD1B,EAA8DC,EAAAmC,CAAA,GAAA;AAAA,oBAArD,MAAA;AAAA,oBAAK,MAAK;AAAA,kBAAA;+BAAK,MAA4B;AAAA,0BAAzBV,EAAA,MAAO,QAAQ,OAAO,GAAA,CAAA;AAAA,oBAAA;;;;;;wBALrDK,EAAkF9B,EAAAkC,CAAA,GAAA;AAAA;cAAjD,MAAK;AAAA,cAAQ,aAAalC,EAAAf,CAAA,EAAC,gBAAA;AAAA,YAAA;YASjDyC,EAAA,MAAO,SAAM,KAAxBhC,KAAAC,EAOM,OAPN4C,IAOM;AAAA,cANJxC,EAAqEC,EAAAmC,CAAA,GAAA;AAAA,gBAA5D,MAAK;AAAA,gBAAK,MAAK;AAAA,cAAA;2BAAQ,MAA2B;AAAA,sBAAxBnC,EAAAf,CAAA,EAAC,kBAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;cACpCc,EAIkBC,EAAAwC,EAAA,GAAA;AAAA,gBAJA,SAAS;AAAA,gBAAG,UAAA;AAAA,gBAAS,MAAK;AAAA,cAAA;2BACpB,MAAqB;AAAA,0BAA3C7C,EAEuBsC,IAAA,MAAAQ,GAFaf,EAAA,OAAM,CAAbE,YAA7BE,EAEuB9B,EAAA0C,EAAA,GAAA;AAAA,oBAFsB,KAAKd,EAAI;AAAA,oBAAQ,OAAOA,EAAI;AAAA,kBAAA;+BACvE,MAAe;AAAA,sBAAZS,EAAAC,EAAAV,EAAI,KAAK,GAAA,CAAA;AAAA,oBAAA;;;;;;;YAKPH,EAAA,MAAO,MAAM,SAAM,KAA9B/B,KAAAC,EAOM,OAPNgD,IAOM;AAAA,cANJ5C,EAAoEC,EAAAmC,CAAA,GAAA;AAAA,gBAA3D,MAAK;AAAA,gBAAK,MAAK;AAAA,cAAA;2BAAQ,MAA0B;AAAA,sBAAvBnC,EAAAf,CAAA,EAAC,iBAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;cACpCc,EAIWC,EAAAE,CAAA,GAAA;AAAA,gBAJD,MAAK;AAAA,gBAAK,MAAA;AAAA,cAAA;2BACR,MAA4B;AAAA,mBAAtCR,EAAA,EAAA,GAAAC,EAEWsC,IAAA,MAAAQ,GAFchB,EAAA,MAAO,QAAfmB,YAAjBd,EAEW9B,EAAAoC,CAAA,GAAA;AAAA,oBAF6B,KAAKQ,EAAK;AAAA,kBAAA;+BAChD,MAAiB;AAAA,sBAAdP,EAAAC,EAAAM,EAAK,MAAM,IAAG,QAAGN,EAAGM,EAAK,QAAQ,GAAA,CAAA;AAAA,oBAAA;;;;;;;oBAMtBpB,EAAA,0BAAtBM,EAAmF9B,EAAA6C,EAAA,GAAA;AAAA;YAApD,MAAK;AAAA,YAAU,aAAa7C,EAAAf,CAAA,EAAC,iBAAA;AAAA,UAAA;;;;;;;;;;;;ACtGlE,UAAMT,IAAQC,GAERG,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3B2B,IAAOU,EAA8B,IAAI,GACzCuB,IAAUvB,EAAI,EAAK,GAEnBwB,IAAOC,EAAyCC,EAAa,GAG7DC,IAAe9D,EAAS,OAAOR,EAAQ,MAAM,UAAU,SAAS,UAAU,KAAK,CAAC,GAEhFuE,IAAU/D,EAAkC,MAAM;AAAA,MACtD,EAAE,KAAK,WAAW,OAAOH,EAAE,cAAc,GAAG,UAAU,GAAA;AAAA,MACtD,EAAE,KAAK,UAAU,OAAOA,EAAE,aAAa,EAAA;AAAA,MACvC;AAAA,QACE,KAAK;AAAA,QACL,OAAOA,EAAE,eAAe;AAAA,QACxB,OAAO;AAAA,QACP,QAAQ,CAACiE,EAAa;AAAA,QACtB,WAAW;AAAA,MAAA;AAAA,MAEb,EAAE,KAAK,cAAc,OAAOjE,EAAE,eAAe,GAAG,WAAW,KAAK,aAAa,GAAA;AAAA,IAAK,CACnF;AAED,mBAAemE,EAAKC,GAAkC;AACpD,MAAAP,EAAQ,QAAQ;AAEhB,UAAI;AACF,QAAAjC,EAAK,QAAQ,MAAMS,EAAI,QAAQ;AAAA,UAC7B,GAAG+B,EAAM;AAAA,UACT,MAAMA,EAAM;AAAA,UACZ,UAAUA,EAAM;AAAA,QAAA,CACjB;AAAA,MACH,UAAA;AACE,QAAAP,EAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;2BAIEhB,EA+CawB,IAAA;AAAA,MA/CA,MAAM9E,EAAM;AAAA,MAAM,SAAQ;AAAA,MAAW,+BAAMwB,EAAA+C,CAAA,EAAI,CAAA,CAAA;AAAA,IAAA;iBAC1D,MA6CU;AAAA,QA7CVhD,EA6CUC,EAAAuD,EAAA,GAAA,MAAA;AAAA,UA5CG,UAAO,MAAyB;AAAA,gBAAtBvD,EAAAf,CAAA,EAAC,gBAAA,CAAA,GAAA,CAAA;AAAA,UAAA;qBAEtB,MAKE;AAAA,YALFc,EAKEC,EAAAkC,CAAA,GAAA;AAAA,cAJA,MAAK;AAAA,cACL,SAAQ;AAAA,cACP,aAAalC,EAAAf,CAAA,EAAC,mBAAA;AAAA,cACf,OAAM;AAAA,YAAA;YAGRc,EAkCWC,EAAAwD,EAAA,GAAA;AAAA,cAjCR,MAAM3C,EAAA;AAAA,cACN,SAASsC,EAAA;AAAA,cACV,WAAQ;AAAA,cACR,YAAA;AAAA,cACA,OAAA;AAAA,cACC,SAASL,EAAA;AAAA,cACT,sBAAoB9C,EAAAf,CAAA,EAAC,qBAAA;AAAA,cACrB,cAAYe,EAAAf,CAAA,EAAC,oBAAA;AAAA,cACb,eAAcmE;AAAA,YAAA;cAEJ,gBAAYK,EACrB,CAAmD,EAD1B,KAAAC,QAAG;AAAA,gBAC5B3D,EAAmDC,EAAAmC,CAAA,GAAA;AAAA,kBAA1C,MAAA;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BAAK,MAAiB;AAAA,oBAAdE,EAAAC,EAAAoB,EAAI,OAAO,GAAA,CAAA;AAAA,kBAAA;;;;cAM7B,eAAWD,EACpB,CAEU,EAHc,KAAAC,QAAG;AAAA,gBACZA,EAAI,UAAUA,EAAI,mBAAjC5B,EAEU9B,EAAA2D,EAAA,GAAA;AAAA;kBAFoC,MAAMD,EAAI;AAAA,kBAAY,UAAA;AAAA,kBAAS,MAAK;AAAA,gBAAA;6BAChF,MAAkD;AAAA,oBAAlD3D,EAAkDC,EAAAmC,CAAA,GAAA;AAAA,sBAAzC,MAAA;AAAA,sBAAK,MAAK;AAAA,oBAAA;iCAAK,MAAgB;AAAA,wBAAbE,EAAAC,EAAAoB,EAAI,MAAM,GAAA,CAAA;AAAA,sBAAA;;;;;4CAEvC5B,EAA+D9B,EAAAoC,CAAA,GAAA;AAAA;kBAA9C,MAAK;AAAA,gBAAA;6BAAU,MAAoB;AAAA,wBAAjBpC,EAAAf,CAAA,EAAC,WAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAG3B,eAAWwE,EACpB,CAAqC,EADb,KAAAC,QAAG;AAAA,gBAC3B3D,EAAqCC,EAAAoC,CAAA,GAAA,MAAA;AAAA,6BAA3B,MAAgB;AAAA,oBAAbC,EAAAC,EAAAoB,EAAI,MAAM,GAAA,CAAA;AAAA,kBAAA;;;;cAGd,mBAAeD,EACxB,CAEU,EAHkB,KAAAC,QAAG;AAAA,gBAC/B3D,EAEUC,EAAAmC,CAAA,GAAA;AAAA,kBAFD,MAAK;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BACtB,MAAqE;AAAA,wBAAlEuB,EAAI,aAAU,IAAO,KAAKA,EAAI,UAAU,EAAE,mBAAc,EAAA,GAAA,CAAA;AAAA,kBAAA;;;;;;;;;;;;;oEC4E1DE,KAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;;;;;;;;;;;;;;;;;;;;AC3KA,UAAMpF,IAAQC,GA+BRoF,IAAQC,GAA4BrF,GAAA,YAAkB;AAE5D,IAAAJ,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAC3B6E,IAAUC,GAAA,GAEVC,IAAa1C,EAAmB,IAAI,GACpC2C,IAAe3C,EAAI,EAAK;AAG9B,aAAS4C,EAAgCC,GAAQ;AAC/C,aAAOhF,EAAS;AAAA,QACd,KAAK,MAAMyE,EAAM,QAAQO,CAAG;AAAA,QAC5B,KAAK,CAAC5E,MAAsB;AAC1B,UAAAqE,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,IAAK,CAACO,CAAG,GAAG5E,EAAA;AAAA,QACjD;AAAA,MAAA,CACD;AAAA,IACH;AAEA,UAAML,IAAQgF,EAAM,OAAO,GACrBE,IAAKF,EAAM,IAAI,GACfG,IAAcH,EAAM,aAAa,GACjCI,IAAWJ,EAAM,UAAU,GAC3BK,IAAUL,EAAM,UAAU,GAC1BM,IAAgBN,EAAM,gBAAgB,GAEtCO,IAAYtF,EAAiB;AAAA,MACjC,KAAK,MAAMyE,EAAM,OAAO,aAAa;AAAA,MACrC,KAAK,CAACrE,MAAiB;AACrB,QAAAqE,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,CAAA,GAAK,WAAWrE,EAAK,KAAA,MAAW,KAAK,OAAOA,EAAA;AAAA,MACjF;AAAA,IAAA,CACD,GAEKmF,IAAQvF,EAAS;AAAA,MACrB,KAAK,MAAMyE,EAAM,OAAO,YAAY;AAAA,MACpC,KAAK,CAACrE,MAA0B;AAC9B,QAAAqE,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,CAAA,GAAK,UAAUrE,EAAA;AAAA,MACpD;AAAA,IAAA,CACD,GAKKoF,IAAaxF,EAAS,OAAOyE,EAAM,OAAO,UAAU,IAAI,MAAM,GAAG,EAAE,IAAI,CAACjC,MAAQA,EAAI,KAAA,CAAM,CAAC,GAE3FiD,IAAOzF;AAAA,MAAS,MACpBwF,EAAW,MAAM,OAAO,CAAChD,MAAQA,MAAQ,MAAM,CAACgC,GAAiB,SAAShC,CAAY,CAAC;AAAA,IAAA,GAGnFkD,IAAQ1F,EAAS;AAAA,MACrB,KAAK,MAAMwF,EAAW,MAAM,OAAO,CAAChD,MAAQgC,GAAiB,SAAShC,CAAY,CAAC;AAAA,MACnF,KAAK,CAACpC,MAA4B;AAChC,cAAMuF,IAAS,CAAC,GAAGvF,GAAM,GAAGqF,EAAK,KAAK,EAAE,KAAK,IAAI;AACjD,QAAAhB,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,IAAK,QAAQkB,MAAW,KAAK,OAAOA,EAAA;AAAA,MACzE;AAAA,IAAA,CACD,GAEKC,IAAgB5F;AAAA,MAAS,MAC7BwE,GAAiB,IAAI,CAAChC,OAAS,EAAE,OAAOA,GAAK,OAAO3C,EAAE,QAAQ2C,CAAG,EAAE,IAAI;AAAA,IAAA,GAGnEqD,IAAS7F,EAAiB;AAAA,MAC9B,KAAK,MAAM;AACT,YAAI6E,EAAW,UAAU,KAAM,QAAOA,EAAW;AAEjD,cAAMiB,IAASrB,EAAM,OAAO;AAE5B,eAAOqB,KAAW,OAA+B,KAAK,KAAK,UAAUA,GAAQ,MAAM,CAAC;AAAA,MACtF;AAAA,MACA,KAAK,CAAC1F,MAAiB;AAKrB,YAFAyE,EAAW,QAAQzE,GAEfA,EAAK,KAAA,MAAW,IAAI;AACtB,UAAA0E,EAAa,QAAQ,IACrBL,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,CAAA,GAAK,SAAS,KAAA;AAEjD;AAAA,QACF;AAEA,YAAI;AACF,UAAAA,EAAM,QAAQ,EAAE,GAAIA,EAAM,SAAS,IAAK,SAAS,KAAK,MAAMrE,CAAI,EAAA,GAChE0E,EAAa,QAAQ;AAAA,QACvB,QAAQ;AACN,UAAAA,EAAa,QAAQ;AAAA,QACvB;AAAA,MACF;AAAA,IAAA,CACD;AAGD,aAASiB,EAASC,GAAyC;AACzD,aAAOC,GAAeD,GAAKrB,EAAQ,OAAO,KAAK;AAAA,IACjD;AAEA,UAAMuB,KAAUlG,EAAS,OAAO;AAAA,MAC9B,OAAO+F,EAAShG,EAAM,KAAmC,EAAE;AAAA,MAC3D,aAAagG,EAASb,EAAY,KAAmC,EAAE;AAAA,IAAA,EACvE,GAEIiB,IAAUnG,EAAS,OAAO;AAAA,MAC9B,OAAO+F,EAAShG,EAAM,KAAmC;AAAA,MACzD,aAAagG,EAASb,EAAY,KAAmC;AAAA,MACrE,SAASI,EAAU,UAAUlG,EAAM,QAAQ;AAAA,IAAA,EAC3C,GAEIgH,KAAapG,EAAS,MAAMmG,EAAQ,MAAM,UAAU,MAAMA,EAAQ,MAAM,gBAAgB,EAAE,GAE1FE,KAAWrG;AAAA,MAAS,MACxByF,EAAK,MAAM,WAAW,IAClB,SACA5F,EAAE,oBAAoB,EAAE,YAAY4F,EAAK,MAAM,KAAK,IAAI,GAAG;AAAA,IAAA;AAGjE,aAASa,GAAKC,GAAeC,GAAoC;AAC/D,aAAOD,IAAQC,IAAQ,YAAY;AAAA,IACrC;sBAIElG,EAAA,GAAAC,EAiGM,OAjGNC,IAiGM;AAAA,MAhGJG,EA+FUC,EAAA6F,EAAA,GAAA,EA/FD,cAAA,MAAU;AAAA,mBACjB,MAuCS;AAAA,UAvCT9F,EAuCSC,EAAA8F,EAAA,GAAA;AAAA,YAvCA,OAAO9F,EAAAf,CAAA,EAAC,mBAAA;AAAA,YAAuB,OAAM;AAAA,UAAA;uBAC5C,MAqCM;AAAA,cArCNY,EAqCM,OArCNC,IAqCM;AAAA,gBApCJC,EAOeC,EAAA+F,CAAA,GAAA;AAAA,kBAPA,OAAO/F,EAAAf,CAAA,EAAC,YAAA;AAAA,kBAAiB,UAAUR,EAAA;AAAA,gBAAA;6BAChD,MAAsC;AAAA,oBAAtCsB,EAAsCC,EAAAgC,CAAA,GAAA;AAAA,kCAAnBhC,EAAAb,CAAA;AAAA,2EAAAA,EAAK,QAAAiB,IAAA;AAAA,sBAAE,WAAA;AAAA,oBAAA;oBAC1BP,EAIM,OAJN0C,IAIM;AAAA,sBAHJxC,EAEUC,EAAAmC,CAAA,GAAA;AAAA,wBAFD,MAAK;AAAA,wBAAM,MAAMuD,GAAKJ,SAAQ,OAAO7G,EAAA,UAAU;AAAA,sBAAA;mCACtD,MAAmB;AAAA,0BAAhB4D,EAAAC,EAAAgD,GAAA,MAAQ,KAAK,IAAG,UAAM7G,EAAA,UAAU,GAAA,CAAA;AAAA,wBAAA;;;;;;;gBAKzCsB,EAEeC,EAAA+F,CAAA,GAAA;AAAA,kBAFA,OAAO/F,EAAAf,CAAA,EAAC,SAAA;AAAA,kBAAc,UAAUR,EAAA;AAAA,gBAAA;6BAC7C,MAAmC;AAAA,oBAAnCsB,EAAmCC,EAAAgC,CAAA,GAAA;AAAA,kCAAhBhC,EAAAqE,CAAA;AAAA,2EAAAA,EAAE,QAAAjE,IAAA;AAAA,sBAAE,WAAA;AAAA,oBAAA;;;;gBAGzBL,EAOeC,EAAA+F,CAAA,GAAA;AAAA,kBAPA,OAAO/F,EAAAf,CAAA,EAAC,kBAAA;AAAA,kBAAuB,UAAUR,EAAA;AAAA,gBAAA;6BACtD,MAAyD;AAAA,oBAAzDsB,EAAyDC,EAAAgG,EAAA,GAAA;AAAA,kCAAnChG,EAAAsE,CAAA;AAAA,2EAAAA,EAAW,QAAAlE,IAAA;AAAA,sBAAE,WAAA;AAAA,sBAAW,MAAM;AAAA,oBAAA;oBACpDP,EAIM,OAJN8C,IAIM;AAAA,sBAHJ5C,EAEUC,EAAAmC,CAAA,GAAA;AAAA,wBAFD,MAAK;AAAA,wBAAM,MAAMuD,GAAKJ,SAAQ,aAAa7G,EAAA,gBAAgB;AAAA,sBAAA;mCAClE,MAAyB;AAAA,0BAAtB4D,EAAAC,EAAAgD,GAAA,MAAQ,WAAW,IAAG,UAAM7G,EAAA,gBAAgB,GAAA,CAAA;AAAA,wBAAA;;;;;;;gBAKrDsB,EAEeC,EAAA+F,CAAA,GAAA;AAAA,kBAFA,OAAO/F,EAAAf,CAAA,EAAC,eAAA;AAAA,kBAAoB,UAAUR,EAAA;AAAA,gBAAA;6BACnD,MAAyC;AAAA,oBAAzCsB,EAAyCC,EAAAgC,CAAA,GAAA;AAAA,kCAAtBhC,EAAAuE,CAAA;AAAA,2EAAAA,EAAQ,QAAAnE,IAAA;AAAA,sBAAE,WAAA;AAAA,oBAAA;;;;gBAGpBoF,GAAA,SAAX9F,EAAA,GAAAC,EASM,OATNsG,IASM;AAAA,kBARJlG,EAAiEC,EAAAmC,CAAA,GAAA;AAAA,oBAAxD,MAAK;AAAA,oBAAK,MAAK;AAAA,kBAAA;+BAAQ,MAAuB;AAAA,0BAApBnC,EAAAf,CAAA,EAAC,cAAA,CAAA,GAAA,CAAA;AAAA,oBAAA;;;kBACpCY,EAMM,OANNqG,IAMM;AAAA,oBALOX,EAAA,MAAQ,WAAnB7F,EAAA,GAAAC,EAEM,OAFNwG,IAEM7D,EADDiD,EAAA,MAAQ,OAAO,GAAA,CAAA;oBAEpB1F,EAA4D,OAA5DuG,IAA4D9D,EAAtBiD,EAAA,MAAQ,KAAK,GAAA,CAAA;AAAA,oBACnD1F,EAAiE,OAAjEwG,IAAiE/D,EAA5BiD,EAAA,MAAQ,WAAW,GAAA,CAAA;AAAA,kBAAA;;;;;;UAMhExF,EAoBSC,EAAA8F,EAAA,GAAA;AAAA,YApBA,OAAO9F,EAAAf,CAAA,EAAC,oBAAA;AAAA,YAAwB,OAAM;AAAA,UAAA;uBAC7C,MAkBM;AAAA,cAlBNY,EAkBM,OAlBNyG,IAkBM;AAAA,gBAfI7H,EAAA,cAFRiB,EAAA,GAAAoC,EAQEyE,GAPK9H,EAAA,UAAU,GAAA;AAAA;8BAENkG,EAAA;AAAA,gEAAAA,EAAK,QAAAvE;AAAA,kBACb,OAAOJ,EAAAf,CAAA,EAAC,eAAA;AAAA,kBACT,QAAO;AAAA,kBACP,QAAO;AAAA,kBACN,UAAUR,EAAA;AAAA,gBAAA;gBAGbsB,EAEeC,EAAA+F,CAAA,GAAA;AAAA,kBAFA,OAAO/F,EAAAf,CAAA,EAAC,eAAA;AAAA,kBAAoB,MAAMe,EAAAf,CAAA,EAAC,cAAA;AAAA,kBAAmB,UAAUR,EAAA;AAAA,gBAAA;6BAC7E,MAAwC;AAAA,oBAAxCsB,EAAwCC,EAAAgC,CAAA,GAAA;AAAA,kCAArBhC,EAAAwE,CAAA;AAAA,2EAAAA,EAAO,QAAApE,IAAA;AAAA,sBAAE,WAAA;AAAA,oBAAA;;;;gBAG9BL,EAEeC,EAAA+F,CAAA,GAAA;AAAA,kBAFA,OAAO/F,EAAAf,CAAA,EAAC,qBAAA;AAAA,kBAA0B,UAAUR,EAAA;AAAA,gBAAA;6BACzD,MAA2D;AAAA,oBAA3DsB,EAA2DC,EAAAgG,EAAA,GAAA;AAAA,kCAArChG,EAAAyE,CAAA;AAAA,2EAAAA,EAAa,QAAArE,IAAA;AAAA,sBAAE,WAAA;AAAA,sBAAW,MAAM;AAAA,oBAAA;;;;;;;;UAK5DL,EA8BSC,EAAA8F,EAAA,GAAA;AAAA,YA9BA,OAAO9F,EAAAf,CAAA,EAAC,uBAAA;AAAA,YAA2B,OAAM;AAAA,UAAA;uBAChD,MA4BM;AAAA,cA5BNY,EA4BM,OA5BN2G,IA4BM;AAAA,gBA3BJzG,EAMeC,EAAA+F,CAAA,GAAA;AAAA,kBALZ,OAAO/F,EAAAf,CAAA,EAAC,gBAAA;AAAA,kBACR,MAAMe,EAAAf,CAAA,EAAC,qBAAA;AAAA,kBACP,UAAUR,EAAA;AAAA,gBAAA;6BAEX,MAAuE;AAAA,oBAAvEsB,EAAuEC,EAAAgC,CAAA,GAAA;AAAA,kCAApD0C,EAAA;AAAA,oEAAAA,EAAS,QAAAtE;AAAA,sBAAE,aAAY;AAAA,oBAAA;;;;gBAG5CL,EAEeC,EAAA+F,CAAA,GAAA;AAAA,kBAFA,OAAO/F,EAAAf,CAAA,EAAC,aAAA;AAAA,kBAAkB,MAAMwG,GAAA;AAAA,kBAAW,UAAUhH,EAAA;AAAA,gBAAA;6BAClE,MAA8D;AAAA,oBAA9DsB,EAA8DC,EAAAyG,EAAA,GAAA;AAAA,kCAAlC3B,EAAA;AAAA,oEAAAA,EAAK,QAAA1E;AAAA,sBAAG,SAAS4E,EAAA;AAAA,oBAAA;;;;gBAIvCvG,EAAA,uBADRqD,EAce9B,EAAA+F,CAAA,GAAA;AAAA;kBAZZ,OAAO/F,EAAAf,CAAA,EAAC,cAAA;AAAA,kBACR,MAAMe,EAAAf,CAAA,EAAC,mBAAA;AAAA,kBACP,OAAOiF,EAAA,QAAelE,EAAAf,CAAA,4BAA4B;AAAA,kBAClD,UAAUR,EAAA;AAAA,gBAAA;6BAEX,MAME;AAAA,oBANFsB,EAMEC,EAAA0G,EAAA,GAAA;AAAA,kCALSzB,EAAA;AAAA,oEAAAA,EAAM,QAAA7E;AAAA,sBACf,UAAS;AAAA,sBACT,MAAA;AAAA,sBACA,cAAW;AAAA,sBACX,cAAW;AAAA,oBAAA;;;;;;;;;;;;;;;;;;;AC9PzB,UAAM5B,IAAQC,GAER,EAAE,SAAAkI,GAAS,SAAAC,GAAS,MAAAxF,EAAA,IAASC,GAAA,GAE7BzC,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3B2H,IAAStF,EAAI,EAAK,GAClBuF,IAASvF,EAA8B,EAAE,GACzCwF,IAAWxF,EAAqB,IAAI,GAEpCyF,IAAOzF,EAAI;AAAA,MACf,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,IAAA,CACZ,GAEK0F,IAAU7H,EAAS,MAAMZ,EAAM,aAAa,IAAI,GAEhD0I,IAAc9H,EAAS,MAAM;AAAA,MACjC,EAAE,OAAO,SAAS,OAAOH,EAAE,YAAY,EAAA;AAAA,MACvC,EAAE,OAAO,QAAQ,OAAOA,EAAE,WAAW,EAAA;AAAA,MACrC,EAAE,OAAO,SAAS,OAAOA,EAAE,YAAY,EAAA;AAAA,IAAE,CAC1C,GAIKkI,IAAgB;AAAA,MACpB,EAAE,OAAO,KAAK,OAAO,MAAA;AAAA,MACrB,EAAE,OAAO,KAAK,OAAO,MAAA;AAAA,IAAM;AAG7B,IAAAC;AAAA,MACE,MAAM5I,EAAM;AAAA,MACZ,CAAC6I,MAAa;AACZ,QAAAL,EAAK,QAAQ;AAAA,UACX,YAAYK,GAAU,cAAc;AAAA,UACpC,SAASA,GAAU,WAAW;AAAA,UAC9B,QAAQA,GAAU,UAAU;AAAA,UAC5B,QAAQA,GAAU,UAAU;AAAA,UAC5B,WAAWA,GAAU,aAAa;AAAA,QAAA,GAEpCP,EAAO,QAAQ,CAAA,GACfC,EAAS,QAAQ,MACZO,EAAA;AAAA,MACP;AAAA,MACA,EAAE,WAAW,GAAA;AAAA,IAAK;AAepB,mBAAeA,IAA0B;AACvC,YAAMC,IAAUP,EAAK,MAAM,QAAQ,KAAA;AAEnC,UAAIA,EAAK,MAAM,eAAe,WAAWO,MAAY,IAAI;AACvD,QAAAR,EAAS,QAAQ;AAEjB;AAAA,MACF;AAEA,UAAI;AACF,QAAAA,EAAS,SAAS,MAAMzF,EAAI,KAAKiG,CAAO,GAAG;AAAA,MAC7C,QAAQ;AAEN,QAAAR,EAAS,QAAQ;AAAA,MACnB;AAAA,IACF;AAEA,aAASS,EAAQrD,GAAmC;AAClD,aAAO2C,EAAO,MAAM3C,CAAK,IAAI,CAAC;AAAA,IAChC;AAEA,mBAAesD,IAAsB;AACnC,MAAAZ,EAAO,QAAQ,IACfC,EAAO,QAAQ,CAAA;AAEf,UAAI;AACF,cAAMY,IAAQlJ,EAAM,WAChB,MAAM8C,EAAI,eAAe9C,EAAM,SAAS,IAAIwI,EAAK,KAAK,IACtD,MAAM1F,EAAI,eAAe0F,EAAK,KAAK;AAEvC,QAAAW,EAAM,QAAQ1I,EAAE,YAAY,CAAC,GAC7B0H,EAAQe,CAAK;AAAA,MACf,SAASE,GAAO;AACd,cAAMhH,IAAQgH,EAA6E;AAE3F,QAAIhH,GAAM,UACRkG,EAAO,QAAQlG,EAAK,QACpB+G,EAAM,OAAO1I,EAAE,aAAa,CAAC,KAE7B0I,EAAM,OAAO/G,GAAM,WAAW3B,EAAE,aAAa,CAAC;AAAA,MAElD,UAAA;AACE,QAAA4H,EAAO,QAAQ;AAAA,MACjB;AAAA,IACF;2BAIE/E,EAsDY9B,EAAA+B,EAAA,GAAA;AAAA,MAtDO,MAAM/B,EAAAoB,CAAA;AAAA,qDAAAA,EAAI,QAAAhB,IAAA;AAAA,MAAG,OAAOJ,EAAAf,CAAA,EAAC,eAAA;AAAA,MAAoB,OAAO;AAAA,IAAA;MA8CtD,UACT,MAKW;AAAA,QALXc,EAKWC,EAAAE,CAAA,GAAA,EALD,MAAK,QAAI;AAAA,qBACjB,MAAkF;AAAA,YAAlFH,EAAkFC,EAAAK,CAAA,GAAA;AAAA,cAAvE,SAAQ;AAAA,cAAW,gCAAOL,EAAA4G,CAAA,EAAA;AAAA,YAAO;yBAAI,MAAsB;AAAA,oBAAnB5G,EAAAf,CAAA,EAAC,aAAA,CAAA,GAAA,CAAA;AAAA,cAAA;;;YACpDc,EAEYC,EAAAK,CAAA,GAAA;AAAA,cAFD,MAAK;AAAA,cAAW,SAASwG,EAAA;AAAA,cAAS,SAAOY;AAAA,YAAA;yBAClD,MAAuD;AAAA,oBAApDR,EAAA,QAAUjH,EAAAf,CAAA,EAAC,WAAA,IAAgBe,EAAAf,CAAA,EAAC,mBAAA,CAAA,GAAA,CAAA;AAAA,cAAA;;;;;;;iBAjDrC,MA2CM;AAAA,QA3CNY,EA2CM,OA3CND,IA2CM;AAAA,UA1CJG,EAEeC,EAAA+F,CAAA,GAAA;AAAA,YAFA,OAAO/F,EAAAf,CAAA,EAAC,WAAA;AAAA,UAAA;uBACrB,MAAiF;AAAA,cAAjFc,EAAiFC,EAAA6H,EAAA,GAAA;AAAA,gBAA7D,YAAAb,EAAA,MAAK;AAAA,gBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,aAAU5G;AAAA,gBAAG,SAAS8G,EAAA;AAAA,gBAAc,UAAQI;AAAA,cAAA;;;;UAGvEvH,EAMeC,EAAA+F,CAAA,GAAA;AAAA,YALZ,OAAO/F,EAAAf,CAAA,EAAC,cAAA;AAAA,YACR,MAAMe,EAAAf,CAAA,EAAC,mBAAA;AAAA,YACP,OAAOuI,EAAO,SAAA;AAAA,UAAA;uBAEf,MAA+E;AAAA,cAA/EzH,EAA+EC,EAAAgC,CAAA,GAAA;AAAA,gBAA5D,YAAAgF,EAAA,MAAK;AAAA,gBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,UAAO5G;AAAA,gBAAE,aAAY;AAAA,gBAAgB,QAAMkH;AAAA,cAAA;;;;UAM7DP,EAAA,cADRjF,EASE9B,EAAAkC,CAAA,GAAA;AAAA;YAPA,MAAK;AAAA,YACL,SAAQ;AAAA,YACP,aAAwB6E,EAAA,MAAS,SAAI,UAA2B/G,EAAAf,CAAA,mCAAmC8H,EAAA,MAAS,UAAUA,EAAA,MAAS,KAAA,CAAI,IAAkB/G,EAAAf,CAAA,EAAC,iBAAA,EAAA,MAA0B8H,EAAA,MAAS,KAAA,CAAI;AAAA,UAAA;UAOhMhH,EAMeC,EAAA+F,CAAA,GAAA;AAAA,YALZ,OAAO/F,EAAAf,CAAA,EAAC,aAAA;AAAA,YACR,MAAMe,EAAAf,CAAA,EAAC,kBAAA;AAAA,YACP,OAAOuI,EAAO,QAAA;AAAA,UAAA;uBAEf,MAA6D;AAAA,cAA7DzH,EAA6DC,EAAAgC,CAAA,GAAA;AAAA,gBAA1C,YAAAgF,EAAA,MAAK;AAAA,gBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,SAAM5G;AAAA,gBAAE,aAAY;AAAA,cAAA;;;;UAG9CP,EAQM,OARNC,IAQM;AAAA,YAPJC,EAEeC,EAAA+F,CAAA,GAAA;AAAA,cAFA,OAAO/F,EAAAf,CAAA,EAAC,aAAA;AAAA,YAAA;yBACrB,MAA4D;AAAA,gBAA5Dc,EAA4DC,EAAA6H,EAAA,GAAA;AAAA,kBAAxC,YAAAb,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,SAAM5G;AAAA,kBAAG,SAAS+G;AAAA,gBAAA;;;;YAG7CpH,EAEeC,EAAA+F,CAAA,GAAA;AAAA,cAFA,OAAO/F,EAAAf,CAAA,EAAC,YAAA;AAAA,YAAA;yBACrB,MAAsC;AAAA,gBAAtCc,EAAsCC,EAAA+H,EAAA,GAAA;AAAA,kBAAlB,YAAAf,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,YAAS5G;AAAA,gBAAA;;;;;;;;;;;;;;;;ACjJ5C,UAAM5B,IAAQC,GAERG,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3B2B,IAAOU,EAAiC,IAAI,GAC5CuB,IAAUvB,EAAI,EAAK;AAEzB,QAAIyG,IAAmB,EAAE,MAAM,GAAG,SAAS,IAAI,MAAM,MAAM,QAAQ,GAAA;AAEnE,UAAMC,IAAYrJ,EAAQ,IAAI,YAAY,GAEpCsJ,IAAOlF,EAA2DmF,EAAiB,GACnFpF,IAAOC,EAAyCC,EAAa,GAE7DE,IAAU/D,EAAqC,MAAM;AAAA,MACzD,EAAE,KAAK,WAAW,OAAOH,EAAE,cAAc,GAAG,UAAU,GAAA;AAAA,MACtD,EAAE,KAAK,UAAU,OAAOA,EAAE,aAAa,EAAA;AAAA,MACvC,EAAE,KAAK,UAAU,OAAOA,EAAE,aAAa,GAAG,OAAO,UAAU,WAAW,IAAA;AAAA,MACtE,EAAE,KAAK,QAAQ,OAAOA,EAAE,WAAW,GAAG,OAAO,UAAU,UAAU,IAAM,WAAW,IAAA;AAAA,MAClF;AAAA,QACE,KAAK;AAAA,QACL,OAAOA,EAAE,eAAe;AAAA,QACxB,UAAU;AAAA,QACV,WAAW;AAAA,QACX,aAAa;AAAA,MAAA;AAAA,MAEf,EAAE,KAAK,aAAa,OAAOA,EAAE,YAAY,GAAG,OAAO,UAAU,WAAW,IAAA;AAAA,MACxE;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP,QAAQ,CAACgJ;AAAA,QACT,aAAa;AAAA,MAAA;AAAA,IACf,CACD;AAED,mBAAe7E,EAAKC,GAAkC;AACpD,MAAA2E,IAAO3E,GACPP,EAAQ,QAAQ;AAEhB,UAAI;AACF,QAAAjC,EAAK,QAAQ,MAAMS,EAAI,UAAU;AAAA,UAC/B,GAAG+B,EAAM;AAAA,UACT,MAAMA,EAAM,OAAO,GAAGA,EAAM,KAAK,UAAU,SAAS,MAAM,EAAE,GAAGA,EAAM,KAAK,GAAG,KAAK;AAAA,UAClF,MAAMA,EAAM;AAAA,UACZ,UAAUA,EAAM;AAAA,QAAA,CACjB;AAAA,MACH,UAAA;AACE,QAAAP,EAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;AAEA,mBAAe1B,EAAKiG,GAA6C;AAG/D,MAFc,MAAMa,EAAK,EAAE,UAAAb,GAAU,KAErBjE,EAAK4E,CAAI;AAAA,IAC3B;AAEA,mBAAeI,EAAOf,GAAsC;AAS1D,UARe,MAAMgB,GAAQ;AAAA,QAC3B,OAAOpJ,EAAE,qBAAqB,EAAE,SAASoI,EAAS,SAAS;AAAA,QAC3D,SAASpI,EAAE,kBAAkB;AAAA,QAC7B,aAAaA,EAAE,aAAa;AAAA,QAC5B,YAAYA,EAAE,aAAa;AAAA,QAC3B,MAAM;AAAA,MAAA,CACP;AAID,YAAI;AACF,gBAAMqC,EAAI,eAAe+F,EAAS,EAAE,GACpCM,EAAM,QAAQ1I,EAAE,cAAc,CAAC,GAC1BmE,EAAK4E,CAAI;AAAA,QAChB,SAASJ,GAAO;AACd,UAAAD,EAAM,OAAQC,EAA0C,MAAM,WAAW,OAAOA,CAAK,CAAC;AAAA,QACxF;AAAA,IACF;2BAIE9F,EA2DawB,IAAA;AAAA,MA3DA,MAAM9E,EAAM;AAAA,MAAM,SAAQ;AAAA,MAAa,+BAAMwB,EAAA+C,CAAA,EAAI,CAAA,CAAA;AAAA,IAAA;iBAC5D,MAyDU;AAAA,QAzDVhD,EAyDUC,EAAAuD,EAAA,GAAA,MAAA+E,GAAA;AAAA,UAxDG,UAAO,MAAyB;AAAA,gBAAtBtI,EAAAf,CAAA,EAAC,gBAAA,CAAA,GAAA,CAAA;AAAA,UAAA;qBAQtB,MA+CW;AAAA,YA/CXc,EA+CWC,EAAAwD,EAAA,GAAA;AAAA,cA9CR,MAAM3C,EAAA;AAAA,cACN,SAASsC,EAAA;AAAA,cACV,WAAQ;AAAA,cACR,YAAA;AAAA,cACA,OAAA;AAAA,cACA,OAAA;AAAA,cACC,SAASL,EAAA;AAAA,cACT,sBAAoB9C,EAAAf,CAAA,EAAC,uBAAA;AAAA,cACrB,cAAYe,EAAAf,CAAA,EAAC,YAAA;AAAA,cACb,eAAcmE;AAAA,cACd,mCAAWpD,EAAAiI,CAAA,IAAY7G,EAAKhB,CAAM,IAAI;AAAA,YAAA;cAE5B,gBAAYqD,EACrB,CAAmD,EAD1B,KAAAC,QAAG;AAAA,gBAC5B3D,EAAmDC,EAAAmC,CAAA,GAAA;AAAA,kBAA1C,MAAA;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BAAK,MAAiB;AAAA,oBAAdE,EAAAC,EAAAoB,EAAI,OAAO,GAAA,CAAA;AAAA,kBAAA;;;gBAGtBA,EAAI,gBAApB5B,EAA2E9B,EAAAoC,CAAA,GAAA;AAAA;kBAA9C,MAAK;AAAA,gBAAA;6BAAU,MAAoB;AAAA,wBAAjBpC,EAAAf,CAAA,EAAC,WAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAGvC,eAAWwE,EACpB,CAAkD,EAD1B,KAAAC,QAAG;AAAA,gBAC3B3D,EAAkDC,EAAAmC,CAAA,GAAA;AAAA,kBAAzC,MAAA;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BAAK,MAAgB;AAAA,oBAAbE,EAAAC,EAAAoB,EAAI,MAAM,GAAA,CAAA;AAAA,kBAAA;;;;cAG5B,oBAAgBD,EACzB,CAEU,EAHmB,KAAAC,QAAG;AAAA,gBAChC3D,EAEUC,EAAAmC,CAAA,GAAA;AAAA,kBAFD,MAAK;AAAA,kBAAM,MAAMuB,EAAI,cAAW,YAAA;AAAA,gBAAA;6BACvC,MAAoF;AAAA,wBAAjFA,EAAI,cAAW,IAAO,KAAKA,EAAI,WAAW,EAAE,eAAA,IAAmB1D,EAAAf,CAAA,EAAC,YAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAI5D,kBAAcwE,EACvB,CAEW,EAHgB,KAAAC,QAAG;AAAA,gBAC9B3D,EAEWC,EAAAoC,CAAA,GAAA;AAAA,kBAFA,MAAMsB,EAAI,YAAS,YAAA;AAAA,kBAA0B,KAAA;AAAA,gBAAA;6BACtD,MAA2D;AAAA,oBAAxDrB,EAAAC,EAAAoB,EAAI,YAAY1D,EAAAf,CAAA,mBAAmBe,EAAAf,CAAA,EAAC,eAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAIhC,gBAAYwE,EACrB,CAEa,EAHY,KAAAC,QAAG;AAAA,gBACV1D,EAAAiI,CAAA,UAAlBnG,EAEa9B,EAAAuI,EAAA,GAAA;AAAA;kBAFgB,MAAK;AAAA,kBAAM,4BAAD,MAAA;AAAA,kBAAA,GAAW,CAAA,MAAA,CAAA;AAAA,gBAAA;6BAChD,MAA0E;AAAA,oBAA1ExI,EAA0EC,EAAAwI,EAAA,GAAA;AAAA,sBAA/D,MAAK;AAAA,sBAAU,OAAOxI,EAAAf,CAAA,EAAC,aAAA;AAAA,sBAAkB,SAAK,CAAAmB,MAAEgI,EAAO1E,CAAG;AAAA,oBAAA;;;;;cAI9D,gBAAYD,EACrB,CAEa,EAHY,KAAAC,QAAG;AAAA,gBAC5B3D,EAEaC,EAAAuI,EAAA,GAAA;AAAA,kBAFD,MAAK;AAAA,kBAAM,4BAAD,MAAA;AAAA,kBAAA,GAAW,CAAA,MAAA,CAAA;AAAA,gBAAA;6BAC/B,MAA0E;AAAA,oBAA1ExI,EAA0EC,EAAAwI,EAAA,GAAA;AAAA,sBAA/D,MAAK;AAAA,sBAAU,OAAOxI,EAAAf,CAAA,EAAC,aAAA;AAAA,sBAAkB,SAAK,CAAAmB,MAAEgI,EAAO1E,CAAG;AAAA,oBAAA;;;;;;;;;;UAlD3D1D,EAAAiI,CAAA;kBAAY;AAAA,kBAC1B,MAEY;AAAA,cAFZlI,EAEYC,EAAAK,CAAA,GAAA;AAAA,gBAFD,MAAK;AAAA,gBAAU,MAAK;AAAA,gBAAO,gCAAOe,EAAI,IAAA;AAAA,cAAA;2BAC/C,MAA4B;AAAA,sBAAzBpB,EAAAf,CAAA,EAAC,mBAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;;;;;;;;;ICrHRwJ,KAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAASC,GAAUC,GAAkBxH,GAAyB;AACnE,SAAOkE,GAAesD,EAAK,OAAqCxH,CAAM;AACxE;AAGO,SAASyH,GAAMD,GAAmC;AACvD,MAAI,CAACA,EAAM,QAAO,CAAA;AAElB,QAAM9E,IAAkB,CAAA;AAExB,aAAWO,KAAOqE,IAAY;AAC5B,UAAM7G,IAAM+G,EAAKvE,CAAG;AAEpB,IAAIxC,KAAQ,QACV,OAAO,OAAOiC,GAAO,EAAE,CAACO,CAAG,GAAGxC,GAAK;AAAA,EAEvC;AAEA,SAAOiC;AACT;AAQO,SAASgF,GACdC,GACAnH,GACa;AACb,QAAMV,IAAqB,EAAE,GAAG6H,EAAA;AAEhC,aAAW1E,KAAOqE;AAChB,WAAO,OAAOxH,GAAO,EAAE,CAACmD,CAAG,GAAGzC,EAAIyC,CAAG,KAAK,MAAM;AAGlD,SAAOnD;AACT;;;;;;;;AChCA,UAAMzC,IAAQC,GAKR,EAAE,SAAAkI,GAAS,SAAAC,GAAS,MAAAxF,EAAA,IAASC,GAAA,GAE7BzC,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3B2H,IAAStF,EAAI,EAAK,GAClBuF,IAASvF,EAA8B,EAAE,GAEzCyF,IAAOzF,EAAI;AAAA,MACf,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW;AAAA,IAAA,CACZ,GAEKI,IAAMJ,EAAc,EAAE,GAEtB0F,IAAU7H,EAAS,MAAMZ,EAAM,SAAS,IAAI,GAE5C0I,IAAc9H,EAAS,MAAM;AAAA,MACjC,EAAE,OAAO,SAAS,OAAOH,EAAE,YAAY,EAAA;AAAA,MACvC,EAAE,OAAO,QAAQ,OAAOA,EAAE,WAAW,EAAA;AAAA,MACrC,EAAE,OAAO,SAAS,OAAOA,EAAE,YAAY,EAAA;AAAA,IAAE,CAC1C;AAED,IAAAmI;AAAA,MACE,MAAM5I,EAAM;AAAA,MACZ,CAACmK,MAAS;AACR,QAAA3B,EAAK,QAAQ;AAAA,UACX,YAAY2B,GAAM,cAAc;AAAA,UAChC,SAASA,GAAM,WAAW;AAAA,UAC1B,UAAUA,GAAM,YAAY;AAAA,UAC5B,WAAWA,GAAM,aAAa;AAAA,QAAA,GAEhChH,EAAI,QAAQiH,GAAMD,CAAI,GACtB7B,EAAO,QAAQ,CAAA;AAAA,MACjB;AAAA,MACA,EAAE,WAAW,GAAA;AAAA,IAAK;AAGpB,aAASU,EAAQrD,GAAmC;AAClD,aAAO2C,EAAO,MAAM3C,CAAK,IAAI,CAAC;AAAA,IAChC;AAEA,mBAAesD,IAAsB;AACnC,MAAAZ,EAAO,QAAQ,IACfC,EAAO,QAAQ,CAAA;AAEf,YAAM7F,IAAQ4H,GAAU7B,EAAK,OAAOrF,EAAI,KAAK;AAE7C,UAAI;AACF,cAAM+F,IAAQlJ,EAAM,OAChB,MAAM8C,EAAI,UAAU9C,EAAM,KAAK,IAAIyC,CAAK,IACxC,MAAMK,EAAI,UAAUL,CAAK;AAE7B,QAAA0G,EAAM,QAAQ1I,EAAE,YAAY,CAAC,GAC7B0H,EAAQe,CAAK;AAAA,MACf,SAASE,GAAO;AACd,cAAMhH,IAAQgH,EAA6E;AAE3F,QAAIhH,GAAM,UACRkG,EAAO,QAAQlG,EAAK,QACpB+G,EAAM,OAAO1I,EAAE,aAAa,CAAC,KAE7B0I,EAAM,OAAO/G,GAAM,WAAW3B,EAAE,aAAa,CAAC;AAAA,MAElD,UAAA;AACE,QAAA4H,EAAO,QAAQ;AAAA,MACjB;AAAA,IACF;2BAIE/E,EAoCY9B,EAAA+B,EAAA,GAAA;AAAA,MApCO,MAAM/B,EAAAoB,CAAA;AAAA,qDAAAA,EAAI,QAAAhB,IAAA;AAAA,MAAG,OAAOJ,EAAAf,CAAA,EAAC,WAAA;AAAA,MAAgB,OAAO;AAAA,IAAA;MA4BlD,UACT,MAKW;AAAA,QALXc,EAKWC,EAAAE,CAAA,GAAA,EALD,MAAK,QAAI;AAAA,qBACjB,MAAkF;AAAA,YAAlFH,EAAkFC,EAAAK,CAAA,GAAA;AAAA,cAAvE,SAAQ;AAAA,cAAW,gCAAOL,EAAA4G,CAAA,EAAA;AAAA,YAAO;yBAAI,MAAsB;AAAA,oBAAnB5G,EAAAf,CAAA,EAAC,aAAA,CAAA,GAAA,CAAA;AAAA,cAAA;;;YACpDc,EAEYC,EAAAK,CAAA,GAAA;AAAA,cAFD,MAAK;AAAA,cAAW,SAASwG,EAAA;AAAA,cAAS,SAAOY;AAAA,YAAA;yBAClD,MAAmD;AAAA,oBAAhDR,EAAA,QAAUjH,EAAAf,CAAA,EAAC,WAAA,IAAgBe,EAAAf,CAAA,EAAC,eAAA,CAAA,GAAA,CAAA;AAAA,cAAA;;;;;;;iBA/BrC,MAyBM;AAAA,QAzBNY,EAyBM,OAzBND,IAyBM;AAAA,UAxBJC,EAqBM,OArBNC,IAqBM;AAAA,YApBJC,EAEeC,EAAA+F,CAAA,GAAA;AAAA,cAFA,OAAO/F,EAAAf,CAAA,EAAC,WAAA;AAAA,YAAA;yBACrB,MAA8D;AAAA,gBAA9Dc,EAA8DC,EAAA6H,EAAA,GAAA;AAAA,kBAA1C,YAAAb,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,aAAU5G;AAAA,kBAAG,SAAS8G,EAAA;AAAA,gBAAA;;;;YAGjDnH,EAOeC,EAAA+F,CAAA,GAAA;AAAA,cANZ,OAAO/F,EAAAf,CAAA,EAAC,cAAA;AAAA,cACR,MAAMe,EAAAf,CAAA,EAAC,mBAAA;AAAA,cACP,OAAOuI,EAAO,SAAA;AAAA,cACf,OAAM;AAAA,YAAA;yBAEN,MAAgE;AAAA,gBAAhEzH,EAAgEC,EAAAgC,CAAA,GAAA;AAAA,kBAA7C,YAAAgF,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,UAAO5G;AAAA,kBAAE,aAAY;AAAA,gBAAA;;;;YAG/CL,EAEeC,EAAA+F,CAAA,GAAA;AAAA,cAFA,OAAO/F,EAAAf,CAAA,EAAC,eAAA;AAAA,cAAoB,MAAMe,EAAAf,CAAA,EAAC,oBAAA;AAAA,YAAA;yBAChD,MAAsD;AAAA,gBAAtDc,EAAsDC,EAAA+I,EAAA,GAAA;AAAA,kBAA5B,YAAA/B,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,WAAQ5G;AAAA,kBAAG,MAAM;AAAA,gBAAA;;;;YAGlDL,EAEeC,EAAA+F,CAAA,GAAA;AAAA,cAFA,OAAO/F,EAAAf,CAAA,EAAC,YAAA;AAAA,YAAA;yBACrB,MAAsC;AAAA,gBAAtCc,EAAsCC,EAAA+H,EAAA,GAAA;AAAA,kBAAlB,YAAAf,EAAA,MAAK;AAAA,kBAAL,uBAAAc,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA,CAAA1H,MAAA4G,EAAA,MAAK,YAAS5G;AAAA,gBAAA;;;;;UAItCL,EAA0DiJ,IAAA;AAAA,wBAAvCrH,EAAA;AAAA,0DAAAA,EAAG,QAAAvB;AAAA,YAAG,eAAa5B,EAAM;AAAA,UAAA;;;;;;;;;;;;;ACtGlD,UAAMA,IAAQC,GAKRG,IAAUL,EAAA,GACV+C,IAAMd,EAAa5B,CAAO;AAChC,IAAAP,EAAA;AAEA,UAAMY,IAAIC,EAAa,UAAU,GAE3B2B,IAAOU,EAAgC,IAAI,GAC3CuB,IAAUvB,EAAI,EAAK,GAGnB0H,IAAO1H,EAAI,KAAK;AAEtB,QAAIyG,IAAmB,EAAE,MAAM,GAAG,SAAS,IAAI,MAAM,MAAM,QAAQ,GAAA;AAEnE,UAAMC,IAAYrJ,EAAQ,IAAI,YAAY,GAEpCsJ,IAAOlF;AAAA,MACXkG;AAAA,IAAA,GAEInG,IAAOC,EAAyCC,EAAa,GAE7DiE,IAAc9H,EAAS,MAAM;AAAA,MACjC,EAAE,OAAO,OAAO,OAAOH,EAAE,eAAe,EAAA;AAAA,MACxC,EAAE,OAAO,SAAS,OAAOA,EAAE,YAAY,EAAA;AAAA,MACvC,EAAE,OAAO,QAAQ,OAAOA,EAAE,WAAW,EAAA;AAAA,MACrC,EAAE,OAAO,SAAS,OAAOA,EAAE,YAAY,EAAA;AAAA,IAAE,CAC1C,GAEKkE,IAAU/D,EAAoC,MAAM;AAAA,MACxD,EAAE,KAAK,WAAW,OAAOH,EAAE,cAAc,EAAA;AAAA,MACzC,EAAE,KAAK,cAAc,OAAOA,EAAE,WAAW,GAAG,WAAW,IAAA;AAAA,MACvD,EAAE,KAAK,SAAS,OAAOA,EAAE,YAAY,GAAG,WAAW,IAAA;AAAA,MACnD,EAAE,KAAK,YAAY,OAAOA,EAAE,eAAe,GAAG,OAAO,UAAU,WAAW,IAAA;AAAA,MAC1E,EAAE,KAAK,aAAa,OAAOA,EAAE,YAAY,GAAG,OAAO,UAAU,WAAW,IAAA;AAAA,MACxE;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP,QAAQ,CAACgJ;AAAA,QACT,aAAa;AAAA,MAAA;AAAA,IACf,CACD;AAED,mBAAe7E,EAAKC,GAAkC;AACpD,MAAA2E,IAAO3E,GACPP,EAAQ,QAAQ;AAEhB,UAAI;AACF,QAAAjC,EAAK,QAAQ,MAAMS,EAAI,KAAK;AAAA,UAC1B,GAAG+B,EAAM;AAAA,UACT,YAAY4F,EAAK,UAAU,QAAQ,OAAQA,EAAK;AAAA,UAChD,MAAM5F,EAAM;AAAA,UACZ,UAAUA,EAAM;AAAA,QAAA,CACjB;AAAA,MACH,UAAA;AACE,QAAAP,EAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;AAEA,mBAAe1B,EAAKuH,GAAwC;AAG1D,MAFc,MAAMT,EAAK,EAAE,MAAAS,GAAM,YAAYnK,EAAM,YAAY,KAE/C4E,EAAK4E,CAAI;AAAA,IAC3B;AAEA,mBAAeI,EAAOO,GAAiC;AASrD,UARe,MAAMN,GAAQ;AAAA,QAC3B,OAAOpJ,EAAE,qBAAqB,EAAE,SAAS0J,EAAK,SAAS;AAAA,QACvD,SAAS1J,EAAE,kBAAkB;AAAA,QAC7B,aAAaA,EAAE,aAAa;AAAA,QAC5B,YAAYA,EAAE,aAAa;AAAA,QAC3B,MAAM;AAAA,MAAA,CACP;AAID,YAAI;AACF,gBAAMqC,EAAI,UAAUqH,EAAK,EAAE,GAC3BhB,EAAM,QAAQ1I,EAAE,cAAc,CAAC,GAC1BmE,EAAK4E,CAAI;AAAA,QAChB,SAASJ,GAAO;AACd,UAAAD,EAAM,OAAQC,EAA0C,MAAM,WAAW,OAAOA,CAAK,CAAC;AAAA,QACxF;AAAA,IACF;2BAIE9F,EA4DawB,IAAA;AAAA,MA5DA,MAAM9E,EAAM;AAAA,MAAM,SAAQ;AAAA,MAAS,+BAAMwB,EAAA+C,CAAA,EAAI,CAAA,CAAA;AAAA,IAAA;iBACxD,MA0DU;AAAA,QA1DVhD,EA0DUC,EAAAuD,EAAA,GAAA,MAAA+E,GAAA;AAAA,UAzDG,UAAO,MAAqB;AAAA,gBAAlBtI,EAAAf,CAAA,EAAC,YAAA,CAAA,GAAA,CAAA;AAAA,UAAA;qBAQtB,MAgDW;AAAA,YAhDXc,EAgDWC,EAAAwD,EAAA,GAAA;AAAA,cA/CR,MAAM3C,EAAA;AAAA,cACN,SAASsC,EAAA;AAAA,cACV,WAAQ;AAAA,cACR,YAAA;AAAA,cACA,OAAA;AAAA,cACA,OAAA;AAAA,cACC,SAASL,EAAA;AAAA,cACT,sBAAoB9C,EAAAf,CAAA,EAAC,mBAAA;AAAA,cACrB,cAAYe,EAAAf,CAAA,EAAC,YAAA;AAAA,cACb,eAAcmE;AAAA,cACd,mCAAWpD,EAAAiI,CAAA,IAAY7G,EAAKhB,CAAM,IAAI;AAAA,YAAA;cAE5B,WACT,MAA6E;AAAA,gBAA7EL,EAA6EC,EAAA6H,EAAA,GAAA;AAAA,8BAAzDoB,EAAA;AAAA,gEAAAA,EAAI,QAAA7I;AAAA,kBAAG,SAAS8G,EAAA;AAAA,kBAAa,OAAM;AAAA,gBAAA;;cAG9C,gBAAYzD,EACrB,CAAmD,EAD1B,KAAAC,QAAG;AAAA,gBAC5B3D,EAAmDC,EAAAmC,CAAA,GAAA;AAAA,kBAA1C,MAAA;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BAAK,MAAiB;AAAA,oBAAdE,EAAAC,EAAAoB,EAAI,OAAO,GAAA,CAAA;AAAA,kBAAA;;;;cAG7B,mBAAeD,EACxB,CAAsD,EAD1B,KAAAC,QAAG;AAAA,gBAC/B3D,EAAsDC,EAAAoC,CAAA,GAAA,MAAA;AAAA,6BAA5C,MAAiC;AAAA,wBAA9BpC,EAAAf,CAAA,EAAC,QAASyE,EAAI,UAAU,EAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAG5B,cAAUD,EACnB,CAAuE,EADhD,KAAAC,QAAG;AAAA,gBACX1D,EAAA0I,EAAA,EAAUhF,CAAG,UAA5B5B,EAAuE9B,EAAAmC,CAAA,GAAA;AAAA;kBAAxC,MAAK;AAAA,gBAAA;6BAAK,MAAoB;AAAA,oBAAjBE,EAAAC,EAAAtC,EAAA0I,EAAA,EAAUhF,CAAG,CAAA,GAAA,CAAA;AAAA,kBAAA;;kCACzD5B,EAAkD9B,EAAAmC,CAAA,GAAA;AAAA;kBAAlC,MAAK;AAAA,kBAAK,MAAK;AAAA,gBAAA;6BAAQ,MAAC,CAAA,GAAA2F,EAAA,CAAA,MAAAA,EAAA,CAAA,IAAA;AAAA,sBAAD,KAAC,EAAA;AAAA,kBAAA;;;;cAG/B,kBAAcrE,EACvB,CAEW,EAHgB,KAAAC,QAAG;AAAA,gBAC9B3D,EAEWC,EAAAoC,CAAA,GAAA;AAAA,kBAFA,MAAMsB,EAAI,YAAS,YAAA;AAAA,kBAA0B,KAAA;AAAA,gBAAA;6BACtD,MAA2D;AAAA,oBAAxDrB,EAAAC,EAAAoB,EAAI,YAAY1D,EAAAf,CAAA,mBAAmBe,EAAAf,CAAA,EAAC,eAAA,CAAA,GAAA,CAAA;AAAA,kBAAA;;;;cAIhC,gBAAYwE,EACrB,CAEa,EAHY,KAAAC,QAAG;AAAA,gBACV1D,EAAAiI,CAAA,UAAlBnG,EAEa9B,EAAAuI,EAAA,GAAA;AAAA;kBAFgB,MAAK;AAAA,kBAAM,4BAAD,MAAA;AAAA,kBAAA,GAAW,CAAA,MAAA,CAAA;AAAA,gBAAA;6BAChD,MAA0E;AAAA,oBAA1ExI,EAA0EC,EAAAwI,EAAA,GAAA;AAAA,sBAA/D,MAAK;AAAA,sBAAU,OAAOxI,EAAAf,CAAA,EAAC,aAAA;AAAA,sBAAkB,SAAK,CAAAmB,MAAEgI,EAAO1E,CAAG;AAAA,oBAAA;;;;;cAI9D,gBAAYD,EAErB,CAEa,EAJY,KAAAC,QAAG;AAAA,gBAE5B3D,EAEaC,EAAAuI,EAAA,GAAA;AAAA,kBAFD,MAAK;AAAA,kBAAM,4BAAD,MAAA;AAAA,kBAAA,GAAW,CAAA,MAAA,CAAA;AAAA,gBAAA;6BAC/B,MAA0E;AAAA,oBAA1ExI,EAA0EC,EAAAwI,EAAA,GAAA;AAAA,sBAA/D,MAAK;AAAA,sBAAU,OAAOxI,EAAAf,CAAA,EAAC,aAAA;AAAA,sBAAkB,SAAK,CAAAmB,MAAEgI,EAAO1E,CAAG;AAAA,oBAAA;;;;;;;;;;UAnD3D1D,EAAAiI,CAAA;kBAAY;AAAA,kBAC1B,MAEY;AAAA,cAFZlI,EAEYC,EAAAK,CAAA,GAAA;AAAA,gBAFD,MAAK;AAAA,gBAAU,MAAK;AAAA,gBAAO,gCAAOe,EAAI,IAAA;AAAA,cAAA;2BAC/C,MAAwB;AAAA,sBAArBpB,EAAAf,CAAA,EAAC,eAAA,CAAA,GAAA,CAAA;AAAA,gBAAA;;;;;;;;;;;;AC1GP,SAAS0C,GAAIrC,IAAsB,IAAiB;AACzD,QAAMG,IAAOH,EAAQ,QAAQ,QAGvB6J,IAAa7J,EAAQ,aAAa8J,GAAQ9J,EAAQ,UAAU,IAAI;AAEtE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAAG;AAAA,IACA,QAAQ;AAAA;AAAA;AAAA,MAGN;AAAA,QACE,MAAAA;AAAA,QACA,MAAM;AAAA,QACN,WAAW4J;AAAA,QACX,OAAO,EAAE,MAAM5J,GAAM,YAAA0J,EAAA;AAAA,MAAW;AAAA,MAElC;AAAA,QACE,MAAM,GAAG1J,CAAI;AAAA,QACb,MAAM;AAAA,QACN,WAAW6J;AAAAA,QACX,OAAO,EAAE,MAAM7J,EAAA;AAAA,MAAK;AAAA;AAAA;AAAA;AAAA,MAKtB;AAAA,QACE,MAAM,GAAGA,CAAI;AAAA,QACb,MAAM;AAAA,QACN,WAAW8J;AAAA,QACX,OAAO,EAAE,MAAM9J,EAAA;AAAA,MAAK;AAAA,IACtB;AAAA;AAAA;AAAA,IAIF,OAAO;AAAA,MACL,UAAU;AAAA,QACR,WAAWuJ;AAAA,QACX,MAAM;AAAA,QACN,MAAM,OAAO,EAAE,YAAAG,EAAA;AAAA,MAAW;AAAA,IAC5B;AAAA,EACF;AAEJ;"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-seo[data-v-b7ea284e]{container-type:inline-size}.wx-seo__fields[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-12);padding-block-start:var(--wx-space-12)}.wx-seo__count[data-v-b7ea284e]{display:flex;justify-content:flex-end}.wx-seo__preview[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-4)}.wx-seo__snippet[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-2);padding:var(--wx-space-12);border:1px solid var(--wx-color-border);border-radius:var(--wx-radius-md);background:var(--wx-color-surface-sunken)}.wx-seo__snippet-address[data-v-b7ea284e]{color:var(--wx-color-text-muted);font-size:var(--wx-font-size-sm);overflow-wrap:anywhere}.wx-seo__snippet-title[data-v-b7ea284e]{color:var(--wx-color-primary);font-size:var(--wx-font-size-lg);overflow-wrap:anywhere}.wx-seo__snippet-text[data-v-b7ea284e]{color:var(--wx-color-text);font-size:var(--wx-font-size-sm);overflow-wrap:anywhere}.wx-seo-
|
|
1
|
+
.wx-seo-layout[data-v-f68ce04b]{display:flex;flex-direction:column;gap:var(--wx-space-16);container-type:inline-size}.wx-seo-layout__head[data-v-f68ce04b]{display:flex;align-items:center;justify-content:space-between;gap:var(--wx-space-12);flex-wrap:wrap}@container (max-width: 560px){.wx-seo-layout__head[data-v-f68ce04b]>*{flex:1 1 100%}}.wx-seo-test[data-v-116e47cd]{display:flex;flex-direction:column;gap:var(--wx-space-16)}.wx-seo-test__ask[data-v-116e47cd]{width:100%}.wx-seo-test__input[data-v-116e47cd]{flex:1 1 auto}.wx-seo-test__matched[data-v-116e47cd],.wx-seo-test__result[data-v-116e47cd],.wx-seo-test__chain[data-v-116e47cd]{display:flex;flex-direction:column;gap:var(--wx-space-6)}.wx-seo-aliases__note[data-v-11e0403c]{margin-bottom:var(--wx-space-12)}.wx-seo[data-v-b7ea284e]{container-type:inline-size}.wx-seo__fields[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-12);padding-block-start:var(--wx-space-12)}.wx-seo__count[data-v-b7ea284e]{display:flex;justify-content:flex-end}.wx-seo__preview[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-4)}.wx-seo__snippet[data-v-b7ea284e]{display:flex;flex-direction:column;gap:var(--wx-space-2);padding:var(--wx-space-12);border:1px solid var(--wx-color-border);border-radius:var(--wx-radius-md);background:var(--wx-color-surface-sunken)}.wx-seo__snippet-address[data-v-b7ea284e]{color:var(--wx-color-text-muted);font-size:var(--wx-font-size-sm);overflow-wrap:anywhere}.wx-seo__snippet-title[data-v-b7ea284e]{color:var(--wx-color-primary);font-size:var(--wx-font-size-lg);overflow-wrap:anywhere}.wx-seo__snippet-text[data-v-b7ea284e]{color:var(--wx-color-text);font-size:var(--wx-font-size-sm);overflow-wrap:anywhere}.wx-seo-redirect[data-v-fabd0f11]{display:flex;flex-direction:column;gap:var(--wx-space-12)}.wx-seo-redirect__row[data-v-fabd0f11]{display:grid;grid-template-columns:140px 1fr;gap:var(--wx-space-12);align-items:start}.wx-seo-rule[data-v-fd7826d1]{display:flex;flex-direction:column;gap:var(--wx-space-16);container-type:inline-size}.wx-seo-rule__where[data-v-fd7826d1]{display:grid;grid-template-columns:180px 1fr 140px 100px;gap:var(--wx-space-12);align-items:start}@container (max-width: 720px){.wx-seo-rule__where[data-v-fd7826d1]{grid-template-columns:1fr 1fr}.wx-seo-rule__address[data-v-fd7826d1]{grid-column:1 / -1}}.wx-seo-urls__kind[data-v-d9fce714]{width:200px}.wx-table--cards .wx-seo-urls__kind[data-v-d9fce714]{width:100%}
|
package/dist/types.d.ts
CHANGED
|
@@ -71,6 +71,51 @@ export interface SeoRedirect {
|
|
|
71
71
|
updated_at: string | null;
|
|
72
72
|
[key: string]: unknown;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* An address a rename left behind, from `webx-ui/routing`.
|
|
76
|
+
*
|
|
77
|
+
* The same words a manual redirect uses where the two overlap, so one screen can show both
|
|
78
|
+
* without learning two vocabularies. What it has instead of the rest is an entity: these are
|
|
79
|
+
* written by whatever moved, always exact, always 301, and never edited here.
|
|
80
|
+
*/
|
|
81
|
+
export interface SeoAlias {
|
|
82
|
+
id: number;
|
|
83
|
+
locale: string;
|
|
84
|
+
/** The old address. */
|
|
85
|
+
pattern: string;
|
|
86
|
+
/** The address it leads to now; null when the row it pointed at is gone. */
|
|
87
|
+
target: string | null;
|
|
88
|
+
url: string;
|
|
89
|
+
target_url: string | null;
|
|
90
|
+
/** `page`, `article`, `product` — what the content module called itself. */
|
|
91
|
+
entity_type: string;
|
|
92
|
+
entity_id: number;
|
|
93
|
+
created_at: string | null;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
}
|
|
96
|
+
export interface SeoAliasQuery {
|
|
97
|
+
q?: string;
|
|
98
|
+
locale?: string | null;
|
|
99
|
+
page?: number;
|
|
100
|
+
per_page?: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What the address registry holds at an address, when something does.
|
|
104
|
+
*
|
|
105
|
+
* A rule written by hand is tried before any of it — that is deliberate — so this is a warning
|
|
106
|
+
* and never a refusal: the page at this address is about to stop being reachable.
|
|
107
|
+
*/
|
|
108
|
+
export interface SeoRoute {
|
|
109
|
+
path: string;
|
|
110
|
+
url: string;
|
|
111
|
+
kind: 'canonical' | 'alias';
|
|
112
|
+
/** Where an old address leads now; null on a live page, which has nowhere to lead. */
|
|
113
|
+
target: string | null;
|
|
114
|
+
entity_type: string;
|
|
115
|
+
entity_id: number;
|
|
116
|
+
/** False when a shorter address matched: the page at `/parts` answering for `/parts/bobcat`. */
|
|
117
|
+
exact: boolean;
|
|
118
|
+
}
|
|
74
119
|
export interface SeoRedirectInput {
|
|
75
120
|
match_type: MatchType;
|
|
76
121
|
pattern: string;
|
|
@@ -123,6 +168,8 @@ export interface SeoTestResult {
|
|
|
123
168
|
url: string;
|
|
124
169
|
/** Said first because it happens first: a redirected address never reaches the rules. */
|
|
125
170
|
redirect: SeoRedirect | null;
|
|
171
|
+
/** What the address registry has here — a live page, or the trail of one that moved. */
|
|
172
|
+
route: SeoRoute | null;
|
|
126
173
|
matched: SeoUrlRule | null;
|
|
127
174
|
chain: SeoChainStep[];
|
|
128
175
|
seo: SeoResolved;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-seo",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SEO for the WebX UI admin panel: rules for addresses, redirects, and the card that edits what a page says about itself.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vue-router": "^4.5.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@webx-ui/core": "^0.
|
|
47
|
-
"@webx-ui/module-admin": "^0.4.
|
|
48
|
-
"@webx-ui/schema": "^0.2.
|
|
46
|
+
"@webx-ui/core": "^0.20.0",
|
|
47
|
+
"@webx-ui/module-admin": "^0.4.2",
|
|
48
|
+
"@webx-ui/schema": "^0.2.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|