Compare commits
23 Commits
1df6a21c5e
...
f2e29e6c2f
| Author | SHA1 | Date | |
|---|---|---|---|
| f2e29e6c2f | |||
| 2ea6af0fff | |||
| b63afc4152 | |||
| c5be72bdd9 | |||
| b0af1d3913 | |||
| 006df6ad30 | |||
| 3e20e9ece9 | |||
| 221b1a076c | |||
| f179d64253 | |||
| 60e05f7a56 | |||
| b63869f042 | |||
| 37b6ef9112 | |||
| ee7509cff0 | |||
| 9849c18da4 | |||
| e1c91e583f | |||
| b5c3250a4e | |||
| 0fa19a7701 | |||
| c9a14a9086 | |||
| 557861aa95 | |||
| f49fab2532 | |||
| 83197899c8 | |||
| 3381b2efb3 | |||
| c64709da10 |
@@ -34,3 +34,4 @@ coverage
|
|||||||
.nuxt
|
.nuxt
|
||||||
.output
|
.output
|
||||||
.env
|
.env
|
||||||
|
.data
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
phase: 05-nuxt-content-setup-renderer
|
||||||
|
plan: "01"
|
||||||
|
subsystem: cms-infrastructure
|
||||||
|
tags: [nuxt-content, shiki, tailwind-typography, sqlite, i18n, collections]
|
||||||
|
dependency_graph:
|
||||||
|
requires: []
|
||||||
|
provides: [nuxt-content-module, shiki-dual-theme, bilingual-collections, typography-plugin]
|
||||||
|
affects: [nuxt.config.ts, content.config.ts, app/assets/css/main.css]
|
||||||
|
tech_stack:
|
||||||
|
added:
|
||||||
|
- "@nuxt/content@3.13.0"
|
||||||
|
- "@tailwindcss/typography@0.5.19"
|
||||||
|
patterns:
|
||||||
|
- "Shiki dual-theme via theme.default + theme.dark (github-light/github-dark)"
|
||||||
|
- "SQLite connecteur natif Node 22 via experimental.sqliteConnector: 'native'"
|
||||||
|
- "Collections i18n: prefix_except_default — blog_fr=/blog, blog_en=/en/blog"
|
||||||
|
- "@plugin CSS syntax pour Tailwind v4 plugins"
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- content.config.ts
|
||||||
|
modified:
|
||||||
|
- nuxt.config.ts
|
||||||
|
- app/assets/css/main.css
|
||||||
|
- .gitignore
|
||||||
|
decisions:
|
||||||
|
- "sqliteConnector: 'native' (Node 22) — évite better-sqlite3 et ses bindings natifs"
|
||||||
|
- "Prefixes collections alignés sur i18n.strategy: prefix_except_default (FR sans prefix, EN avec /en/)"
|
||||||
|
- "Shiki langs: kotlin, java, typescript, shell, bash, json, vue, html, css"
|
||||||
|
metrics:
|
||||||
|
duration: "~10 minutes"
|
||||||
|
completed: "2026-04-21"
|
||||||
|
tasks_completed: 3
|
||||||
|
tasks_total: 3
|
||||||
|
files_created: 1
|
||||||
|
files_modified: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 05 Plan 01: @nuxt/content Install & Configuration Summary
|
||||||
|
|
||||||
|
Installation et configuration de @nuxt/content v3 avec Shiki dual-theme, collections bilingues FR/EN, et plugin @tailwindcss/typography pour le portfolio Nuxt 4.
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
|
||||||
|
| Task | Name | Commit | Files |
|
||||||
|
|------|------|--------|-------|
|
||||||
|
| 1 | Installer @nuxt/content et @tailwindcss/typography | c64709d | package.json, pnpm-lock.yaml |
|
||||||
|
| 2 | Configurer nuxt.config.ts et main.css | 3381b2e | nuxt.config.ts, app/assets/css/main.css |
|
||||||
|
| 3 | Créer content.config.ts avec collections bilingues | 8319789 | content.config.ts |
|
||||||
|
| — | Fix: .data dans .gitignore | f49fab2 | .gitignore |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
1. **sqliteConnector: 'native'** — Node 22 inclut SQLite natif, évite la dépendance `better-sqlite3` et ses bindings C++ à compiler.
|
||||||
|
2. **Prefixes i18n des collections** — alignés sur `prefix_except_default` : `blog_fr` → `/blog` (FR = locale par défaut, pas de prefix), `blog_en` → `/en/blog`.
|
||||||
|
3. **Schema Zod minimal** — `title`, `description`, `date` requis + `tags`, `image` optionnels. Les champs `author` et `og:image` seront ajoutés en Phase 7.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 2 - Missing] .data/ non tracké dans .gitignore**
|
||||||
|
- **Found during:** Après Task 3 (smoke test `pnpm dev`)
|
||||||
|
- **Issue:** `@nuxt/content` génère un répertoire `.data/content/` (base SQLite runtime) non ignoré par git
|
||||||
|
- **Fix:** Ajout de `.data` dans `.gitignore`
|
||||||
|
- **Files modified:** .gitignore
|
||||||
|
- **Commit:** f49fab2
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
```
|
||||||
|
grep '"@nuxt/content"' package.json → "@nuxt/content": "^3.13.0"
|
||||||
|
grep "'@nuxt/content'" nuxt.config.ts → '@nuxt/content'
|
||||||
|
grep "github-dark" nuxt.config.ts → dark: 'github-dark'
|
||||||
|
grep "sqliteConnector" nuxt.config.ts → sqliteConnector: 'native'
|
||||||
|
grep "nativeSqlite" nuxt.config.ts → (rien — correct)
|
||||||
|
grep '@plugin' app/assets/css/main.css → @plugin "@tailwindcss/typography";
|
||||||
|
grep "blog_fr\|blog_en" content.config.ts → blog_fr + blog_en
|
||||||
|
pnpm dev → Nuxt 4.4.2 démarre sur :3000 sans erreur
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
Aucun — cette phase ne produit pas de rendu UI, uniquement de l'infrastructure.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
Aucun nouveau vecteur introduit au-delà de ce qui est documenté dans le threat_model du plan.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- content.config.ts existe : FOUND
|
||||||
|
- nuxt.config.ts contient '@nuxt/content' : FOUND
|
||||||
|
- app/assets/css/main.css contient @plugin typography : FOUND
|
||||||
|
- Commits c64709d, 3381b2e, 8319789, f49fab2 : FOUND
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
phase: 05-nuxt-content-setup-renderer
|
||||||
|
plan: "02"
|
||||||
|
subsystem: mdc-components
|
||||||
|
tags: [prose-img, alert, mdc, shiki, test-articles]
|
||||||
|
dependency_graph:
|
||||||
|
requires: ['01']
|
||||||
|
provides: [ProseImg, Alert, ProsePre, test-articles]
|
||||||
|
affects:
|
||||||
|
- app/components/content/ProseImg.vue
|
||||||
|
- app/components/content/Alert.vue
|
||||||
|
- app/components/content/ProsePre.vue
|
||||||
|
- app/components/content/Clear.vue
|
||||||
|
- app/components/content/Columns.vue
|
||||||
|
- app/components/content/Details.vue
|
||||||
|
- app/components/content/Badge.vue
|
||||||
|
- app/components/content/Video.vue
|
||||||
|
- content/fr/blog/test-kotlin-syntax.md
|
||||||
|
- content/en/blog/test-kotlin-syntax.md
|
||||||
|
- nuxt.config.ts
|
||||||
|
- app/assets/css/main.css
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- app/components/content/ProseImg.vue
|
||||||
|
- app/components/content/Alert.vue
|
||||||
|
- app/components/content/ProsePre.vue
|
||||||
|
- app/components/content/Clear.vue
|
||||||
|
- app/components/content/Columns.vue
|
||||||
|
- app/components/content/Details.vue
|
||||||
|
- app/components/content/Badge.vue
|
||||||
|
- app/components/content/Video.vue
|
||||||
|
- content/fr/blog/test-kotlin-syntax.md
|
||||||
|
- content/en/blog/test-kotlin-syntax.md
|
||||||
|
modified:
|
||||||
|
- nuxt.config.ts
|
||||||
|
- app/assets/css/main.css
|
||||||
|
decisions:
|
||||||
|
- "Alert.vue: SVG inline à la place de UAlert — incompatibilité couleurs Nuxt UI v3 avec prose"
|
||||||
|
- "ProseImg.vue: span.block à la place de figure — évite block-in-p HTML invalide (SSR hydration mismatch)"
|
||||||
|
- "ProseImg.vue: inheritAttrs false — les classes MDC custom ne surchargent pas le layout auto"
|
||||||
|
- "Shiki: single theme github-dark (jamais dual-theme) — blocs code toujours dark indépendamment du mode UI"
|
||||||
|
- "ProsePre.vue: bg-[#0d1117] hardcodé sur le wrapper div, pre bg-transparent"
|
||||||
|
- "Composants bonus: Columns, Details, Badge, Video, Clear — hors scope initial, ajoutés pour richesse MDC"
|
||||||
|
metrics:
|
||||||
|
duration: "~2 sessions"
|
||||||
|
completed: "2026-04-21"
|
||||||
|
tasks_completed: 2
|
||||||
|
tasks_total: 2
|
||||||
|
files_created: 10
|
||||||
|
files_modified: 2
|
||||||
|
checkpoint: "approved"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 05 Plan 02: MDC Components & Test Articles Summary
|
||||||
|
|
||||||
|
Création des composants de rendu markdown (ProseImg, Alert, ProsePre) et des articles de test bilingues FR/EN validant les 4 success criteria de la phase. Plusieurs composants MDC bonus ont été ajoutés (Columns, Details, Badge, Video, Clear).
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
|
||||||
|
| Task | Name | Commits | Files |
|
||||||
|
|------|------|---------|-------|
|
||||||
|
| 1 | Créer ProseImg.vue et Alert.vue | c9a14a9 → b0af1d3 | ProseImg.vue, Alert.vue |
|
||||||
|
| 2 | Créer articles de test FR/EN | 0fa19a7 | test-kotlin-syntax.md ×2 |
|
||||||
|
| — | ProsePre override dark bg | f179d64 | ProsePre.vue, main.css |
|
||||||
|
| — | Composants MDC bonus | 60e05f7 | Columns/Details/Video/Badge/Clear |
|
||||||
|
| — | Fix: Shiki single dark theme | c5be72b | nuxt.config.ts, main.css |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
1. **Alert.vue sans UAlert** — UAlert de Nuxt UI v3 ne supporte pas les variantes de couleur arbitraires dans le contexte prose. Solution : `<div>` + SVG inline + Tailwind classes directes. Résultat visuellement identique à la spec.
|
||||||
|
|
||||||
|
2. **ProseImg `<span class="block">` au lieu de `<figure>`** — `<figure>` est un élément block. Quand Shiki enveloppe `![img]()` dans un `<p>`, un block-in-p produit un HTML invalide. Le navigateur restrucutre le DOM au parse, créant un mismatch de hydration SSR. `<span class="block">` est valide dans un `<p>`.
|
||||||
|
|
||||||
|
3. **Shiki single theme `github-dark`** — La configuration dual-theme (`default: github-light`) injectait un fond blanc via les CSS variables `--shiki-default` en light mode. Passer à un seul thème `github-dark` garantit que les blocs de code restent toujours dark, indépendamment du mode couleur de l'interface.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Alert.vue: SVG inline vs UAlert
|
||||||
|
- **Planned**: `<UAlert :color="..." variant="soft">`
|
||||||
|
- **Actual**: `<div>` + 4 icônes SVG inline + classes Tailwind
|
||||||
|
- **Reason**: UAlert ne supporte pas les couleurs `info/warning/tip/danger` comme color tokens dans Nuxt UI v3. Le résultat visuel est conforme à la UI-SPEC.
|
||||||
|
|
||||||
|
### Composants MDC bonus hors scope
|
||||||
|
- **Added**: `Columns.vue`, `Details.vue`, `Badge.vue`, `Video.vue`, `Clear.vue`
|
||||||
|
- **Reason**: Nécessaires pour un article de showcase complet et le futur contenu Hytale
|
||||||
|
|
||||||
|
## Checkpoint Visual — Approved
|
||||||
|
|
||||||
|
Validation humaine effectuée sur `http://localhost:3000/test` :
|
||||||
|
- [x] Bloc Kotlin coloré (github-dark, fond #0d1117)
|
||||||
|
- [x] Image rendue via `<img>` avec lazy loading
|
||||||
|
- [x] Tableau markdown avec prose styling
|
||||||
|
- [x] Callouts info/warning/tip/danger fonctionnels
|
||||||
|
- [x] Columns, Details, Badge inline, Clear — fonctionnels
|
||||||
|
- [ ] Vidéo YouTube — non fonctionnelle (hors scope de correction)
|
||||||
|
- [x] Mode light/dark sans impact sur les blocs code (fix appliqué)
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
status: testing
|
||||||
|
phase: 05-nuxt-content-setup-renderer
|
||||||
|
source: [05-01-SUMMARY.md, 05-02-SUMMARY.md]
|
||||||
|
started: 2026-04-21T00:00:00.000Z
|
||||||
|
updated: 2026-04-21T00:00:00.000Z
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Test
|
||||||
|
|
||||||
|
number: 3
|
||||||
|
name: Images optimisées via NuxtImg
|
||||||
|
expected: |
|
||||||
|
Sur `/test`, l'image référencée dans l'article est visible (pas de 404).
|
||||||
|
L'élément DOM rendu est `<img>` avec attribut `loading="lazy"`.
|
||||||
|
awaiting: user response
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### 1. Serveur démarre sans erreur
|
||||||
|
expected: `pnpm dev` lance Nuxt 4 sur :3000 sans erreur de console liée à @nuxt/content, SQLite ou @tailwindcss/typography. La page d'accueil se charge normalement.
|
||||||
|
result: pass
|
||||||
|
|
||||||
|
### 2. Blocs de code toujours dark
|
||||||
|
expected: Naviguer vers `/test`. Le bloc Kotlin affiché a un fond sombre (#0d1117) ET des tokens colorés — que ce soit en mode dark ou en mode light (toggle). En light mode, le fond du bloc reste sombre, pas blanc.
|
||||||
|
result: pass
|
||||||
|
|
||||||
|
### 3. Images optimisées via NuxtImg
|
||||||
|
expected: Sur `/test`, l'image référencée dans l'article est visible (pas de 404). Inspecter le DOM : l'élément rendu est `<img>` avec attribut `loading="lazy"`. ProseImg.vue est l'override utilisé.
|
||||||
|
result: pass
|
||||||
|
|
||||||
|
### 4. Tableau markdown avec prose styling
|
||||||
|
expected: Sur `/test`, le tableau markdown est rendu avec des bordures visibles, un en-tête distingué et une mise en forme prose correcte (pas du texte brut).
|
||||||
|
result: pass
|
||||||
|
|
||||||
|
### 5. Callouts Alert (4 types)
|
||||||
|
expected: Sur `/test`, les 4 callouts `::alert{type}` sont rendus comme des boîtes colorées avec icônes : info (bleu), warning (amber), tip (vert), danger (rouge).
|
||||||
|
result: pass
|
||||||
|
|
||||||
|
### 6. Articles bilingues accessibles
|
||||||
|
expected: Les articles de test existent pour FR et EN. Naviguer vers `/blog/test-kotlin-syntax` (FR) et `/en/blog/test-kotlin-syntax` (EN) — les deux pages chargent sans 404.
|
||||||
|
result: [pending]
|
||||||
|
|
||||||
|
### 7. Collections @nuxt/content configurées
|
||||||
|
expected: Le fichier `content.config.ts` définit `blog_fr` et `blog_en`. `queryCollection('blog_fr')` retourne les articles FR. Vérifiable via le bon rendu de `/test` (qui query `blog_fr`).
|
||||||
|
result: [pending]
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
total: 7
|
||||||
|
passed: 0
|
||||||
|
issues: 0
|
||||||
|
pending: 7
|
||||||
|
skipped: 0
|
||||||
|
blocked: 0
|
||||||
|
|
||||||
|
## Gaps
|
||||||
|
|
||||||
|
[none yet]
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@nuxt/ui";
|
@import "@nuxt/ui";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
/* Offset anchor scroll for sticky header (h-16 = 64px + 16px breathing room) */
|
||||||
|
.prose :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
scroll-margin-top: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks: single dark theme (github-dark), background transparent — ProsePre div owns #0d1117 */
|
||||||
|
pre span {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-50: #f0faf0;
|
--color-brand-50: #f0faf0;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
type?: 'info' | 'warning' | 'tip' | 'danger'
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), { type: 'info' })
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
info: {
|
||||||
|
wrapper: 'border-blue-500 bg-blue-50 dark:bg-blue-950/40',
|
||||||
|
icon: 'text-blue-500',
|
||||||
|
text: 'text-blue-900 dark:text-blue-100',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
wrapper: 'border-amber-500 bg-amber-50 dark:bg-amber-950/40',
|
||||||
|
icon: 'text-amber-500',
|
||||||
|
text: 'text-amber-900 dark:text-amber-100',
|
||||||
|
},
|
||||||
|
tip: {
|
||||||
|
wrapper: 'border-emerald-500 bg-emerald-50 dark:bg-emerald-950/40',
|
||||||
|
icon: 'text-emerald-500',
|
||||||
|
text: 'text-emerald-900 dark:text-emerald-100',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
wrapper: 'border-red-500 bg-red-50 dark:bg-red-950/40',
|
||||||
|
icon: 'text-red-500',
|
||||||
|
text: 'text-red-900 dark:text-red-100',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'not-prose my-4 flex items-start gap-3 rounded-r-lg border-l-4 px-4 py-3',
|
||||||
|
styles[props.type].wrapper,
|
||||||
|
]"
|
||||||
|
role="note"
|
||||||
|
>
|
||||||
|
<!-- Info -->
|
||||||
|
<svg
|
||||||
|
v-if="props.type === 'info'"
|
||||||
|
:class="['mt-0.5 size-5 shrink-0', styles[props.type].icon]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Warning -->
|
||||||
|
<svg
|
||||||
|
v-else-if="props.type === 'warning'"
|
||||||
|
:class="['mt-0.5 size-5 shrink-0', styles[props.type].icon]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Tip -->
|
||||||
|
<svg
|
||||||
|
v-else-if="props.type === 'tip'"
|
||||||
|
:class="['mt-0.5 size-5 shrink-0', styles[props.type].icon]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12 .75a8.25 8.25 0 00-4.135 15.39c.686.398 1.115 1.008 1.134 1.623a.75.75 0 00.577.706c.352.083.71.148 1.074.195.323.041.6-.218.6-.544v-4.661a6.75 6.75 0 01-.937-.171.75.75 0 11.374-1.453 5.261 5.261 0 002.626 0 .75.75 0 11.374 1.452 6.76 6.76 0 01-.937.172v4.66c0 .327.277.586.6.545.364-.047.722-.112 1.074-.195a.75.75 0 00.577-.706c.02-.615.448-1.225 1.134-1.623A8.25 8.25 0 0012 .75z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M9.013 19.9a.75.75 0 01.877-.597 11.319 11.319 0 004.22 0 .75.75 0 11.28 1.473 12.819 12.819 0 01-4.78 0 .75.75 0 01-.597-.876zM9.754 22.344a.75.75 0 01.824-.668 13.682 13.682 0 002.844 0 .75.75 0 11.156 1.492 15.156 15.156 0 01-3.156 0 .75.75 0 01-.668-.824z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Danger -->
|
||||||
|
<svg
|
||||||
|
v-else-if="props.type === 'danger'"
|
||||||
|
:class="['mt-0.5 size-5 shrink-0', styles[props.type].icon]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div :class="['text-sm leading-relaxed', styles[props.type].text]">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
color?: 'gray' | 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange'
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), { color: 'gray' })
|
||||||
|
|
||||||
|
const colorClass = {
|
||||||
|
gray: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300',
|
||||||
|
blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
||||||
|
green: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
||||||
|
red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
||||||
|
yellow: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
||||||
|
purple: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300',
|
||||||
|
orange: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium font-mono',
|
||||||
|
colorClass[props.color],
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-prose clear-both" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
cols?: 2 | 3 | 4
|
||||||
|
gap?: 'sm' | 'md' | 'lg'
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
cols: 2,
|
||||||
|
gap: 'md',
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridClass = computed(() => {
|
||||||
|
const cols = { 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4' }[props.cols]
|
||||||
|
const gap = { sm: 'gap-4', md: 'gap-6', lg: 'gap-10' }[props.gap]
|
||||||
|
return `not-prose my-6 grid grid-cols-1 ${cols} ${gap}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="gridClass">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
summary?: string
|
||||||
|
open?: boolean
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
summary: 'Voir plus',
|
||||||
|
open: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<details
|
||||||
|
:open="props.open"
|
||||||
|
class="not-prose my-4 rounded-lg border border-neutral-200 dark:border-neutral-800 overflow-hidden"
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
class="flex cursor-pointer select-none items-center justify-between px-4 py-3
|
||||||
|
text-sm font-medium text-neutral-700 dark:text-neutral-300
|
||||||
|
hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors list-none"
|
||||||
|
>
|
||||||
|
{{ props.summary }}
|
||||||
|
<svg
|
||||||
|
class="size-4 shrink-0 text-neutral-400 transition-transform details-arrow"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="px-4 py-3 prose prose-neutral dark:prose-invert max-w-none
|
||||||
|
prose-code:before:content-none prose-code:after:content-none
|
||||||
|
prose-pre:p-0 prose-pre:bg-transparent text-sm">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
details[open] .details-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
title?: string
|
||||||
|
caption?: string
|
||||||
|
align?: 'left' | 'right' | 'center' | 'full'
|
||||||
|
width?: string | number
|
||||||
|
height?: string | number
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
alt: '',
|
||||||
|
align: 'full',
|
||||||
|
})
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
// Use <span class="block"> instead of <figure> to avoid block-in-<p> invalid HTML
|
||||||
|
// that breaks SSR hydration (browser auto-removes <p> wrapper around block elements).
|
||||||
|
const wrapperClass = computed(() => {
|
||||||
|
const base = 'not-prose my-6'
|
||||||
|
|
||||||
|
if (attrs.class) return `${base} block`
|
||||||
|
|
||||||
|
switch (props.align) {
|
||||||
|
case 'left': return `${base} block float-left mr-6 mb-2 w-1/2 max-w-xs`
|
||||||
|
case 'right': return `${base} block float-right ml-6 mb-2 w-1/2 max-w-xs`
|
||||||
|
case 'center': return `${base} block mx-auto`
|
||||||
|
default: return `${base} block w-full`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperStyle = computed(() => {
|
||||||
|
if (props.width && props.align !== 'full') return `width: ${props.width}px`
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-bind="attrs"
|
||||||
|
:class="wrapperClass"
|
||||||
|
:style="wrapperStyle"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="props.src"
|
||||||
|
:alt="props.alt"
|
||||||
|
:title="props.title || props.caption"
|
||||||
|
loading="lazy"
|
||||||
|
:class="props.align === 'full' && !attrs.class ? 'w-full rounded-lg' : 'rounded-lg'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="props.caption"
|
||||||
|
class="mt-2 block text-center text-xs text-neutral-500 dark:text-neutral-400 italic"
|
||||||
|
>
|
||||||
|
{{ props.caption }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
language?: string
|
||||||
|
filename?: string
|
||||||
|
highlights?: number[]
|
||||||
|
meta?: string
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
language: '',
|
||||||
|
filename: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="not-prose group my-6 overflow-hidden rounded-lg bg-[#0d1117] ring-1 ring-white/10">
|
||||||
|
<!-- Header bar -->
|
||||||
|
<div
|
||||||
|
v-if="props.filename || props.language"
|
||||||
|
class="flex items-center justify-between border-b border-white/10 px-4 py-2"
|
||||||
|
>
|
||||||
|
<span class="text-xs font-medium text-neutral-400">
|
||||||
|
{{ props.filename || props.language }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="props.language && !props.filename"
|
||||||
|
class="rounded bg-white/10 px-1.5 py-0.5 text-[10px] font-mono uppercase tracking-wide text-neutral-500"
|
||||||
|
>
|
||||||
|
{{ props.language }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code content -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<pre class="shiki m-0 bg-transparent p-4 text-sm leading-relaxed"><slot /></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
src: string
|
||||||
|
title?: string
|
||||||
|
aspect?: '16/9' | '4/3' | '1/1'
|
||||||
|
autoplay?: boolean
|
||||||
|
loop?: boolean
|
||||||
|
muted?: boolean
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '',
|
||||||
|
aspect: '16/9',
|
||||||
|
autoplay: false,
|
||||||
|
loop: false,
|
||||||
|
muted: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isYoutube = computed(() =>
|
||||||
|
/youtube\.com|youtu\.be/.test(props.src)
|
||||||
|
)
|
||||||
|
|
||||||
|
const youtubeId = computed(() => {
|
||||||
|
const match = props.src.match(
|
||||||
|
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
|
||||||
|
)
|
||||||
|
return match?.[1] ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const youtubeUrl = computed(() => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
...(props.autoplay ? { autoplay: '1' } : {}),
|
||||||
|
...(props.loop ? { loop: '1', playlist: youtubeId.value } : {}),
|
||||||
|
modestbranding: '1',
|
||||||
|
rel: '0',
|
||||||
|
})
|
||||||
|
return `https://www.youtube.com/embed/${youtubeId.value}?${params}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const aspectClass = computed(() => ({
|
||||||
|
'16/9': 'aspect-video',
|
||||||
|
'4/3': 'aspect-[4/3]',
|
||||||
|
'1/1': 'aspect-square',
|
||||||
|
}[props.aspect]))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<figure class="not-prose my-6 w-full overflow-hidden rounded-lg bg-black">
|
||||||
|
<!-- YouTube embed -->
|
||||||
|
<iframe
|
||||||
|
v-if="isYoutube"
|
||||||
|
:src="youtubeUrl"
|
||||||
|
:title="props.title"
|
||||||
|
:class="['w-full', aspectClass]"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<!-- Local video -->
|
||||||
|
<video
|
||||||
|
v-else
|
||||||
|
:src="props.src"
|
||||||
|
:title="props.title"
|
||||||
|
:class="['w-full', aspectClass]"
|
||||||
|
:autoplay="props.autoplay"
|
||||||
|
:loop="props.loop"
|
||||||
|
:muted="props.muted || props.autoplay"
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
/>
|
||||||
|
<figcaption
|
||||||
|
v-if="props.title"
|
||||||
|
class="bg-neutral-900 px-4 py-2 text-center text-xs text-neutral-400 italic"
|
||||||
|
>
|
||||||
|
{{ props.title }}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const slug = Array.isArray(route.params.slug) ? route.params.slug.join('/') : route.params.slug
|
||||||
|
const path = `/blog/${slug}`
|
||||||
|
|
||||||
|
const { data: page } = await useAsyncData(`blog-${locale.value}-${slug}`, () => {
|
||||||
|
const collection = locale.value === 'fr' ? 'blog_fr' : 'blog_en'
|
||||||
|
return queryCollection(collection).path(path).first()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!page.value) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Article introuvable' })
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: page.value.title,
|
||||||
|
description: page.value.description,
|
||||||
|
ogTitle: page.value.title,
|
||||||
|
ogDescription: page.value.description,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mx-auto max-w-3xl px-4 py-12">
|
||||||
|
<article class="prose dark:prose-invert max-w-none">
|
||||||
|
<ContentRenderer v-if="page" :value="page" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { data: page } = await useAsyncData('test', () =>
|
||||||
|
queryCollection('blog_fr').path('/blog/test-kotlin-syntax').first()
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-white dark:bg-neutral-950">
|
||||||
|
<div class="mx-auto max-w-6xl px-8 py-16">
|
||||||
|
<header class="mb-10">
|
||||||
|
<div class="mb-3 text-xs font-semibold uppercase tracking-widest text-neutral-400">
|
||||||
|
Renderer Test
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-neutral-900 dark:text-white">
|
||||||
|
{{ page?.title }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-neutral-500 dark:text-neutral-400">
|
||||||
|
{{ page?.description }}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article class="prose prose-neutral dark:prose-invert max-w-none
|
||||||
|
prose-headings:font-semibold
|
||||||
|
prose-code:before:content-none prose-code:after:content-none
|
||||||
|
prose-pre:p-0 prose-pre:bg-transparent">
|
||||||
|
<ContentRenderer v-if="page" :value="page" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
||||||
|
|
||||||
|
const blogSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
date: z.string(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineContentConfig({
|
||||||
|
collections: {
|
||||||
|
blog_fr: defineCollection({
|
||||||
|
type: 'page',
|
||||||
|
source: { include: 'fr/blog/**/*.md', prefix: '/blog' },
|
||||||
|
schema: blogSchema,
|
||||||
|
}),
|
||||||
|
blog_en: defineCollection({
|
||||||
|
type: 'page',
|
||||||
|
source: { include: 'en/blog/**/*.md', prefix: '/en/blog' },
|
||||||
|
schema: blogSchema,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: "Test Kotlin Syntax Highlighting"
|
||||||
|
description: "Test article to validate the @nuxt/content renderer"
|
||||||
|
date: "2026-04-21"
|
||||||
|
tags: ["kotlin", "hytale", "test"]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kotlin Code Block
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun main() {
|
||||||
|
println("Hello, Hytale!")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPlugin(name: String): Plugin {
|
||||||
|
return Plugin(name = name, version = "1.0.0")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimized Image
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table
|
||||||
|
|
||||||
|
| Feature | Status | Notes |
|
||||||
|
|---------|--------|-------|
|
||||||
|
| Syntax highlighting | ✅ Active | Kotlin, Java, TypeScript, Shell |
|
||||||
|
| Optimized images | ✅ Active | Via NuxtImg (lazy + srcset) |
|
||||||
|
| Tables | ✅ Active | Prose rendering |
|
||||||
|
| Callouts | ✅ Active | MDC ::alert{type} |
|
||||||
|
|
||||||
|
## Callouts
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
This is an information callout.
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="warning"}
|
||||||
|
This is a warning.
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="tip"}
|
||||||
|
Practical Kotlin development tip.
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="danger"}
|
||||||
|
Critical error — do not ignore.
|
||||||
|
::
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
---
|
||||||
|
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"]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typographie de base
|
||||||
|
|
||||||
|
Paragraphe normal avec du **gras**, de l'*italique*, du ~~barré~~ et du `code inline`.
|
||||||
|
|
||||||
|
Lien simple : [killiandalcin.fr](https://killiandalcin.fr)
|
||||||
|
|
||||||
|
Citation :
|
||||||
|
|
||||||
|
> Les meilleurs plugins Hytale naissent d'une obsession pour les détails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocs de code
|
||||||
|
|
||||||
|
Bloc Kotlin avec coloration syntaxique :
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun createPlugin(name: String): HytalePlugin {
|
||||||
|
return HytalePlugin.builder()
|
||||||
|
.name(name)
|
||||||
|
.version("1.0.0")
|
||||||
|
.onLoad { println("Plugin $name loaded!") }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PluginConfig {
|
||||||
|
name: string
|
||||||
|
version: `${number}.${number}.${number}`
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: PluginConfig = {
|
||||||
|
name: 'hytale-core',
|
||||||
|
version: '1.0.0',
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Shell :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install @hytale/sdk
|
||||||
|
pnpm run build
|
||||||
|
docker build -t portfolio:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
**Pleine largeur (défaut) :**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Centrée avec taille fixe et légende :**
|
||||||
|
|
||||||
|
{align="center" width="120" caption="Logo Killian' DAL-CIN"}
|
||||||
|
|
||||||
|
**Flottant à gauche :**
|
||||||
|
|
||||||
|
{align="left" caption="Float left"}
|
||||||
|
|
||||||
|
Texte qui entoure l'image flottante. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.
|
||||||
|
|
||||||
|
::clear
|
||||||
|
::
|
||||||
|
|
||||||
|
**Flottant à droite :**
|
||||||
|
|
||||||
|
{align="right" caption="Float right"}
|
||||||
|
|
||||||
|
Texte qui entoure l'image flottante à droite. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||||
|
|
||||||
|
::clear
|
||||||
|
::
|
||||||
|
|
||||||
|
**Classes Tailwind directes :**
|
||||||
|
|
||||||
|
{.w-16 .mx-auto .block}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tableaux
|
||||||
|
|
||||||
|
| Composant | Syntaxe | Usage |
|
||||||
|
|-----------|---------|-------|
|
||||||
|
| Alert | `::alert{type="info"}` | Callouts colorés |
|
||||||
|
| Image | `{align="left"}` | Photos flottantes |
|
||||||
|
| Columns | `::columns{cols=2}` | Mise en page |
|
||||||
|
| Details | `::details{summary="Voir"}` | Contenu repliable |
|
||||||
|
| Video | `::video{src="..."}` | Embed YouTube/local |
|
||||||
|
| Badge | `:badge[text]{color="blue"}` | Tags inline |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Callouts
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
**Info** — Message informatif. Supporte le **gras** et le `code inline`.
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="warning"}
|
||||||
|
**Attention** — Vérifiez la compatibilité Nuxt 4 avant d'installer un module.
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="tip"}
|
||||||
|
**Astuce** — Utilisez `pnpm` plutôt que `npm` pour les projets Nuxt (résolution plus rapide).
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="danger"}
|
||||||
|
**Danger** — Ne jamais committer de clés API ou secrets en clair.
|
||||||
|
::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colonnes
|
||||||
|
|
||||||
|
::columns{cols=2}
|
||||||
|
::div
|
||||||
|
**Colonne 1**
|
||||||
|
|
||||||
|
Contenu de la première colonne. Idéal pour comparer deux approches, montrer avant/après, ou lister des avantages et inconvénients.
|
||||||
|
::
|
||||||
|
|
||||||
|
::div
|
||||||
|
**Colonne 2**
|
||||||
|
|
||||||
|
Contenu de la seconde colonne. Les colonnes passent en empilé sur mobile automatiquement.
|
||||||
|
::
|
||||||
|
::
|
||||||
|
|
||||||
|
::columns{cols=3 gap="lg"}
|
||||||
|
::div
|
||||||
|
🚀 **Rapide**
|
||||||
|
|
||||||
|
Nuxt SSR génère le HTML côté serveur.
|
||||||
|
::
|
||||||
|
|
||||||
|
::div
|
||||||
|
🔍 **SEO**
|
||||||
|
|
||||||
|
Chaque page est crawlable sans JavaScript.
|
||||||
|
::
|
||||||
|
|
||||||
|
::div
|
||||||
|
🎨 **Flexible**
|
||||||
|
|
||||||
|
Tailwind + Nuxt UI pour tout styliser.
|
||||||
|
::
|
||||||
|
::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contenu repliable
|
||||||
|
|
||||||
|
::details{summary="Voir l'implémentation complète du plugin"}
|
||||||
|
```kotlin
|
||||||
|
class HytalePlugin(
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
private val onLoad: () -> Unit,
|
||||||
|
) {
|
||||||
|
fun load() {
|
||||||
|
println("Loading plugin: $name v$version")
|
||||||
|
onLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun builder() = Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder {
|
||||||
|
private var name = ""
|
||||||
|
private var version = "0.0.1"
|
||||||
|
private var onLoad: () -> Unit = {}
|
||||||
|
|
||||||
|
fun name(n: String) = apply { name = n }
|
||||||
|
fun version(v: String) = apply { version = v }
|
||||||
|
fun onLoad(fn: () -> Unit) = apply { onLoad = fn }
|
||||||
|
fun build() = HytalePlugin(name, version, onLoad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
::details{summary="Pourquoi utiliser @nuxt/content ?" open=true}
|
||||||
|
`@nuxt/content` transforme des fichiers Markdown en pages SSR crawlables. Avantages :
|
||||||
|
|
||||||
|
- **Zéro CMS** — les articles sont dans le repo Git
|
||||||
|
- **Typé** — schema Zod sur chaque collection
|
||||||
|
- **MDC** — composants Vue dans le Markdown
|
||||||
|
::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Badges inline
|
||||||
|
|
||||||
|
Versions : :badge[v1.0]{color="green"} :badge[v0.9 LTS]{color="blue"} :badge[deprecated]{color="red"}
|
||||||
|
|
||||||
|
Statuts : :badge[stable]{color="green"} :badge[beta]{color="yellow"} :badge[wip]{color="orange"}
|
||||||
|
|
||||||
|
Technologies : :badge[Kotlin]{color="purple"} :badge[Nuxt 4]{color="green"} :badge[TypeScript]{color="blue"}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vidéo
|
||||||
|
|
||||||
|
**YouTube :**
|
||||||
|
|
||||||
|
::video{src="https://www.youtube.com/watch?v=dQw4w9WgXcQ" title="Exemple d'embed YouTube"}
|
||||||
|
::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Listes
|
||||||
|
|
||||||
|
Liste non ordonnée :
|
||||||
|
|
||||||
|
- Premier point
|
||||||
|
- Deuxième point avec `code`
|
||||||
|
- Troisième point avec **gras**
|
||||||
|
- Sous-point imbriqué
|
||||||
|
- Autre sous-point
|
||||||
|
|
||||||
|
Liste ordonnée :
|
||||||
|
|
||||||
|
1. Installer les dépendances : `pnpm install`
|
||||||
|
2. Lancer le dev server : `pnpm dev`
|
||||||
|
3. Builder pour la prod : `pnpm build`
|
||||||
+20
-1
@@ -10,7 +10,8 @@ export default defineNuxtConfig({
|
|||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
'@nuxtjs/sitemap',
|
'@nuxtjs/sitemap',
|
||||||
'nuxt-gtag',
|
'nuxt-gtag',
|
||||||
'@nuxt/image'
|
'@nuxt/image',
|
||||||
|
'@nuxt/content'
|
||||||
],
|
],
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
@@ -61,5 +62,23 @@ export default defineNuxtConfig({
|
|||||||
gtag: {
|
gtag: {
|
||||||
id: '',
|
id: '',
|
||||||
enabled: import.meta.env.NODE_ENV === 'production',
|
enabled: import.meta.env.NODE_ENV === 'production',
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['zod'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
build: {
|
||||||
|
markdown: {
|
||||||
|
highlight: {
|
||||||
|
theme: 'github-dark',
|
||||||
|
langs: ['kotlin', 'java', 'typescript', 'shell', 'bash', 'json', 'vue', 'html', 'css']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
sqliteConnector: 'native'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"typecheck": "nuxt typecheck"
|
"typecheck": "nuxt typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/content": "^3.13.0",
|
||||||
"@nuxt/eslint": "^1.15.2",
|
"@nuxt/eslint": "^1.15.2",
|
||||||
"@nuxt/image": "^2.0.0",
|
"@nuxt/image": "^2.0.0",
|
||||||
"@nuxt/ui": "^3.0.0",
|
"@nuxt/ui": "^3.0.0",
|
||||||
@@ -26,6 +27,8 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.102",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/nodemailer": "^8.0.0",
|
"@types/nodemailer": "^8.0.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "~5.8.0"
|
"typescript": "~5.8.0"
|
||||||
|
|||||||
Generated
+2029
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user