Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1d89ea532 | |||
| dd9ce6e8b4 | |||
| 5397390be2 | |||
| 63d0173b2d | |||
| 6b4935ebba |
+3
-3
@@ -3,8 +3,8 @@ gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: Context gathered — ready for /gsd-plan-phase 6
|
||||
last_updated: "2026-04-21T23:11:57.514Z"
|
||||
last_activity: 2026-04-21
|
||||
last_updated: "2026-04-22T06:55:05.535Z"
|
||||
last_activity: 2026-04-22
|
||||
progress:
|
||||
total_phases: 8
|
||||
completed_phases: 3
|
||||
@@ -26,7 +26,7 @@ progress:
|
||||
Phase: Phase 6 — Blog Pages
|
||||
Plan: —
|
||||
Status: Context gathered — ready for /gsd-plan-phase 6
|
||||
Last activity: 2026-04-21
|
||||
Last activity: 2026-04-22
|
||||
Resume file: .planning/phases/06-blog-pages/06-UI-SPEC.md
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Fallback reading-time helper when `article.minutes` is not available
|
||||
* (e.g., dev hot-reload before the Nitro hook has re-parsed).
|
||||
*
|
||||
* Source of truth = server/plugins/reading-time.ts + content.config.ts schema.
|
||||
* This is only a client-side safety net (per D-19).
|
||||
*
|
||||
* @param wordCountOrText number (word count already computed) OR string (raw text to tokenize)
|
||||
* @returns minutes (>= 1), rounded up, using 200 words per minute
|
||||
*/
|
||||
export function useReadingTime(wordCountOrText: number | string): number {
|
||||
if (typeof wordCountOrText === 'number') {
|
||||
return Math.max(1, Math.ceil(wordCountOrText / 200))
|
||||
}
|
||||
const count = wordCountOrText.trim().split(/\s+/).filter(Boolean).length
|
||||
return Math.max(1, Math.ceil(count / 200))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,6 +6,9 @@ const blogSchema = z.object({
|
||||
date: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
image: z.string().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
wordCount: z.number().optional(),
|
||||
minutes: z.number().optional(),
|
||||
})
|
||||
|
||||
export default defineContentConfig({
|
||||
|
||||
@@ -3,6 +3,7 @@ title: "Markdown Format Guide"
|
||||
description: "Complete reference of all elements and components available in articles"
|
||||
date: "2026-04-21"
|
||||
tags: ["guide", "markdown", "mdc"]
|
||||
draft: true
|
||||
---
|
||||
|
||||
## Basic Typography
|
||||
|
||||
@@ -3,6 +3,7 @@ title: "Guide du format Markdown"
|
||||
description: "Référence complète de tous les éléments et composants disponibles dans les articles"
|
||||
date: "2026-04-21"
|
||||
tags: ["guide", "markdown", "mdc"]
|
||||
draft: true
|
||||
---
|
||||
|
||||
## Typographie de base
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { countWordsInMinimalBody } from '~/utils/countWords'
|
||||
|
||||
/**
|
||||
* Nitro plugin: compute reading time for every markdown content file at parse time.
|
||||
*
|
||||
* Injects `wordCount` (number) and `minutes` (number, min 1) on the content object.
|
||||
* Values are persisted in the @nuxt/content SQLite DB and queryable via queryCollection
|
||||
* thanks to the matching Zod schema fields in content.config.ts (per D-18 + D-19).
|
||||
*
|
||||
* Hook reference: https://content.nuxt.com/docs/advanced/hooks
|
||||
*/
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('content:file:afterParse', (ctx) => {
|
||||
const { file, content } = ctx
|
||||
|
||||
// Only process markdown files (defensive — hook fires on all sources)
|
||||
if (!file.id?.endsWith('.md')) return
|
||||
|
||||
const wordCount = countWordsInMinimalBody(content.body)
|
||||
content.wordCount = wordCount
|
||||
content.minutes = Math.max(1, Math.ceil(wordCount / 200)) // D-19: 200 wpm, floor 1 min
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user