import { GENERAL, LLMS, LOCAL, PAGES, SITEMAP, TITLES, TITLES_MORE, WEBMASTER_LABELS, WOOCOMMERCE_TOOLS, postTypeFields, taxonomyFields, } from '../../pages/seo/settings.copy'; import { SEO_NAV_SECTIONS, typeSections, type SeoTypes } from './nav-sections'; import { generalSection, localSection, sitemapSection, titlesSection, } from './option-sections'; /** An option or a section of any SEO settings page, for the search on every page. */ export interface SettingsEntry { page: string; pageLabel: string; section: string; label: string; description: string; tip: string; /** What the option belongs to beside its page: the post type or taxonomy. */ detail?: string; /** A section itself rather than one option in it. */ isSection?: boolean; } const PAGE_LABELS: Record = { '/seo/general': PAGES.general.title, '/seo/titles': PAGES.titles.title, '/seo/sitemap': PAGES.sitemap.title, '/seo/local': PAGES.local.title, }; interface Copy { label: string; description?: string; tip?: string; } /** * Every option and section of the SEO settings pages, from the copy the * pages print, each with the page and section that shows it. * * @param types - The post types and taxonomies, for their own sections. * @return The entries. */ export const settingsIndex = (types?: SeoTypes): SettingsEntry[] => { const entries: SettingsEntry[] = []; const add = ( page: string, section: string, copy: Copy, detail?: string ): void => { entries.push({ page, pageLabel: PAGE_LABELS[page] ?? page, section, label: copy.label, description: copy.description ?? '', tip: copy.tip ?? '', detail, }); }; const postTypes = Object.keys(types?.post_types ?? {}); const taxonomies = Object.keys(types?.taxonomies ?? {}); Object.entries(GENERAL).forEach(([key, copy]) => add('/seo/general', generalSection(`general.${key}`), copy) ); Object.values(WEBMASTER_LABELS).forEach(copy => add('/seo/general', 'webmaster', copy) ); Object.values(LLMS).forEach(copy => add('/seo/general', 'llms', copy)); Object.values(WOOCOMMERCE_TOOLS).forEach(copy => add('/seo/general', 'woocommerce', copy) ); Object.entries({ ...TITLES, ...TITLES_MORE }).forEach(([key, copy]) => add( '/seo/titles', titlesSection(`titles.${key}`, postTypes, taxonomies), copy ) ); Object.entries(types?.post_types ?? {}).forEach(([slug, label]) => Object.values(postTypeFields(label)).forEach(copy => add('/seo/titles', `post-type-${slug}`, copy, label) ) ); Object.entries(types?.taxonomies ?? {}).forEach(([slug, label]) => Object.values(taxonomyFields(label)).forEach(copy => add('/seo/titles', `taxonomy-${slug}`, copy, label) ) ); Object.entries(SITEMAP).forEach(([key, copy]) => add('/seo/sitemap', sitemapSection(key), copy) ); Object.entries(LOCAL).forEach(([key, copy]) => add('/seo/local', localSection(`local.${key}`), copy) ); Object.entries(SEO_NAV_SECTIONS).forEach(([page, known]) => [...known.sections, ...(types ? typeSections(page, types) : [])].forEach( section => entries.push({ page, pageLabel: PAGE_LABELS[page] ?? page, section: section.key, label: section.label, description: section.description, tip: '', detail: section.group, isSection: true, }) ) ); return entries; }; /** The words of a text, lower case and without accents, for matching by word start. */ const words = (text: string): string[] => text .toLowerCase() .normalize('NFD') .replace(/[̀-ͯ]/g, '') .split(/[^a-z0-9%]+/) .filter(Boolean); /** * How well a text answers a query, 0 for not at all: every word typed has * to start a word of it, so "og" finds "og:image" and not "Logo". * * @param query - What was typed. * @param fields - The texts to look in, the most telling first. * @return A rank: higher for a match in an earlier field. */ export const rankMatch = (query: string, fields: string[]): number => { const terms = words(query); if (terms.length === 0) { return 0; } for (let index = 0; index < fields.length; index++) { const own = words(fields[index]); if (terms.every(term => own.some(word => word.startsWith(term)))) { return fields.length - index; } } const all = words(fields.join(' ')); return terms.every(term => all.some(word => word.startsWith(term))) ? 0.5 : 0; };