commit 72641942b81187a04b635875a6fbbc35ddfe832f Author: Paul Atlan Date: Wed Apr 15 01:00:56 2026 +0200 Initial commit: blog IA qu'à... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2df98f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# build +dist/ +.astro/ + +# deps +node_modules/ + +# env +.env +.env.* + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad11e7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# ── Stage 1 : Build Astro ── +FROM node:20-alpine AS build + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# ── Stage 2 : Serve static files ── +FROM nginx:alpine + +# Config nginx optimisée pour un site statique +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copier le build Astro +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dce2152 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# IA qu'à... — Blog + +Blog statique personnel sur l'IA en PME. + +## Stack + +- **Astro** — Générateur de site statique +- **Tailwind CSS** — Styles utilitaires +- **MDX** — Articles en Markdown enrichi +- **Docker + nginx** — Hébergé sur Cloudbreak (Hetzner), derrière Caddy + +## Démarrage + +```bash +npm install +npm run dev # Serveur local → http://localhost:4321 +npm run build # Build de production → dist/ +npm run preview # Prévisualiser le build +``` + +## Structure + +``` +src/ +├── content/blog/ ← Articles en MDX +├── components/ ← Composants Astro (Navbar, Footer, SeriesNav...) +├── layouts/ ← Layouts (BaseLayout, BlogPost) +├── pages/ ← Pages (index, à-propos, séries) +└── styles/ ← CSS global + Tailwind +``` + +## Écrire un article + +Créer un fichier `.mdx` dans `src/content/blog/` : + +```yaml +--- +title: "Mon titre" +description: "Description courte" +pubDate: 2026-04-13 +category: "Outils IA" # ou "Organisation & process", "Retours terrain", "Veille & tendances" +series: # optionnel + name: "Nom de la série" + part: 1 + total: 4 +--- +``` + +## Déploiement + +```bash +# Sur Cloudbreak : +cd /opt/ia-qua-blog # adapter le chemin +sudo bash deploy.sh # git pull + docker build + up +``` diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..ccd8d5a --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from "astro/config"; +import mdx from "@astrojs/mdx"; +import sitemap from "@astrojs/sitemap"; +import tailwind from "@astrojs/tailwind"; + +export default defineConfig({ + site: "https://ia-qua.fr", // À changer si autre domaine + integrations: [mdx(), sitemap(), tailwind()], + markdown: { + shikiConfig: { + theme: "github-dark", + }, + }, +}); diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..5e99aa3 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── deploy.sh — Blog IA qu'à... ─── +# Usage : sudo bash deploy.sh +# Depuis le répertoire du repo sur Cloudbreak + +APP_NAME="ia-qua-blog" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "═══════════════════════════════════" +echo " Déploiement : $APP_NAME" +echo "═══════════════════════════════════" + +cd "$SCRIPT_DIR" + +# 1. Pull les dernières modifs +echo "" +echo "→ Git pull..." +git pull + +# 2. Build + restart +echo "" +echo "→ Build & restart containers..." +docker compose down --remove-orphans 2>/dev/null || true +docker compose build --no-cache +docker compose up -d + +# 3. Attente + status +echo "" +echo "→ Attente 10s..." +sleep 10 + +echo "" +echo "→ Status :" +docker compose ps + +echo "" +echo "→ Logs récents :" +docker compose logs --tail=20 + +echo "" +echo "═══════════════════════════════════" +echo " ✓ $APP_NAME déployé" +echo "═══════════════════════════════════" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c412c4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + blog: + build: . + container_name: ia-qua-blog + restart: unless-stopped + ports: + - "8090:80" + networks: + - caddy-public + +networks: + caddy-public: + external: true diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..65db140 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,2 @@ +# Ce fichier n'est plus utilisé — le blog est hébergé sur Cloudbreak (Hetzner). +# À supprimer du repo. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..4d713ba --- /dev/null +++ b/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Cache long pour les assets (JS, CSS, images, fonts) + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback — Astro génère du statique, mais au cas où + location / { + try_files $uri $uri/ /index.html; + } + + # Pas de log pour favicon/robots + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + # Compression gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; + gzip_min_length 256; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cfce14c --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "ia-qua-blog", + "type": "module", + "version": "1.0.0", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "astro": "^5.0.0", + "@astrojs/mdx": "^4.0.0", + "@astrojs/sitemap": "^3.0.0", + "@astrojs/tailwind": "^6.0.0", + "tailwindcss": "^4.0.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..4637905 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + IA + diff --git a/src/components/ArticleCard.astro b/src/components/ArticleCard.astro new file mode 100644 index 0000000..34749d4 --- /dev/null +++ b/src/components/ArticleCard.astro @@ -0,0 +1,40 @@ +--- +import SeriesBadge from "./SeriesBadge.astro"; + +interface Props { + title: string; + description: string; + pubDate: Date; + category: string; + heroImage?: string; + slug: string; + series?: { name: string; part: number; total: number }; +} + +const { title, description, pubDate, category, heroImage, slug, series } = Astro.props; + +const formattedDate = pubDate.toLocaleDateString("fr-FR", { + day: "numeric", + month: "long", + year: "numeric", +}); +--- + +
+ + {heroImage ? ( + {title} + ) : ( +
+ +
+ )} +
+ {series &&
} + {category} +

