Compare commits
5 Commits
4d1fb94531
...
f1d89ea532
| Author | SHA1 | Date | |
|---|---|---|---|
| f1d89ea532 | |||
| dd9ce6e8b4 | |||
| 5397390be2 | |||
| 63d0173b2d | |||
| 6b4935ebba |
+3
-3
@@ -3,8 +3,8 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: Context gathered — ready for /gsd-plan-phase 6
|
status: Context gathered — ready for /gsd-plan-phase 6
|
||||||
last_updated: "2026-04-21T23:11:57.514Z"
|
last_updated: "2026-04-22T06:55:05.535Z"
|
||||||
last_activity: 2026-04-21
|
last_activity: 2026-04-22
|
||||||
progress:
|
progress:
|
||||||
total_phases: 8
|
total_phases: 8
|
||||||
completed_phases: 3
|
completed_phases: 3
|
||||||
@@ -26,7 +26,7 @@ progress:
|
|||||||
Phase: Phase 6 — Blog Pages
|
Phase: Phase 6 — Blog Pages
|
||||||
Plan: —
|
Plan: —
|
||||||
Status: Context gathered — ready for /gsd-plan-phase 6
|
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
|
Resume file: .planning/phases/06-blog-pages/06-UI-SPEC.md
|
||||||
|
|
||||||
## Accumulated Context
|
## 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(),
|
date: z.string(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
wordCount: z.number().optional(),
|
||||||
|
minutes: z.number().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default defineContentConfig({
|
export default defineContentConfig({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: "Markdown Format Guide"
|
|||||||
description: "Complete reference of all elements and components available in articles"
|
description: "Complete reference of all elements and components available in articles"
|
||||||
date: "2026-04-21"
|
date: "2026-04-21"
|
||||||
tags: ["guide", "markdown", "mdc"]
|
tags: ["guide", "markdown", "mdc"]
|
||||||
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## Basic Typography
|
## 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"
|
description: "Référence complète de tous les éléments et composants disponibles dans les articles"
|
||||||
date: "2026-04-21"
|
date: "2026-04-21"
|
||||||
tags: ["guide", "markdown", "mdc"]
|
tags: ["guide", "markdown", "mdc"]
|
||||||
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## Typographie de base
|
## 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