Compare commits

...

5 Commits

Author SHA1 Message Date
kayjaydee f1d89ea532 chore(06-01): mark test-kotlin-syntax articles as draft (FR + EN)
- Add `draft: true` to frontmatter of both test-kotlin-syntax.md files
  so they are excluded from all `queryCollection(...).where('draft', '=', false)`
  listings (D-14).
- Articles remain accessible via direct URL (no draft filter on `.path(x).first()`),
  keeping them available for internal rendering tests.
- Listings will be empty until real Hytale seed articles land in Phase 8 —
  the empty state will render per D-16 ("Hytale articles coming soon" CTA).
- Body content untouched (only frontmatter +1 line each).
2026-04-22 09:05:47 +02:00
kayjaydee dd9ce6e8b4 feat(06-01): add useReadingTime composable fallback (200 wpm)
- Pure synchronous helper returning minutes (>= 1) from either a pre-computed
  word count (number) or raw text (string, tokenized on whitespace).
- Client-side safety net when `article.minutes` isn't yet populated
  (e.g., dev hot-reload before the Nitro hook re-parsed). Source of truth
  remains the Nitro `content:file:afterParse` hook (D-19).
- Same 200 wpm formula as server-side hook — ensures listing ↔ article parity.
- Auto-imported by Nuxt thanks to `use*` naming convention.
2026-04-22 09:04:53 +02:00
kayjaydee 5397390be2 feat(06-01): add Nitro hook content:file:afterParse for reading-time injection
- Register `content:file:afterParse` hook to inject `wordCount` + `minutes`
  on every parsed markdown content object (D-19: 200 wpm, floor 1 min).
- Import pure util `countWordsInMinimalBody` from app/utils/countWords.
- Guard against non-`.md` files (defensive — hook fires on all sources).
- Values persist in @nuxt/content SQLite DB and are queryable via
  queryCollection thanks to matching Zod fields (content.config.ts).
2026-04-22 09:02:23 +02:00
kayjaydee 63d0173b2d 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
2026-04-22 08:57:05 +02:00
kayjaydee 6b4935ebba feat(06-01): extend blogSchema with draft/wordCount/minutes fields
- Add draft: z.boolean().optional().default(false) to allow .where('draft','=',false)
- Add wordCount + minutes as optional (injected by Nitro hook at parse time)
- Collections blog_fr/blog_en unchanged (schema is referenced by variable)
2026-04-22 08:56:46 +02:00
7 changed files with 82 additions and 3 deletions
+3 -3
View File
@@ -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
+17
View File
@@ -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))
}
+34
View File
@@ -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
}
+3
View File
@@ -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({
+1
View File
@@ -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
+1
View File
@@ -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
+23
View File
@@ -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
})
})