1//#region src/utils/seoDescription.ts 2/** 3* Recursively collect text from a Lexical node tree into `parts`. 4* Only `text` leaf nodes contribute; structure (headings, lists, links) is flattened. 5*/ 6function collectLexicalText(node, parts) { 7 if (typeof node !== "object" || node === null) return; 8 const n = node; 9 if (n.type === "text" && typeof n.text === "string") parts.push(n.text); 10 if (Array.isArray(n.children)) for (const child of n.children) collectLexicalText(child, parts); 11} 12/** Flatten a Lexical rich text value to a single normalised plain-text string. */ 13function richTextToPlainText(richText) { 14 if (typeof richText !== "object" || richText === null) return ""; 15 const root = richText.root; 16 if (typeof root !== "object" || root === null) return ""; 17 const parts = []; 18 const children = root.children; 19 if (Array.isArray(children)) for (const child of children) collectLexicalText(child, parts); 20 return parts.join(" ").replace(/\s+/g, " ").trim(); 21} 22/** 23* Find the first body rich text in a content array. 24* Mirrors the CMS extractor: Article Story block (`block.richText`) wins over a 25* generic Rich Text block (`block.columns[0].richText`), in document order. 26*/ 27function findFirstBodyRichText(content) { 28 if (!content || !Array.isArray(content)) return null; 29 for (const item of content) { 30 if (item.type !== "block" || !item.block || item.block.length === 0) continue; 31 const block = item.block[0]; 32 if (block.blockType === "articleStoryBlock" && block.richText) return block.richText; 33 if (block.blockType === "richtext") { 34 const columns = block.columns; 35 if (Array.isArray(columns) && columns.length > 0) { 36 const firstColumn = columns[0]; 37 if (firstColumn?.richText) return firstColumn.richText; 38 } 39 } 40 } 41 return null; 42} 43/** 44* Truncate plain text to a meta-description-friendly length. 45* 46* Policy: 47* - ~160 char ceiling (Google snippet sweet spot; longer text is wasted). 48* - Trim back to the last whole word so we never cut mid-word. 49* - Strip trailing punctuation/whitespace before appending an ellipsis. 50* - Append "â¦" only when text was actually cut. 51* - Single token longer than maxLength is hard-cut rather than dropped. 52*/ 53function truncateForMeta(text, maxLength = 160) { 54 const normalized = text.trim(); 55 if (!normalized) return ""; 56 if (normalized.length <= maxLength) return normalized; 57 const slice = normalized.slice(0, maxLength); 58 const lastSpace = slice.lastIndexOf(" "); 59 return `${(lastSpace > 0 ? slice.slice(0, lastSpace) : slice).replace(/[\s.,;:!?]+$/, "")}â¦`; 60} 61/** 62* Resolve a content document's description, preferring the editor-set 63* `meta.description` and falling back to extracted, truncated body text. 64* Returns "" when nothing is available (callers treat "" as "no description"). 65*/ 66function getContentDescription(doc) { 67 const explicit = doc.meta?.description?.trim(); 68 if (explicit) return explicit; 69 const richText = findFirstBodyRichText(doc.content); 70 if (!richText) return ""; 71 return truncateForMeta(richTextToPlainText(richText)); 72} 73//#endregion 74export { truncateForMeta as n, getContentDescription as t }; 75 76//# sourceMappingURL=seoDescription-B_c1FOLC.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.