export type SeoConflictMode = 'off' | 'compat'; export interface ISeoModule { label: string; default: boolean; active: boolean; } export interface ISeoGeneral { conflict_mode: SeoConflictMode; remove_generator: boolean; disable_xmlrpc: boolean; frontend_css: string; nofollow_external: boolean; links_check_external: boolean; new_window_external: boolean; nofollow_image_links: boolean; nofollow_domains: string; nofollow_exclude_domains: string; strip_category_base: boolean; redirect_attachments: boolean; noindex_password_protected: boolean; breadcrumbs_separator: string; breadcrumbs_home_label: string; breadcrumbs_prefix: string; breadcrumbs_show_home: boolean; breadcrumbs_home_url: string; breadcrumbs_hide_current: boolean; breadcrumbs_archive_format: string; breadcrumbs_search_format: string; breadcrumbs_404_label: string; breadcrumbs_parent_terms: boolean; breadcrumbs_blog_page: boolean; breadcrumbs_woocommerce: boolean; wc_brand_taxonomy: string; wc_noindex_hidden: boolean; wc_remove_product_base: boolean; wc_remove_category_base: boolean; wc_remove_parent_slugs: boolean; wc_return_policy: '' | 'finite' | 'unlimited' | 'none'; wc_return_days: number; wc_return_fees: 'free' | 'customer'; wc_return_method: 'mail' | 'store' | 'kiosk'; wc_return_countries: string; wc_shipping_details: boolean; wc_shipping_countries: string; wc_shipping_rate: string; wc_shipping_free_over: string; wc_handling_days: string; wc_transit_days: string; redirects_default_code: SeoRedirectCode; redirects_auto_post: boolean; redirects_debug: boolean; redirects_pass_query: boolean; redirects_fallback: 'default' | 'homepage' | 'custom'; redirects_fallback_url: string; monitor_404_mode: 'simple' | 'advanced'; monitor_404_exclude: string; monitor_404_ignore_query: boolean; monitor_404_limit: number; monitor_404_purge_days: number; image_alt_missing: boolean; image_alt_format: string; image_alt_change_all: boolean; image_title_missing: boolean; image_title_format: string; image_title_change_all: boolean; image_caption_missing: boolean; image_caption_format: string; image_alt_on_upload: boolean; image_alt_ai_on_upload: boolean; robots_txt: string; rss_before: string; rss_after: string; } export interface ISeoImageStats { images: number; missing_alt: number; missing_title: number; } /** What one run of the AI alt text wrote, and what it left alone. */ export interface ISeoAiAltResult extends ISeoImageStats { written: number; credits: number; run_id: string; items: { id: number; alt: string; source: string }[]; skipped: { id: number; reason: string }[]; } export interface ISeoNotFoundEntry { id: number; uri: string; referer: string; user_agent: string; times_accessed: number; accessed_at: string; /** A redirect answers the address now: nothing left to do. */ redirected?: boolean; } export interface ISeoNotFoundList { items: ISeoNotFoundEntry[]; total: number; page: number; per_page: number; count: number; mode: 'simple' | 'advanced'; /** The redirect type new redirects start with (General → Redirects). */ default_code?: SeoRedirectCode; } export type SeoRedirectCode = '301' | '302' | '307' | '410' | '451'; export type SeoRedirectComparison = | 'exact' | 'contains' | 'starts' | 'ends' | 'regex'; export interface ISeoRedirectSource { pattern: string; comparison: SeoRedirectComparison; ignore_case: boolean; } export interface ISeoRedirect { id: number; sources: ISeoRedirectSource[]; url_to: string; header_code: number; status: 'active' | 'inactive'; hits: number; created_at: string; updated_at: string; last_accessed_at: string; } /** What an address gets from the redirects, as the Test an address box shows it. */ export type ISeoRedirectTest = | { on_site: false } | { on_site: true; redirect: null; fallback: 'default' | 'homepage' | 'custom'; fallback_url: string; } | { on_site: true; redirect: ISeoRedirect; source: ISeoRedirectSource | null; destination: string; /** The rule points at the address itself, so nothing happens. */ same_page: boolean; }; export interface ISeoRedirectInput { sources: ISeoRedirectSource[]; url_to: string; header_code: number; status: 'active' | 'inactive'; } export interface ISeoRedirectList { items: ISeoRedirect[]; total: number; page: number; per_page: number; count: number; settings: { default_code: SeoRedirectCode }; } export interface ISeoRedirectImportResult extends ISeoRedirectList { imported: number; skipped: number; errors: string[]; } /** One line of a CSV as a dry run reads it: what it would add, or why not. */ export interface ISeoRedirectPreviewRow { line: number; sources: string; url_to: string; header_code: number; /** Empty when the line would be imported. */ problem: string; } /** What an import would do, before it runs. */ export interface ISeoRedirectPreview { imported: number; skipped: number; errors: string[]; rows: ISeoRedirectPreviewRow[]; } /** Fixed keys plus the per post type (pt__*) and taxonomy (tax__*) templates. */ export type ISeoTitles = Record; export interface ISeoSitemap { enabled: boolean; links_per_sitemap: number; include_images: boolean; post_types: string[]; taxonomies: string[]; include_authors: boolean; include_empty_terms: boolean; exclude_posts: string; exclude_terms: string; ping_indexnow: boolean; include_videos: boolean; news_enabled: boolean; news_publication_name: string; news_post_types: string[]; html_sitemap_sort: 'published' | 'modified' | 'alphabetical' | 'id'; html_sitemap_dates: boolean; html_sitemap_seo_titles: boolean; } export interface ISeoPlugin { slug: string; name: string; version?: string; } export interface ISeoConflicts { active: ISeoPlugin[]; sources: ISeoPlugin[]; mode: SeoConflictMode; output_enabled: boolean; fields_enabled: boolean; } export interface ISeoVariable { name: string; label: string; example: string; } export interface ISeoLocal { business_type: string; name: string; street: string; locality: string; region: string; postal_code: string; country: string; phone: string; email: string; price_range: string; latitude: string; longitude: string; opening_hours: string; about_page: number; contact_page: number; show_map: boolean; map_provider: 'osm' | 'google'; google_maps_key: string; multiple_locations: boolean; } export interface ISeoLlms { post_types: string[]; taxonomies: string[]; limit: number; description: string; extra: string; full_enabled: boolean; full_limit: number; url: string; full_url: string; } export interface ISeoSettings { modules: Record; /** Whether the five setup steps are behind this site. */ setup: { done: boolean }; general: ISeoGeneral; titles: ISeoTitles; sitemap: ISeoSitemap; local: ISeoLocal; llms: ISeoLlms; /** What every option starts as, for the Reset options button of a section. */ defaults: { general: ISeoGeneral; titles: ISeoTitles; sitemap: ISeoSitemap; local: ISeoLocal; llms: Omit; }; /** The multi-location side of Local SEO: where the branches are managed and the KML lives, with how many pins it holds. */ locations: { kml_url: string; admin_url: string; count: number; pins: number; }; pages: { id: number; title: string }[]; preview: { page: { id: number; title: string; url: string } | null; pieces: { key: string; label: string; shortcode: string; available: boolean; url: string; }[]; }; business_types: string[]; /** The site title and tagline from Settings → General, which the brand fields fall back to. */ /** The WordPress title (what %sitename% prints) and tagline; whether this user may change the title. */ site: { title: string; tagline: string; can_title?: boolean }; /** The static page shown as the front page, whose own fields come before the Homepage ones. */ front_page: { id: number; title: string; edit_url: string; own_title: boolean; own_description: boolean; } | null; post_types: Record< string, { label: string; has_archive: boolean; count: number; /** The hierarchical taxonomies a primary term can come from: slug => label. */ taxonomies: Record; } >; taxonomies: Record; taxonomy_details: Record; /** What each sitemap file lists of what is published. */ sitemap_listed: { post_types: Record; taxonomies: Record; }; /** Where each sitemap file is served, for the links on the sitemap page. */ sitemap_urls: { index: string; post_types: Record; taxonomies: Record; }; /** Whether WooCommerce is active, and the taxonomies a product brand could come from. */ woocommerce: boolean; /** The shop's own country, which the merchant listing countries fall back to. */ shop_country: string; product_taxonomies: Record; separators: string[]; conflicts: ISeoConflicts; variables: ISeoVariable[]; /** Per kind of template (home, author, post:{type}, term:{taxonomy}...): the variables it can use and what each prints there. */ variable_sets?: Record>; } export type SeoCheckSeverity = 'error' | 'warning' | 'info'; export interface ISeoCheckItem { id: number; title: string; url: string; edit: string; } /** How a failed check gets fixed: a button here, the Copilot, a page of the app or of WordPress. */ export interface ISeoCheckFix { type: 'local' | 'copilot' | 'link' | 'external' | 'score'; label?: string; /** The Copilot's instruction; the items of the check follow it. */ prompt?: string; to?: string; } export interface ISeoCheck { id: string; group: string; severity: SeoCheckSeverity; title: string; description: string; count: number; items: ISeoCheckItem[]; passed: boolean; fix: ISeoCheckFix | null; } export interface ISeoCheckup { generated_at: string; score: number; counts: { error: number; warning: number; info: number; passed: number }; checks: ISeoCheck[]; /** Other SEO plugins printing the site meanwhile. */ rivals?: string[]; } export interface ISeoBadgeCheck { id: string; group: string; severity: SeoCheckSeverity; count: number; to: string; } export interface ISeoBadges { pending: boolean; generated_at: string | null; score: number | null; checks: ISeoBadgeCheck[]; types: { post_types: Record; taxonomies: Record; }; /** While this is false the menu and the dashboard offer the setup. */ setup_done: boolean; /** Module slug → on; the menu leaves out the pages of the ones that are off. */ modules?: Record; } /** One page or term of the site, as a picker shows it. */ export interface ISeoObject { id: number; title: string; subtitle: string; url: string; } /** What the schema's shipping fields can take from WooCommerce's zones. */ export interface ISeoShippingFromWooCommerce { /** Country codes, comma separated; empty when nothing maps. */ countries: string; /** The fixed rate, "0" for free; empty when nothing maps. */ rate: string; /** Free from this order total; empty for no threshold. */ free_over: string; /** The zones the values come from. */ zones: string[]; /** Zones and methods left out, with the reason. */ skipped: { zone: string; reason: string }[]; } /** One address the lookup offers, with every field it filled. */ export interface ISeoAddress { /** The address as the service writes it, for the merchant to recognise. */ label: string; street: string; locality: string; region: string; postal_code: string; country: string; latitude: string; longitude: string; } export interface ISeoCheckFixResult { fixed: boolean; message: string; run_id: string; checkup: ISeoCheckup; } /** One value a machine wrote, with what stood there before. */ export interface ISeoChange { id: number; run_id: string; source: string; object_type: string; object_id: number; object: string; edit_link: string; field: string; before: unknown; after: unknown; created_at: string; reverted_at: string | null; /** The page, term, user or image it changed was deleted since. */ gone: boolean; } export interface ISeoChangeRun { run_id: string; source: string; changes: number; reverted: number; /** Changes still standing on things deleted since. */ gone: number; started_at: string; ended_at: string; } export interface ISeoChangesPayload { items: ISeoChange[]; total: number; runs: ISeoChangeRun[]; } export interface ISeoSettingsPatch { modules?: Record; setup?: { done: boolean }; general?: Partial; titles?: ISeoTitles; sitemap?: Partial; local?: Partial; llms?: Partial>; /** The WordPress site title, set to the website name. */ site?: { title: string }; } export type SeoImportPart = | 'settings' | 'posts' | 'terms' | 'users' | 'redirects' | '404'; export type SeoExportPart = 'posts' | 'terms' | 'users'; export interface ISeoImportCounts { settings: boolean; posts: number; terms: number; users: number; redirects: number; logs_404: number; } export interface ISeoImportSource { slug: string; name: string; active: boolean; available: boolean; counts: ISeoImportCounts; } export interface ISeoImportBatch { part: string; offset: number; next: number; total: number; imported: number; done: boolean; /** Objects an error kept back, each skipped rather than stopping the import. */ skipped?: number; /** The first of those errors. */ error?: string; } /** Plugins a switch turned off and an error after it turned back on. */ export interface ISeoSwitchUndone { label: string; plugins: string[]; message: string; file: string; line: number; url: string; at: number; /** What happened and what to do, in a sentence. */ text: string; } export interface ISeoIndexnowEntry { time: number; status: number; message: string; count: number; /** The first addresses of the batch; the log keeps 20. */ urls: string[]; /** Where it came from; entries logged before this was kept have none. */ source?: 'auto' | 'manual' | 'list' | 'toolbar'; } export interface ISeoIndexnow { enabled: boolean; /** The IndexNow module is on (the dashboard switch). */ module_active?: boolean; /** "Notify IndexNow on publish" is on (Sitemap → IndexNow). */ ping_on?: boolean; /** A production site open to search engines: publishing may ping on its own. */ site_announced?: boolean; key: string; key_url: string; history: ISeoIndexnowEntry[]; } export interface ISeoIndexnowSubmission extends ISeoIndexnow { sent: ISeoIndexnowEntry[]; skipped: number; } export interface ISeoLinksOverview { indexed: boolean; posts: number; internal: number; external: number; nofollow: number; orphans: number; /** Links that answered 4xx, 5xx or nothing, the blocked ones aside. */ broken: number; /** Links whose site turned the checker away (401, 403, 429, 999): check them by hand. */ blocked: number; /** Links through a redirect: the site's own, and external ones that moved for good. */ redirected: number; /** Links marked as not broken, off the other lists. */ ignored: number; unchecked: number; } export interface ISeoOrphanPage { id: number; title: string; link: string; edit_link: string; type: string; date: string; internal: number; } /** A page with its links: out to the site, out of it, and in from other pages. */ export interface ISeoLinkPage { id: number; title: string; type: string; link: string; edit_link: string; internal: number; external: number; incoming: number; } export interface ISeoBrokenLink { id: number; post_id: number; post_title: string; edit_link: string; url: string; type: 'internal' | 'external'; anchor: string; status: number; /** Where a redirected link leads. */ redirect_to: string; /** Whether the link can be pointed at where it leads: not at a sign-in screen. */ updatable: boolean; checked_at: string; } export type SeoLinksList = | 'pages' | 'orphans' | 'broken' | 'redirected' | 'blocked' | 'ignored'; export interface ISeoLinksPayload { overview: ISeoLinksOverview; list: SeoLinksList; items: ISeoLinkPage[] | ISeoOrphanPage[] | ISeoBrokenLink[]; total: number; page: number; per_page: number; } /** The links of one page, and the pages linking to it. */ export interface ISeoLinkDetails { outgoing: { id: number; url: string; anchor: string; type: 'internal' | 'external'; nofollow: boolean; status: number; kind: 'ok' | 'unchecked' | 'redirected' | 'blocked' | 'broken' | 'ignored'; redirect_to: string; }[]; incoming: { post_id: number; title: string; edit_link: string; anchor: string; }[]; } export type SeoLinkAction = | 'unlink' | 'replace' | 'update' | 'recheck' | 'ignore' | 'unignore'; /** What a crawler reads robots.txt for: search results, AI answers now, or training future models. */ /** Answer engines, training crawlers, names that decide both (Google-Extended), search engines. */ export type SeoCrawlerKind = 'search' | 'answers' | 'training' | 'mixed'; export interface ISeoCrawlerStatus { /** The product token in lower case, the id the AI lists store. */ id: string; token: string; company: string; product: string; kind: SeoCrawlerKind; /** Whether a group of the file names it; otherwise it follows `User-agent: *`. */ named: boolean; /** On the home page and the latest post, page and product. */ status: 'allowed' | 'blocked' | 'partial'; blocked: string[]; } /** The AI crawlers the settings allow or block; the rest follow the text. */ export interface ISeoRobotsPolicy { allow: string[]; block: string[]; } /** A page to score, with what the editor's analyzer reads of it. */ export interface ISeoScoreItem { id: number; title: string; input: { title: string; description: string; slug: string; content: string; keyword: string; keywords: string[]; hasFeaturedImage: boolean; imageAlts: string[]; postType: string; /** The post's language from Polylang or WPML, empty without one. */ language: string; siteLanguage: string; /** Not looked up for a batch: it weighs nothing on the score. */ keywordUsedElsewhere: boolean | null; }; } export interface ISeoScoreQueue { items: ISeoScoreItem[]; /** Pages left from this batch on, this one included. */ remaining: number; /** With the first batch: pages without a current score, and all pages. */ counts?: { needed: number; total: number }; } /** Each line of opening hours and what was read from it. */ export interface ISeoHoursRead { lines: { text: string; read: boolean; days: string; time: string }[]; } /** What a file written by hand drops from the default one, or lets a crawler past. */ export interface ISeoRobotsLoss { kind: 'sitemap' | 'every' | 'rules' | 'escape'; text: string; } /** A robots.txt saved before the current one. */ export interface ISeoRobotsVersion { saved_at: string; user: string; /** The text; empty was the default file. */ content: string; policy: ISeoRobotsPolicy; } export interface ISeoRobotsFile { url: string; /** What is saved; empty means the default file is served. */ content: string; default: string; /** A robots.txt file exists on disk and is served instead of the virtual one. */ physical: boolean; /** Whether the site asks to be indexed at all (Settings → Reading). */ public: boolean; warnings: { line: number; text: string }[]; /** The file crawlers get now: the text with the AI choices written over it, or the file on disk. */ served: string; policy: ISeoRobotsPolicy; crawlers: ISeoCrawlerStatus[]; lost: ISeoRobotsLoss[]; /** The last five saved before this one, newest first. */ versions: ISeoRobotsVersion[]; /** Other SEO plugins that write robots.txt while they are active. */ rivals?: string[]; } /** What a draft would serve, before it is saved. */ export interface ISeoRobotsPreview { served: string; crawlers: ISeoCrawlerStatus[]; warnings: { line: number; text: string }[]; lost: ISeoRobotsLoss[]; } export interface ISeoHtaccessBackup { name: string; date: string; size: number; } export interface ISeoHtaccessFile { supported: boolean; allowed: boolean; exists: boolean; writable: boolean; path: string; content: string; /** Lines whose directive Apache may not know. */ warnings: { line: number; text: string }[]; backups: ISeoHtaccessBackup[]; /** After a save: why the site could not be asked how it answers. */ notice?: string; } /** A .htaccess draft read before it is saved. */ export interface ISeoHtaccessCheck { /** Why a save would be refused, empty when it would not. */ error: string; warnings: { line: number; text: string }[]; } export interface ISeoFiles { robots: ISeoRobotsFile; htaccess: ISeoHtaccessFile; } export interface ISeoCapability { slug: string; label: string; description: string; } export interface ISeoRoles { capabilities: ISeoCapability[]; /** opens_app: the role opens the whole app, with the Recomaze account; the others open the SEO pages they are given. */ roles: { slug: string; name: string; opens_app?: boolean }[]; /** Capability slug => role slugs holding it. */ map: Record; customized: boolean; } export type SeoBackupPart = 'settings' | 'modules' | 'roles' | 'redirects'; export interface ISeoBackup { recomaze_seo: number; plugin: string; site: string; exported_at: string; settings?: Record>; modules?: string[]; roles?: Record; redirects?: unknown[]; } /** The settings, modules and roles as they were before an import or a restore. */ export interface ISeoBackupSnapshot { id: string; /** ISO 8601, UTC. */ created_at: string; reason: 'import' | 'restore'; /** Who ran the import or the restore. */ user: string; /** Redirects the import after it added, which a restore removes. */ redirects_added: number; /** When it was put back, or empty. */ restored_at: string; }