From 63d0173b2d1f6d7e9e0806cf72618ec599c5dd10 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Wed, 22 Apr 2026 08:57:05 +0200 Subject: [PATCH] feat(06-01): add countWordsInMinimalBody util for reading-time computation - Pure AST traversal of @nuxt/content v3 minimal body shape - Skips code and pre tags (code snippets are not readable prose) - Zero dependency, zero import, reused by Nitro hook --- app/utils/countWords.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/utils/countWords.ts diff --git a/app/utils/countWords.ts b/app/utils/countWords.ts new file mode 100644 index 0000000..50629da --- /dev/null +++ b/app/utils/countWords.ts @@ -0,0 +1,34 @@ +/** + * Count words in a @nuxt/content v3 "minimal" body AST. + * Ignores code and pre tags (code snippets are not "readable" for reading-time purposes). + * + * Body shape (v3): { type: 'minimal', value: MinimalNode[] } + * MinimalNode = string | [tag: string, attrs: object, ...children: MinimalNode[]] + * + * Used by server/plugins/reading-time.ts at content:file:afterParse. + */ +export function countWordsInMinimalBody(body: unknown): number { + let count = 0 + + const visit = (node: unknown): void => { + if (typeof node === 'string') { + const trimmed = node.trim() + if (trimmed) count += trimmed.split(/\s+/).length + return + } + if (Array.isArray(node)) { + const tag = node[0] + // Skip code/pre — not counted as reading content + if (tag === 'code' || tag === 'pre') return + // children start at index 2 (index 0 = tag, index 1 = attrs) + for (let i = 2; i < node.length; i++) visit(node[i]) + } + } + + const body_ = body as { type?: string; value?: unknown[] } | undefined + if (body_?.value && Array.isArray(body_.value)) { + for (const node of body_.value) visit(node) + } + + return count +}