{title}

+

{description}

+ {formattedDate} +
+
+
diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..7338e28 --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,13 @@ +--- +const year = new Date().getFullYear(); +--- + + diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro new file mode 100644 index 0000000..33e2dc5 --- /dev/null +++ b/src/components/Navbar.astro @@ -0,0 +1,67 @@ +--- +const currentPath = Astro.url.pathname; + +const links = [ + { href: "/", label: "Accueil" }, + { href: "/categorie/outils-ia", label: "Outils IA" }, + { href: "/categorie/organisation", label: "Organisation" }, + { href: "/categorie/retours-terrain", label: "Retours terrain" }, + { href: "/series", label: "Séries" }, + { href: "/a-propos", label: "À propos" }, +]; +--- + + + + diff --git a/src/components/SeriesBadge.astro b/src/components/SeriesBadge.astro new file mode 100644 index 0000000..d581b1a --- /dev/null +++ b/src/components/SeriesBadge.astro @@ -0,0 +1,23 @@ +--- +interface Props { + name: string; + part: number; + total: number; +} + +const { name, part, total } = Astro.props; +const dots = Array.from({ length: total }, (_, i) => i < part); +--- + +
+ + + + + Série : {name} +
+ {dots.map((filled) => ( +
+ ))} +
+
diff --git a/src/components/SeriesNav.astro b/src/components/SeriesNav.astro new file mode 100644 index 0000000..eb4d46f --- /dev/null +++ b/src/components/SeriesNav.astro @@ -0,0 +1,73 @@ +--- +/** + * SeriesNav — navigation inter-articles dans une série. + * Affiche la liste ordonnée des articles de la série, + * avec un indicateur visuel sur l'article courant. + */ +import { getCollection } from "astro:content"; + +interface Props { + seriesName: string; + currentPart: number; +} + +const { seriesName, currentPart } = Astro.props; + +// Récupère tous les articles de la même série +const allPosts = await getCollection("blog", ({ data }) => { + return data.series?.name === seriesName && !data.draft; +}); + +const seriesPosts = allPosts.sort( + (a, b) => (a.data.series?.part ?? 0) - (b.data.series?.part ?? 0) +); +--- + +{seriesPosts.length > 1 && ( + +)} diff --git a/src/content/blog/3-mois-copilot-bilan.mdx b/src/content/blog/3-mois-copilot-bilan.mdx new file mode 100644 index 0000000..a513646 --- /dev/null +++ b/src/content/blog/3-mois-copilot-bilan.mdx @@ -0,0 +1,28 @@ +--- +title: "3 mois avec Copilot : le bilan honnête" +description: "On l'a déployé pour 5 utilisateurs. Voici les vrais chiffres, pas ceux du commercial Microsoft." +pubDate: 2026-04-02 +category: "Retours terrain" +--- + +## Le setup + +5 licences Microsoft 365 Copilot, déployées pour des profils variés : un commercial, deux chargés d'affaires, une personne à l'ADV, et moi. + +Budget : 30€/utilisateur/mois, soit 150€/mois pour le pilote. + +## Ce qui marche vraiment + +Le résumé de mails et de réunions Teams. C'est le use case où le ROI est le plus immédiat et le moins discutable. Quand vous revenez de vacances avec 200 mails, avoir un résumé par fil de discussion, c'est un vrai gain. + +## Ce qui marche... moyen + +La génération de documents Word et PowerPoint. C'est impressionnant en démo, mais en pratique on passe presque autant de temps à corriger qu'à écrire from scratch. + +## Ce qui ne marche pas + +Excel. Le commercial espérait que Copilot allait lui faire ses tableaux croisés dynamiques. En réalité, il faut déjà savoir ce qu'on veut pour que l'IA le fasse. + +## Le verdict + +3 licences sur 5 renouvelées. Les deux autres n'ont pas trouvé de use case qui justifie 30€/mois. C'est honnête. diff --git a/src/content/blog/cahier-des-charges-gpt4-part1.mdx b/src/content/blog/cahier-des-charges-gpt4-part1.mdx new file mode 100644 index 0000000..2314673 --- /dev/null +++ b/src/content/blog/cahier-des-charges-gpt4-part1.mdx @@ -0,0 +1,36 @@ +--- +title: "Peut-on faire rédiger un CdC par GPT-4 ? (1/4)" +description: "Première tentative : on donne un brief brut à GPT-4 et on regarde ce qui sort. Spoiler : c'est à la fois bluffant et inutilisable." +pubDate: 2026-03-25 +category: "Outils IA" +series: + name: "IA et cahiers des charges" + part: 1 + total: 4 +--- + +## Le contexte + +On avait un projet interne à spécifier : un outil de reporting pour l'équipe commerciale. Le genre de CdC qu'on écrit habituellement en 2-3 jours, avec beaucoup d'allers-retours. + +L'idée : et si on demandait à GPT-4 de produire une première version ? + +## Le protocole + +J'ai donné à GPT-4 un brief d'une page. Pas un prompt sophistiqué — juste ce que j'aurais envoyé par mail à un prestataire. Et j'ai demandé un cahier des charges structuré. + +## Ce qui en est sorti + +Le résultat faisait 12 pages. Bien structuré, avec des sections qu'on retrouve dans un vrai CdC : contexte, objectifs, périmètre fonctionnel, contraintes techniques, planning. + +Mais en y regardant de plus près... + +## Les problèmes + +Le document était **générique**. Pas faux, mais pas spécifique non plus. Les sections "contraintes techniques" mentionnaient des bonnes pratiques générales, pas *nos* contraintes réelles. Le planning était fantaisiste. + +C'est le piège classique : ça a l'air pro, ça utilise le bon vocabulaire, mais c'est du remplissage structuré. + +## La suite + +Dans le prochain article, on teste une approche différente : au lieu de demander un CdC complet, on utilise l'IA comme *sparring partner* pour challenger notre propre brief. diff --git a/src/content/blog/cahier-des-charges-gpt4-part2.mdx b/src/content/blog/cahier-des-charges-gpt4-part2.mdx new file mode 100644 index 0000000..e6278be --- /dev/null +++ b/src/content/blog/cahier-des-charges-gpt4-part2.mdx @@ -0,0 +1,22 @@ +--- +title: "L'IA comme sparring partner pour un CdC (2/4)" +description: "On a changé d'approche : au lieu de lui faire écrire le CdC, on lui a fait challenger le nôtre. Les résultats sont bien meilleurs." +pubDate: 2026-04-12 +category: "Outils IA" +series: + name: "IA et cahiers des charges" + part: 2 + total: 4 +--- + +## Le pivot + +Après l'échec relatif de la première tentative, j'ai changé de stratégie. Au lieu de demander à l'IA d'écrire, je lui ai demandé de **lire et critiquer**. + +J'ai repris notre CdC interne (celui qu'on avait rédigé nous-mêmes) et je l'ai soumis à Claude avec cette consigne : *"Tu es un prestataire qui reçoit ce cahier des charges. Quelles questions poses-tu avant de chiffrer ?"* + +## Ce que ça a donné + +15 questions. Dont 8 qu'on ne s'était jamais posées. + +*À suivre dans la partie 3...* diff --git a/src/content/blog/convaincre-codir-claude.mdx b/src/content/blog/convaincre-codir-claude.mdx new file mode 100644 index 0000000..d151b83 --- /dev/null +++ b/src/content/blog/convaincre-codir-claude.mdx @@ -0,0 +1,22 @@ +--- +title: "Comment j'ai convaincu le Codir de tester Claude" +description: "Spoiler : ce n'était pas gagné. Retour sur les arguments qui ont marché et ceux qui ont fait un flop monumental." +pubDate: 2026-04-08 +category: "Organisation & process" +--- + +## Le contexte + +Quand vous êtes DSI d'une PME et que vous dites « on devrait tester l'IA », vous obtenez deux types de réactions. La première : « Ah oui, comme ChatGPT ? Mon fils l'utilise pour ses devoirs. » La deuxième : « C'est pas un peu dangereux pour la confidentialité ? » + +Aucune des deux ne vous aide à obtenir un budget. + +## Ce qui n'a PAS marché + +Le pitch techno. Parler de LLMs, de tokens, de fine-tuning — les yeux du DG se sont vitrifiés en 30 secondes. + +## Ce qui a marché + +Un tableau avec trois colonnes : la tâche, le temps qu'elle prend aujourd'hui, le temps estimé avec l'outil. Pas de promesses, juste des hypothèses à tester. + +Et surtout : proposer un pilote de 3 mois, avec 5 utilisateurs, et un budget de 200€/mois. Le risque était tellement faible que dire non aurait été plus difficile que dire oui. diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..a16e6d6 --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,30 @@ +import { defineCollection, z } from "astro:content"; + +const blog = defineCollection({ + type: "content", + schema: z.object({ + title: z.string(), + description: z.string(), + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: z.string().optional(), + category: z.enum([ + "Outils IA", + "Organisation & process", + "Retours terrain", + "Veille & tendances", + ]), + // Série (optionnel) + series: z + .object({ + name: z.string(), + part: z.number(), + total: z.number(), + }) + .optional(), + // Draft + draft: z.boolean().default(false), + }), +}); + +export const collections = { blog }; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..a16f9b4 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,63 @@ +--- +import "../styles/global.css"; +import Navbar from "../components/Navbar.astro"; +import Footer from "../components/Footer.astro"; + +interface Props { + title: string; + description?: string; + image?: string; +} + +const { + title, + description = "Journal de bord d'un DSI en PME face à l'IA", + image = "/og-default.png", +} = Astro.props; + +const canonicalURL = new URL(Astro.url.pathname, Astro.site); +--- + + + + + + + + + + + {title} | IA qu'à... + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+