import { useMemo } from 'react'; import type { ProductInput } from '../analysis/analyzer'; import type { SeoEditorEnv } from '../env'; import { htmlToText } from '../lib/diff'; import { parseKeywords } from '../lib/keywords'; import { asNumber, asString } from '../lib/meta-values'; import { renderTemplate } from '../lib/render-template'; import { useDebounced } from './use-debounced'; import { useAssignedTerms, type EditedPost, type SeoMeta, } from './use-seo-meta'; /** * What the analysis reads of the object being edited: the title and the * description as they will print, the keywords, the content and, on a * product, its listing data. The panel shows it; in the block editor it * also feeds the score on the toolbar while the sidebar is closed. * * @param env - The editor's environment. * @param meta - The SEO fields as edited. * @param post - The post as edited. * @return The input and the pieces it is made of. */ export const useAnalysisInput = ( env: SeoEditorEnv, meta: SeoMeta, post: EditedPost ) => { const { keys } = env; const has = (key: string): boolean => env.fields.includes(key); // Parsed once per value: a new array on every render ran the whole // analysis again on every keystroke anywhere in the panel. const keywordsText = has(keys.focusKeywords) ? asString(meta[keys.focusKeywords]) : ''; const keywords = useMemo(() => parseKeywords(keywordsText), [keywordsText]); // The taxonomy Titles & meta picks, when the post has terms in it; else // the first hierarchical one that has some, as the breadcrumbs read it. const withTerms = (slug: string): boolean => (post.terms[slug]?.length ?? 0) > 0; const primaryTaxonomy = env.taxonomies.find( taxonomy => taxonomy.slug === env.primaryTaxonomy && taxonomy.hierarchical && withTerms(taxonomy.slug) ) ?? env.taxonomies.find( taxonomy => taxonomy.hierarchical && withTerms(taxonomy.slug) && !(env.brandTaxonomies ?? []).includes(taxonomy.slug) ); const assigned = useAssignedTerms( primaryTaxonomy?.slug ?? 'category', primaryTaxonomy ? post.terms[primaryTaxonomy.slug] : [] ); const primaryId = primaryTaxonomy ? asNumber(meta[primaryTaxonomy.primaryKey]) : 0; const live = { post, keyword: keywords[0] ?? '', primaryTerm: assigned.find(term => term.id === primaryId)?.name ?? '', firstTerm: assigned[0]?.name ?? '', }; const titleTemplate = asString(meta[keys.title]); const descriptionTemplate = asString(meta[keys.description]); const renderedTitle = renderTemplate( titleTemplate || env.templates.title, env, live ); const renderedDescription = renderTemplate( descriptionTemplate || env.templates.description, env, live ); const siteHost = useMemo(() => { try { return new URL(env.siteUrl).host; } catch { return ''; } }, [env.siteUrl]); // The analysis reads the whole content: on a long post, running it on // every keystroke made typing lag, so it waits for a pause. const content = useDebounced(post.content, 400); // A product's listing data, as typed: the price of a variable product is // in its variations, so the saved one stands in for it. const productFacts = useMemo((): ProductInput | undefined => { const facts = post.product; if (!facts || !env.product || env.object !== 'post') return undefined; const categories = (post.terms.product_cat ?? []).filter( id => id !== env.product?.defaultCategory ).length; return { price: ['simple', 'external'].includes(facts.type) ? facts.price : env.product.price, image: post.featuredImageId > 0, gallery: facts.gallery, shortDescription: htmlToText(post.excerpt), gtin: asString(meta[keys.gtin]) || facts.gtin || env.product.gtin, mpn: asString(meta[keys.mpn]), brand: asString(meta[keys.brand]) || env.product.brand || (facts.brands > 0 ? 'picked' : ''), categories, reviews: env.product.reviews, }; }, [ post.product, post.terms, post.featuredImageId, post.excerpt, env.product, env.object, meta, keys.gtin, keys.mpn, keys.brand, ]); const input = useMemo( () => ({ title: renderedTitle, description: renderedDescription, slug: post.slug, content: [content, ...Object.values(env.analyzeFields ?? {})] .filter(Boolean) .join('\n'), keywords, hasFeaturedImage: post.featuredImageId > 0, imageAlts: post.imageAlts, siteHost, scope: (env.object === 'term' ? 'term' : 'post') as 'term' | 'post', postType: env.object === 'post' ? env.type : undefined, product: productFacts, language: env.language, siteLanguage: env.siteLanguage, }), [ renderedTitle, renderedDescription, post.slug, content, env.analyzeFields, keywords, post.featuredImageId, post.imageAlts, siteHost, env.object, env.type, productFacts, env.language, env.siteLanguage, ] ); return { keywordsText, keywords, primaryTaxonomy, assigned, primaryId, live, titleTemplate, descriptionTemplate, renderedTitle, renderedDescription, siteHost, input, }; };