rubis/apps/api/commands/seed_blog.ts
ordinarthur e5530930b3
Some checks failed
Build & Deploy API / build-and-deploy (push) Failing after 17s
Build & Deploy Web / build-and-deploy (push) Successful in 1m15s
Build & Deploy Landing / build-and-deploy (push) Failing after 3m43s
feat: refactor frontend en stack React unifiée (Astro + packages/ui)
Trois surfaces partagent désormais le même design system, Tailwind v4
et React 19 — au lieu d'avoir landing en HTML vanilla, app en React, et
blog en Adonis SSR :

* packages/ui — design system partagé (tokens Tailwind v4 + composants
  TSX) extrait depuis apps/web : Brand, Gem, Button, Card, Chip, Eyebrow,
  EmptyState. apps/web migre 41 imports vers @rubis/ui.

* apps/landing — nouvelle app Astro 6 SSR (rubis.pro), remplace l'ancienne
  landing nginx vanilla. Embarque :
  - Landing complète portée en sections React (Hero, Stats, Promise,
    HowItWorks, Gamification, Legal, Pricing, FAQ, FinalCTA, Footnotes)
  - Pages légales (mentions, confidentialité, CGV) via LegalLayout.astro
  - Blog SSR (/blog, /blog/:slug) qui consomme /api/v1/posts
  - sitemap.xml, blog/rss.xml, robots.txt en endpoints Astro
  - SEO complet (canonical, hreflang, OG, Twitter Card, JSON-LD
    Article/BreadcrumbList/Blog/SoftwareApplication)

* apps/api — BlogController réduit à 2 endpoints JSON (GET /api/v1/posts
  + GET /api/v1/posts/:slug). Suppression des templates SSR Adonis
  (apps/api/app/blog/), de l'alias #blog/*, des deps react-dom et
  @types/react-dom. PostTransformer + PostSummaryTransformer ajoutés.
  Le service blog_renderer + le seeder + les 3 articles fondateurs
  restent intacts (réutilisés par futurs admin + cron IA).

* Infra :
  - Dockerfile.landing (multi-stage Node 22 + tini, Astro standalone)
  - k3s/app/landing.yml (Deployment + Service rubis-landing:4321 +
    ConfigMap avec API_URL=http://rubis-api.rubis.svc.cluster.local:3333)
  - .gitea/workflows/deploy.yml mis à jour pour build rubis-landing
  - .gitea/workflows/deploy-web.yml + Dockerfile.web : prennent en
    compte packages/ui/ comme dépendance
  - Suppression du Dockerfile nginx legacy + k3s/{deployment,service}.yml
  - Suppression de landing/ (assets favicons migrés vers
    apps/landing/public/)

* Docs : architecture.md (vue d'ensemble + §4bis apps/landing complet,
  §3 endpoints JSON blog, layout monorepo), CLAUDE.md (stack technique,
  documents associés, déploiement).

Note infra : l'ancien Deployment "rubis" (nginx) et son Service ne sont
PAS supprimés par la CI — à nettoyer manuellement après validation que
Traefik a été repointé sur rubis-landing:4321 dans le repo proxmox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:09:13 +02:00

78 lines
2.4 KiB
TypeScript

import { BaseCommand, flags } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'
import { DateTime } from 'luxon'
import Post from '#models/post'
import { renderPost } from '#services/blog_renderer'
import { seedArticles } from '#database/seeders/blog_seed/index'
/**
* Insère / met à jour les articles fondateurs du blog en DB.
*
* node ace seed:blog # idempotent, upsert par slug
* node ace seed:blog --reset # supprime tous les posts avant
*
* À lancer une fois en local et en prod après le déploiement de PR1
* (avant la mise en route du routing Traefik en PR2). Les ré-exécutions
* sont sans effet de bord — utile si on retouche les MD source.
*/
export default class SeedBlog extends BaseCommand {
static commandName = 'seed:blog'
static description = 'Seed des 3 articles fondateurs du blog (idempotent par slug)'
static options: CommandOptions = {
startApp: true,
}
@flags.boolean({
description: 'Supprime tous les posts existants avant le seed',
default: false,
})
declare reset: boolean
async run() {
if (this.reset) {
const deleted = await Post.query().delete()
this.logger.warning(`${deleted} posts supprimés (--reset).`)
}
let created = 0
let updated = 0
for (const draft of seedArticles) {
const { contentHtml, wordCount, readingTimeMinutes } = renderPost(draft.contentMd)
const publishedAt = DateTime.now().minus({ days: draft.publishedDaysAgo }).toUTC().startOf('minute')
const existing = await Post.findBy('slug', draft.slug)
const payload = {
slug: draft.slug,
title: draft.title,
excerpt: draft.excerpt,
contentMd: draft.contentMd,
contentHtml,
authorName: draft.authorName,
tags: draft.tags,
status: 'published' as const,
publishedAt,
wordCount,
readingTimeMinutes,
aiGenerated: false,
noindex: false,
}
if (existing) {
existing.merge(payload)
await existing.save()
updated += 1
} else {
await Post.create(payload)
created += 1
}
this.logger.success(`${draft.slug} (${wordCount} mots, ${readingTimeMinutes} min)`)
}
this.logger.info(`\nFait : ${created} créé(s), ${updated} mis à jour, ${seedArticles.length} total.`)
}
}