Commit initial
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(python3 build.py)",
|
||||||
|
"WebFetch(domain:scribe-template.webflow.io)",
|
||||||
|
"Bash(python3 -c \"import markdown; md = markdown.Markdown\\(extensions=[''toc''], extension_configs={''toc'': {''permalink'': ''#'', ''permalink_class'': ''heading-anchor'', ''toc_depth'': ''1-4''}}\\); print\\(md.convert\\(''## Sous-titre\\\\n\\\\nTexte.''\\)\\)\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
257
README.md
Normal file
257
README.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# PageMark
|
||||||
|
|
||||||
|
Générateur de site statique minimaliste alimenté par des fichiers Markdown.
|
||||||
|
|
||||||
|
Écrivez du contenu en `.md`, lancez `python3 build.py`, déployez les fichiers HTML générés.
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- Pages et articles en Markdown avec coloration syntaxique
|
||||||
|
- Page d'accueil générée automatiquement (5 derniers articles, du plus récent au plus ancien)
|
||||||
|
- Catégorisation automatique des articles avec arborescence de dossiers
|
||||||
|
- Métadonnées par article : titre, date, auteur, catégorie, image de couverture
|
||||||
|
- Dark mode : automatique selon l'heure (light 08h30–17h30) + toggle manuel mémorisé
|
||||||
|
- Ancres cliquables sur les titres `h1`–`h4`
|
||||||
|
- HTML personnalisé dans les articles
|
||||||
|
- Menu avec filtrage automatique des liens invalides et distinction liens internes/externes
|
||||||
|
- Aucune dépendance JavaScript côté client
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- `pip install markdown`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/votre-repo/pagemark
|
||||||
|
cd pagemark
|
||||||
|
pip install markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
pagemark/
|
||||||
|
├── build.py # Générateur de site (lancer pour builder)
|
||||||
|
├── README.md
|
||||||
|
├── static/
|
||||||
|
│ ├── style.css # Styles (light + dark mode)
|
||||||
|
│ └── theme.js # Gestion du dark mode (localStorage + heure)
|
||||||
|
└── md/
|
||||||
|
├── header.md # Nom du site (# Titre) + tagline (texte suivant)
|
||||||
|
├── footer.md # Pied de page
|
||||||
|
├── menu.md # Navigation (liste Markdown)
|
||||||
|
├── categories.md # Mapping catégories → dossiers de sortie
|
||||||
|
├── images/ # Images référençables via /images/fichier.jpg
|
||||||
|
└── pages/
|
||||||
|
├── index.md # Contenu optionnel affiché au-dessus des articles
|
||||||
|
└── *.md # Vos pages et articles
|
||||||
|
```
|
||||||
|
|
||||||
|
Les fichiers HTML sont générés à la racine du projet.
|
||||||
|
|
||||||
|
## Mise en route rapide
|
||||||
|
|
||||||
|
### 1. Configurer le site
|
||||||
|
|
||||||
|
**`md/header.md`** — la première ligne `# Titre` devient le logo dans la nav, le reste la tagline :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Mon Site
|
||||||
|
Le blog d'un passionné de technologie
|
||||||
|
```
|
||||||
|
|
||||||
|
**`md/footer.md`** — pied de page en Markdown :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
© 2026 Votre Nom
|
||||||
|
```
|
||||||
|
|
||||||
|
**`md/menu.md`** — liste de liens. Les liens internes sont filtrés automatiquement (masqués si la page n'existe pas), les liens externes s'ouvrent dans un nouvel onglet avec le suffixe `↗` :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- [Accueil](/)
|
||||||
|
- [À propos](/racine/a-propos/)
|
||||||
|
- [GitHub](https://github.com/vous)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Le chemin d'un lien interne doit correspondre au chemin de sortie généré (voir section Catégories).
|
||||||
|
|
||||||
|
### 2. Créer une page ou un article
|
||||||
|
|
||||||
|
Créez un fichier `.md` dans `md/pages/`. Les métadonnées se placent en tête de fichier :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
::Titre:Mon premier article
|
||||||
|
::Date:07/03/2026
|
||||||
|
::Auteur:Votre Nom
|
||||||
|
::Categories:Blog
|
||||||
|
::Image:/images/cover.jpg
|
||||||
|
|
||||||
|
Contenu de l'article en Markdown...
|
||||||
|
```
|
||||||
|
|
||||||
|
> Une page sans `::Date:` est une page statique (elle n'apparaît pas dans le listing de la page d'accueil).
|
||||||
|
|
||||||
|
### 3. Générer le site
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 build.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Tester en local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m http.server 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis ouvrez [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Métadonnées
|
||||||
|
|
||||||
|
| Métadonnée | Syntaxe | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| `::Titre:` | `::Titre:Mon titre` | Affiché en `<h1>` avant le contenu |
|
||||||
|
| `::Date:` | `::Date:dd/mm/YYYY` | Date de publication, utilisée pour le tri |
|
||||||
|
| `::Auteur:` | `::Auteur:Pseudo` | Nom de l'auteur |
|
||||||
|
| `::Categories:` | `::Categories:Blog-Tutos` | Catégorie et sous-catégorie |
|
||||||
|
| `::Image:` | `::Image:/images/cover.jpg` | Image de couverture dans les cards |
|
||||||
|
|
||||||
|
Toutes les métadonnées sont optionnelles. Seules les pages avec `::Date:` sont traitées comme des **articles** et apparaissent dans le listing de la page d'accueil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catégories
|
||||||
|
|
||||||
|
Le fichier `md/categories.md` mappe les noms de catégories vers des dossiers de sortie :
|
||||||
|
|
||||||
|
```
|
||||||
|
- Blog:/blog
|
||||||
|
- News:/news
|
||||||
|
- Tutoriels:/tutos
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exemple :** `::Categories:News-Tech`
|
||||||
|
|
||||||
|
1. `News` est résolu via `categories.md` → dossier `news`
|
||||||
|
2. `Tech` (sous-catégorie) → sous-dossier `tech`
|
||||||
|
3. Fichier `mon-article.md` → généré dans `news/tech/mon-article/index.html`
|
||||||
|
4. URL : `/news/tech/mon-article/`
|
||||||
|
|
||||||
|
Si une catégorie n'est pas dans `categories.md`, son nom en minuscules est utilisé comme dossier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
**`md/menu.md`** contient une liste Markdown de liens :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- [Accueil](/)
|
||||||
|
- [Blog](/blog/)
|
||||||
|
- [GitHub](https://github.com/vous)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Type de lien | Comportement |
|
||||||
|
|---|---|
|
||||||
|
| Chemin relatif (`/page/`) | Lien **interne** — affiché uniquement si la page a été générée |
|
||||||
|
| URL complète (`https://`) | Lien **externe** — toujours affiché, suffixe `↗`, `target="_blank"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
Placez vos images dans `md/images/` et référencez-les dans le Markdown :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
Pour l'image de couverture d'un article (affichée dans les cards de la page d'accueil) :
|
||||||
|
|
||||||
|
```
|
||||||
|
::Image:/images/cover.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTML personnalisé
|
||||||
|
|
||||||
|
Du HTML peut être inséré directement dans n'importe quel fichier `.md` :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div style="background: var(--accent-soft); padding: 1rem; border-radius: 8px;">
|
||||||
|
Bloc personnalisé — les variables CSS du thème sont disponibles.
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Les variables CSS `var(--bg)`, `var(--text)`, `var(--accent)`, etc. s'adaptent automatiquement au dark mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dark mode
|
||||||
|
|
||||||
|
| Comportement | Détail |
|
||||||
|
|---|---|
|
||||||
|
| **Par défaut** | Light de 08h30 à 17h30, dark le reste du temps |
|
||||||
|
| **Manuel** | Bouton `☾` / `☀` dans la barre de navigation |
|
||||||
|
| **Mémorisation** | Le choix est sauvegardé via `localStorage` — il prend le dessus sur le comportement automatique |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ancres de titres
|
||||||
|
|
||||||
|
Les titres `h1` à `h4` reçoivent automatiquement un `id` et un lien `#` cliquable au survol. Cela permet de créer des liens directs vers une section :
|
||||||
|
|
||||||
|
```
|
||||||
|
https://monsite.com/mon-article#sous-titre
|
||||||
|
```
|
||||||
|
|
||||||
|
L'`id` est généré à partir du texte du titre (minuscules, tirets à la place des espaces).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Déploiement avec nginx
|
||||||
|
|
||||||
|
Copiez les fichiers générés vers votre serveur. Configuration nginx minimale :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name monsite.com;
|
||||||
|
root /var/www/pagemark;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La directive `try_files $uri $uri/index.html` permet de résoudre `/mon-article/` → `/mon-article/index.html` sans redirection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow recommandé
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Éditer md/pages/mon-article.md
|
||||||
|
2. python3 build.py
|
||||||
|
3. Copier les fichiers générés sur le serveur
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour automatiser le déploiement, un simple script shell suffit :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
python3 build.py && rsync -avz --delete . user@serveur:/var/www/pagemark \
|
||||||
|
--exclude='md/' --exclude='static/' --exclude='*.py' --exclude='*.md' --exclude='.git'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT
|
||||||
128
blog/exemple-article/index.html
Normal file
128
blog/exemple-article/index.html
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Exemple d'article — toutes les fonctionnalités</title>
|
||||||
|
<script src="/static/theme.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link id="hl-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
|
<link id="hl-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="not all">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" defer></script>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded", () => hljs.highlightAll())</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="site-nav"><div class="container nav-container"><a href="/" class="site-logo">Sébastien QUEROL</a><div class="nav-links"><ul><li><a href="/" class="internal">Accueil</a></li><li><a href="/racine/a-propos/" class="internal">A propos</a></li></ul></div><button class="theme-toggle" id="theme-toggle" aria-label="Basculer le thème">☾</button></div></nav>
|
||||||
|
<div class="site-tagline"><div class="container"><p>Le blog d'un SysAdmin</p></div></div>
|
||||||
|
<div class="tagline-divider-wrap"><div class="container"><hr class="tagline-divider"></div></div>
|
||||||
|
<main class="site-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="article-meta"><h1 class="article-title">Exemple d'article — toutes les fonctionnalités</h1><div class="article-info"><span class="article-date">01/01/2026</span> <span class="article-sep">—</span> <span class="article-author">Votre Nom</span> <span class="article-sep">—</span> <span class="article-categories">Blog</span></div></div>
|
||||||
|
<p>Ceci est un <strong>article d'exemple</strong> montrant toutes les fonctionnalités disponibles dans PageMark. Supprimez ce fichier quand vous n'en avez plus besoin.</p>
|
||||||
|
<h2 id="metadonnees">Métadonnées<a class="heading-anchor" href="#metadonnees" title="Permanent link">#</a></h2>
|
||||||
|
<p>Les métadonnées se placent en tête de fichier avec la syntaxe <code>::Clé:Valeur</code>. Elles définissent le titre, la date, l'auteur et la catégorie de l'article.</p>
|
||||||
|
<h2 id="mise-en-forme-du-texte">Mise en forme du texte<a class="heading-anchor" href="#mise-en-forme-du-texte" title="Permanent link">#</a></h2>
|
||||||
|
<p>Le texte supporte toute la syntaxe Markdown :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Gras</strong> avec <code>**texte**</code></li>
|
||||||
|
<li><em>Italique</em> avec <code>*texte*</code></li>
|
||||||
|
<li>~~Barré~~ avec <code>~~texte~~</code></li>
|
||||||
|
<li><code>Code inline</code> avec des backticks</li>
|
||||||
|
<li><a href="https://example.com">Lien</a> avec <code>[texte](url)</code></li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="titres-et-ancres">Titres et ancres<a class="heading-anchor" href="#titres-et-ancres" title="Permanent link">#</a></h2>
|
||||||
|
<p>Les titres <code>h1</code> à <code>h4</code> génèrent automatiquement une ancre cliquable <code>#</code> au survol, permettant de créer des liens directs vers une section :</p>
|
||||||
|
<pre><code>https://monsite.com/exemple-article#images
|
||||||
|
</code></pre>
|
||||||
|
<h3 id="niveau-3">Niveau 3<a class="heading-anchor" href="#niveau-3" title="Permanent link">#</a></h3>
|
||||||
|
<h4 id="niveau-4">Niveau 4<a class="heading-anchor" href="#niveau-4" title="Permanent link">#</a></h4>
|
||||||
|
<h2 id="images">Images<a class="heading-anchor" href="#images" title="Permanent link">#</a></h2>
|
||||||
|
<p>Placez vos images dans <code>md/images/</code> et insérez-les ainsi :</p>
|
||||||
|
<pre><code class="language-markdown">
|
||||||
|
</code></pre>
|
||||||
|
<p>L'image de couverture (affichée dans les cards de la page d'accueil) se définit avec <code>::Image:/images/cover.jpg</code>.</p>
|
||||||
|
<h2 id="blocs-de-code">Blocs de code<a class="heading-anchor" href="#blocs-de-code" title="Permanent link">#</a></h2>
|
||||||
|
<p>Les blocs de code supportent la coloration syntaxique pour la plupart des langages :</p>
|
||||||
|
<pre><code class="language-python">def saluer(nom: str) -> str:
|
||||||
|
return f"Bonjour, {nom} !"
|
||||||
|
|
||||||
|
print(saluer("PageMark"))
|
||||||
|
</code></pre>
|
||||||
|
<pre><code class="language-bash"># Générer le site
|
||||||
|
python3 build.py
|
||||||
|
|
||||||
|
# Test local
|
||||||
|
python3 -m http.server 3000
|
||||||
|
</code></pre>
|
||||||
|
<pre><code class="language-javascript">document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
console.log("PageMark chargé !");
|
||||||
|
});
|
||||||
|
</code></pre>
|
||||||
|
<h2 id="tableaux">Tableaux<a class="heading-anchor" href="#tableaux" title="Permanent link">#</a></h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fonctionnalité</th>
|
||||||
|
<th>Syntaxe</th>
|
||||||
|
<th>Résultat</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Titre</td>
|
||||||
|
<td><code>::Titre:Mon titre</code></td>
|
||||||
|
<td>h1 avant l'article</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Date</td>
|
||||||
|
<td><code>::Date:dd/mm/YYYY</code></td>
|
||||||
|
<td>Tri chronologique</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Catégorie</td>
|
||||||
|
<td><code>::Categories:A-B</code></td>
|
||||||
|
<td>Dossier <code>a/b/</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Image couverture</td>
|
||||||
|
<td><code>::Image:/img/x.jpg</code></td>
|
||||||
|
<td>Card sur l'accueil</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 id="citations">Citations<a class="heading-anchor" href="#citations" title="Permanent link">#</a></h2>
|
||||||
|
<blockquote>
|
||||||
|
<p>Les blockquotes s'écrivent avec <code>></code> en début de ligne.
|
||||||
|
Elles peuvent s'étendre sur plusieurs lignes.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="html-personnalise">HTML personnalisé<a class="heading-anchor" href="#html-personnalise" title="Permanent link">#</a></h2>
|
||||||
|
<p>Vous pouvez insérer du HTML directement dans le Markdown — utile pour des mises en forme avancées, des iframes, des widgets, etc.</p>
|
||||||
|
<div style="background: var(--accent-soft); border-left: 3px solid var(--accent); padding: 1rem 1.4rem; border-radius: 0 8px 8px 0; margin: 1.5rem 0;">
|
||||||
|
<strong>Astuce :</strong> utilisez les variables CSS <code>var(--accent)</code>, <code>var(--bg)</code>, <code>var(--text)</code>, etc. pour que votre HTML s'adapte automatiquement au dark mode.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="listes">Listes<a class="heading-anchor" href="#listes" title="Permanent link">#</a></h2>
|
||||||
|
<p>Liste non ordonnée :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Élément A</li>
|
||||||
|
<li>Élément B</li>
|
||||||
|
<li>Sous-élément</li>
|
||||||
|
<li>Sous-élément</li>
|
||||||
|
</ul>
|
||||||
|
<p>Liste ordonnée :</p>
|
||||||
|
<ol>
|
||||||
|
<li>Première étape</li>
|
||||||
|
<li>Deuxième étape</li>
|
||||||
|
<li>Troisième étape</li>
|
||||||
|
</ol>
|
||||||
|
<p>Liste de tâches :</p>
|
||||||
|
<ul>
|
||||||
|
<li>[x] Tâche terminée</li>
|
||||||
|
<li>[ ] Tâche en cours</li>
|
||||||
|
<li>[ ] Tâche à faire</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="site-footer"><div class="container"><p>© 2026 Sebastien QUEROL — Propulsé par <a href="https://github.com/">PageMark</a></p></div></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
482
build.py
Normal file
482
build.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Générateur de site statique — PageMark
|
||||||
|
|
||||||
|
Prérequis : pip install markdown
|
||||||
|
|
||||||
|
Utilisation : python3 build.py
|
||||||
|
|
||||||
|
── Métadonnées ───────────────────────────────────────────────────────────────
|
||||||
|
::Titre:Mon article
|
||||||
|
::Date:01/12/2026
|
||||||
|
::Auteur:Pseudo
|
||||||
|
::Categories:Principale-SousCategorie
|
||||||
|
::Image:/images/cover.jpg ← optionnel, image de couverture pour les cards
|
||||||
|
|
||||||
|
── Fichiers de layout ────────────────────────────────────────────────────────
|
||||||
|
md/header.md Ligne 1 : # Nom du site | Reste : tagline
|
||||||
|
md/footer.md Pied de page
|
||||||
|
md/menu.md Navigation
|
||||||
|
md/categories.md Mapping NomCategorie:dossier
|
||||||
|
|
||||||
|
── Sortie ────────────────────────────────────────────────────────────────────
|
||||||
|
index.html ← homepage auto (5 derniers articles)
|
||||||
|
md/pages/index.md ← contenu affiché AU-DESSUS des articles
|
||||||
|
md/pages/article.md ← article/index.html
|
||||||
|
md/pages/article.md (catégorie) ← cat/sous-cat/article/index.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import markdown
|
||||||
|
except ImportError:
|
||||||
|
print("Erreur : installez la dépendance avec : pip install markdown")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).parent
|
||||||
|
MD_DIR = BASE_DIR / 'md'
|
||||||
|
PAGES_DIR = MD_DIR / 'pages'
|
||||||
|
|
||||||
|
MD_EXTENSIONS = ['fenced_code', 'tables', 'attr_list', 'toc']
|
||||||
|
|
||||||
|
MD_EXTENSION_CONFIGS = {
|
||||||
|
'toc': {
|
||||||
|
'permalink': '#', # symbole cliquable affiché après le titre
|
||||||
|
'permalink_class': 'heading-anchor',
|
||||||
|
'toc_depth': '1-4', # h1 à h4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Markdown ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_md(text):
|
||||||
|
md = markdown.Markdown(
|
||||||
|
extensions=MD_EXTENSIONS,
|
||||||
|
extension_configs=MD_EXTENSION_CONFIGS,
|
||||||
|
output_format='html5'
|
||||||
|
)
|
||||||
|
return md.convert(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Métadonnées ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_META_RE = re.compile(r'^::([^:]+):(.*)$')
|
||||||
|
|
||||||
|
def parse_metadata(text):
|
||||||
|
meta = {}
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
|
i = 0
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.rstrip('\n')
|
||||||
|
m = _META_RE.match(stripped)
|
||||||
|
if m:
|
||||||
|
meta[m.group(1).strip()] = m.group(2).strip()
|
||||||
|
i += 1
|
||||||
|
elif stripped.strip() == '':
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return meta, ''.join(lines[i:])
|
||||||
|
|
||||||
|
|
||||||
|
# ── Catégories ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_categories():
|
||||||
|
cat_file = MD_DIR / 'categories.md'
|
||||||
|
cats = {}
|
||||||
|
if not cat_file.exists():
|
||||||
|
return cats
|
||||||
|
for line in cat_file.read_text('utf-8').splitlines():
|
||||||
|
line = line.strip().lstrip('-*').strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
if ':' in line:
|
||||||
|
name, folder = line.split(':', 1)
|
||||||
|
cats[name.strip().lower()] = folder.strip().lstrip('/')
|
||||||
|
return cats
|
||||||
|
|
||||||
|
def categories_to_path(categories_str, cat_map):
|
||||||
|
parts = [p.strip() for p in categories_str.split('-') if p.strip()]
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
result = []
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
result.append(cat_map.get(part.lower(), part.lower()) if i == 0 else part.lower())
|
||||||
|
return '/'.join(result)
|
||||||
|
|
||||||
|
def categories_to_display(categories_str):
|
||||||
|
parts = [p.strip() for p in categories_str.split('-') if p.strip()]
|
||||||
|
return ' > '.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Header du site ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def extract_site_name(header_md):
|
||||||
|
"""Extrait le premier # Heading comme nom/logo du site."""
|
||||||
|
if not header_md:
|
||||||
|
return ''
|
||||||
|
m = re.search(r'^#\s+(.+)$', header_md, re.MULTILINE)
|
||||||
|
return m.group(1).strip() if m else ''
|
||||||
|
|
||||||
|
def extract_tagline_html(header_md):
|
||||||
|
"""Extrait tout ce qui suit le premier # Heading comme tagline."""
|
||||||
|
if not header_md:
|
||||||
|
return ''
|
||||||
|
content = re.sub(r'^#[^\n]*\n?', '', header_md, count=1, flags=re.MULTILINE).strip()
|
||||||
|
return parse_md(content) if content else ''
|
||||||
|
|
||||||
|
|
||||||
|
# ── Articles ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_date(date_str):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(date_str.strip(), '%d/%m/%Y')
|
||||||
|
except Exception:
|
||||||
|
return datetime.min
|
||||||
|
|
||||||
|
def extract_excerpt(content_md, max_chars=180):
|
||||||
|
"""Extrait le premier paragraphe lisible du markdown."""
|
||||||
|
text = re.sub(r'```[\s\S]*?```', '', content_md)
|
||||||
|
text = re.sub(r'^#{1,6}\s+.*$', '', text, flags=re.MULTILINE)
|
||||||
|
text = re.sub(r'<[^>]+>', '', text)
|
||||||
|
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
|
||||||
|
text = re.sub(r'[*_]{1,3}', '', text)
|
||||||
|
paragraphs = [p.strip().replace('\n', ' ') for p in text.split('\n\n') if p.strip()]
|
||||||
|
if not paragraphs:
|
||||||
|
return ''
|
||||||
|
excerpt = paragraphs[0]
|
||||||
|
if len(excerpt) > max_chars:
|
||||||
|
excerpt = excerpt[:max_chars].rsplit(' ', 1)[0] + '\u2026'
|
||||||
|
return excerpt
|
||||||
|
|
||||||
|
def build_article_card_html(article, featured=False):
|
||||||
|
img_html = ''
|
||||||
|
if article.get('image'):
|
||||||
|
img_html = (f'<div class="card-image">'
|
||||||
|
f'<img src="{article["image"]}" alt="{article["title"]}" loading="lazy">'
|
||||||
|
f'</div>')
|
||||||
|
|
||||||
|
meta_parts = []
|
||||||
|
if article.get('categories_display'):
|
||||||
|
meta_parts.append(f'<span class="card-category">{article["categories_display"]}</span>')
|
||||||
|
if article.get('date_str'):
|
||||||
|
meta_parts.append(f'<span class="card-date">{article["date_str"]}</span>')
|
||||||
|
if article.get('author'):
|
||||||
|
meta_parts.append(f'<span class="card-author">{article["author"]}</span>')
|
||||||
|
|
||||||
|
sep = ' <span class="card-sep">\u00b7</span> '
|
||||||
|
card_meta = ('<div class="card-meta">' + sep.join(meta_parts) + '</div>') if meta_parts else ''
|
||||||
|
excerpt_html = f'<p class="card-excerpt">{article["excerpt"]}</p>' if article.get('excerpt') else ''
|
||||||
|
|
||||||
|
if featured:
|
||||||
|
return (
|
||||||
|
f'<article class="featured-card">'
|
||||||
|
f'{img_html}'
|
||||||
|
f'<div class="card-body">'
|
||||||
|
f'{card_meta}'
|
||||||
|
f'<h2 class="featured-title"><a href="{article["url"]}">{article["title"]}</a></h2>'
|
||||||
|
f'{excerpt_html}'
|
||||||
|
f'<a href="{article["url"]}" class="card-readmore">Lire l\'article \u2192</a>'
|
||||||
|
f'</div>'
|
||||||
|
f'</article>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
f'<article class="article-card">'
|
||||||
|
f'{img_html}'
|
||||||
|
f'<div class="card-body">'
|
||||||
|
f'{card_meta}'
|
||||||
|
f'<h3 class="card-title"><a href="{article["url"]}">{article["title"]}</a></h3>'
|
||||||
|
f'{excerpt_html}'
|
||||||
|
f'</div>'
|
||||||
|
f'</article>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_homepage_html(articles, intro_html=''):
|
||||||
|
intro = f'<div class="home-intro">{intro_html}</div>' if intro_html else ''
|
||||||
|
|
||||||
|
if not articles:
|
||||||
|
return intro + '<p class="no-articles">Aucun article publié pour l\'instant.</p>'
|
||||||
|
|
||||||
|
featured = articles[0]
|
||||||
|
recent = articles[1:5]
|
||||||
|
|
||||||
|
featured_section = (
|
||||||
|
f'<section class="home-featured">'
|
||||||
|
f'<h2 class="section-label">À la une</h2>'
|
||||||
|
f'{build_article_card_html(featured, featured=True)}'
|
||||||
|
f'</section>'
|
||||||
|
)
|
||||||
|
|
||||||
|
recent_section = ''
|
||||||
|
if recent:
|
||||||
|
cards = '\n'.join(build_article_card_html(a) for a in recent)
|
||||||
|
recent_section = (
|
||||||
|
f'<section class="home-recent">'
|
||||||
|
f'<h2 class="section-label">Récents</h2>'
|
||||||
|
f'<div class="articles-grid">{cards}</div>'
|
||||||
|
f'</section>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return intro + featured_section + recent_section
|
||||||
|
|
||||||
|
|
||||||
|
# ── Fichiers de layout ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def read_layout(name):
|
||||||
|
for p in [MD_DIR / f'{name}.md', MD_DIR / name]:
|
||||||
|
if p.exists():
|
||||||
|
return p.read_text('utf-8')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Menu ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def is_external(url):
|
||||||
|
return bool(re.match(r'^https?://', url))
|
||||||
|
|
||||||
|
def url_to_slug(url):
|
||||||
|
slug = url.strip('/')
|
||||||
|
return slug if slug else 'index'
|
||||||
|
|
||||||
|
def build_menu_html(existing_slugs):
|
||||||
|
text = read_layout('menu')
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', text)
|
||||||
|
items = []
|
||||||
|
for label, url in links:
|
||||||
|
if is_external(url):
|
||||||
|
items.append(
|
||||||
|
f'<li><a href="{url}" class="external"'
|
||||||
|
f' target="_blank" rel="noopener noreferrer">{label} \u2197</a></li>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if url_to_slug(url) in existing_slugs:
|
||||||
|
items.append(f'<li><a href="{url}" class="internal">{label}</a></li>')
|
||||||
|
return '<ul>' + ''.join(items) + '</ul>' if items else ''
|
||||||
|
|
||||||
|
|
||||||
|
# ── Page HTML ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def extract_title(md_text):
|
||||||
|
m = re.search(r'^#\s+(.+)$', md_text, re.MULTILINE)
|
||||||
|
return m.group(1).strip() if m else 'Page'
|
||||||
|
|
||||||
|
def render_meta_block(meta):
|
||||||
|
if not meta:
|
||||||
|
return ''
|
||||||
|
parts = []
|
||||||
|
titre = meta.get('Titre', '')
|
||||||
|
if titre:
|
||||||
|
parts.append(f'<h1 class="article-title">{titre}</h1>')
|
||||||
|
info = []
|
||||||
|
if meta.get('Date'):
|
||||||
|
info.append(f'<span class="article-date">{meta["Date"]}</span>')
|
||||||
|
if meta.get('Auteur'):
|
||||||
|
info.append(f'<span class="article-author">{meta["Auteur"]}</span>')
|
||||||
|
if meta.get('Categories'):
|
||||||
|
info.append(
|
||||||
|
f'<span class="article-categories">'
|
||||||
|
f'{categories_to_display(meta["Categories"])}'
|
||||||
|
f'</span>'
|
||||||
|
)
|
||||||
|
if info:
|
||||||
|
sep = ' <span class="article-sep">\u2014</span> '
|
||||||
|
parts.append(f'<div class="article-info">{sep.join(info)}</div>')
|
||||||
|
return '<div class="article-meta">' + ''.join(parts) + '</div>' if parts else ''
|
||||||
|
|
||||||
|
def render_page(meta_html, content_html, title, site_name, tagline_html, menu_html, footer_html):
|
||||||
|
logo = f'<a href="/" class="site-logo">{site_name}</a>' if site_name else ''
|
||||||
|
nav_links = f'<div class="nav-links">{menu_html}</div>' if menu_html else '<div></div>'
|
||||||
|
|
||||||
|
nav = (
|
||||||
|
f'<nav class="site-nav">'
|
||||||
|
f'<div class="container nav-container">'
|
||||||
|
f'{logo}'
|
||||||
|
f'{nav_links}'
|
||||||
|
f'<button class="theme-toggle" id="theme-toggle"'
|
||||||
|
f' aria-label="Basculer le th\u00e8me">\u263e</button>'
|
||||||
|
f'</div></nav>'
|
||||||
|
)
|
||||||
|
|
||||||
|
tagline = (
|
||||||
|
f'<div class="site-tagline"><div class="container">'
|
||||||
|
f'{tagline_html}'
|
||||||
|
f'</div></div>'
|
||||||
|
) if tagline_html else ''
|
||||||
|
|
||||||
|
divider = '<div class="tagline-divider-wrap"><div class="container"><hr class="tagline-divider"></div></div>'
|
||||||
|
|
||||||
|
footer = (
|
||||||
|
f'<footer class="site-footer"><div class="container">{footer_html}</div></footer>'
|
||||||
|
) if footer_html else ''
|
||||||
|
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{title}</title>
|
||||||
|
<script src="/static/theme.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link id="hl-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
|
<link id="hl-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="not all">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" defer></script>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded", () => hljs.highlightAll())</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{nav}
|
||||||
|
{tagline}
|
||||||
|
{divider}
|
||||||
|
<main class="site-content">
|
||||||
|
<div class="container">
|
||||||
|
{meta_html}
|
||||||
|
{content_html}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{footer}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
# ── Chemin de sortie ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def output_path(slug, meta, cat_map):
|
||||||
|
base_name = slug.split('/')[-1]
|
||||||
|
categories_str = meta.get('Categories', '')
|
||||||
|
if categories_str:
|
||||||
|
cat_path = categories_to_path(categories_str, cat_map)
|
||||||
|
if cat_path:
|
||||||
|
if base_name == 'index':
|
||||||
|
return BASE_DIR / cat_path / 'index.html'
|
||||||
|
return BASE_DIR / cat_path / base_name / 'index.html'
|
||||||
|
if slug == 'index':
|
||||||
|
return BASE_DIR / 'index.html'
|
||||||
|
return BASE_DIR / slug / 'index.html'
|
||||||
|
|
||||||
|
|
||||||
|
# ── Collecte des pages ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def collect_pages():
|
||||||
|
pages = {}
|
||||||
|
if not PAGES_DIR.exists():
|
||||||
|
return pages
|
||||||
|
for f in sorted(PAGES_DIR.rglob('*')):
|
||||||
|
if not f.is_file():
|
||||||
|
continue
|
||||||
|
rel = f.relative_to(PAGES_DIR)
|
||||||
|
slug = str(rel.with_suffix('')) if f.suffix == '.md' else str(rel)
|
||||||
|
pages[slug.replace('\\', '/')] = f
|
||||||
|
return pages
|
||||||
|
|
||||||
|
|
||||||
|
# ── Build ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def build():
|
||||||
|
raw_pages = collect_pages()
|
||||||
|
if not raw_pages:
|
||||||
|
print(f"Aucune page trouvée dans {PAGES_DIR}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cat_map = load_categories()
|
||||||
|
|
||||||
|
# Lecture et parsing de toutes les pages
|
||||||
|
all_pages = {}
|
||||||
|
for slug, md_file in raw_pages.items():
|
||||||
|
raw = md_file.read_text('utf-8')
|
||||||
|
meta, content = parse_metadata(raw)
|
||||||
|
all_pages[slug] = {'slug': slug, 'md_file': md_file, 'meta': meta, 'content': content}
|
||||||
|
|
||||||
|
# Collecte des articles (pages avec ::Date:), triés du plus récent au plus ancien
|
||||||
|
articles = []
|
||||||
|
for slug, page in all_pages.items():
|
||||||
|
if not page['meta'].get('Date'):
|
||||||
|
continue
|
||||||
|
out = output_path(slug, page['meta'], cat_map)
|
||||||
|
rel = out.relative_to(BASE_DIR)
|
||||||
|
parent = rel.parent.as_posix()
|
||||||
|
url = '/' if parent == '.' else f'/{parent}/'
|
||||||
|
articles.append({
|
||||||
|
'slug': slug,
|
||||||
|
'url': url,
|
||||||
|
'title': page['meta'].get('Titre') or extract_title(page['content']),
|
||||||
|
'excerpt': extract_excerpt(page['content']),
|
||||||
|
'date': parse_date(page['meta'].get('Date', '')),
|
||||||
|
'date_str': page['meta'].get('Date', ''),
|
||||||
|
'author': page['meta'].get('Auteur', ''),
|
||||||
|
'categories_display': categories_to_display(page['meta'].get('Categories', ''))
|
||||||
|
if page['meta'].get('Categories') else '',
|
||||||
|
'image': page['meta'].get('Image', ''),
|
||||||
|
})
|
||||||
|
articles.sort(key=lambda a: a['date'], reverse=True)
|
||||||
|
|
||||||
|
# Layout partagé
|
||||||
|
header_md = read_layout('header')
|
||||||
|
site_name = extract_site_name(header_md or '')
|
||||||
|
tagline_html = extract_tagline_html(header_md or '')
|
||||||
|
footer_html = parse_md(read_layout('footer') or '')
|
||||||
|
|
||||||
|
# existing_slugs : slugs sources ET chemins de sortie réels
|
||||||
|
# (le menu peut pointer vers /news/technologie/test/ qui est le chemin généré,
|
||||||
|
# différent du slug source 'test')
|
||||||
|
output_url_slugs = set()
|
||||||
|
for slug, page in all_pages.items():
|
||||||
|
out = output_path(slug, page['meta'], cat_map)
|
||||||
|
rel = out.relative_to(BASE_DIR)
|
||||||
|
parent = rel.parent.as_posix()
|
||||||
|
output_url_slugs.add('index' if parent == '.' else parent)
|
||||||
|
existing_slugs = set(raw_pages.keys()) | {'index'} | output_url_slugs
|
||||||
|
|
||||||
|
menu_html = build_menu_html(existing_slugs)
|
||||||
|
|
||||||
|
print(f"Construction du site ({len(all_pages)} page(s), {len(articles)} article(s))...\n")
|
||||||
|
|
||||||
|
# Rendu de toutes les pages
|
||||||
|
for slug, page in sorted(all_pages.items()):
|
||||||
|
meta = page['meta']
|
||||||
|
content = page['content']
|
||||||
|
|
||||||
|
if slug == 'index' and not meta.get('Date'):
|
||||||
|
# Page d'accueil : contenu de index.md comme intro + listing articles
|
||||||
|
intro_html = parse_md(content) if content.strip() else ''
|
||||||
|
content_html = generate_homepage_html(articles, intro_html)
|
||||||
|
meta_html = ''
|
||||||
|
title = site_name or 'Accueil'
|
||||||
|
out = BASE_DIR / 'index.html'
|
||||||
|
else:
|
||||||
|
content_html = parse_md(content)
|
||||||
|
meta_html = render_meta_block(meta)
|
||||||
|
title = meta.get('Titre') or extract_title(content)
|
||||||
|
out = output_path(slug, meta, cat_map)
|
||||||
|
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out.write_text(
|
||||||
|
render_page(meta_html, content_html, title, site_name, tagline_html, menu_html, footer_html),
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
print(f" {str(page['md_file'].relative_to(BASE_DIR)):<42} \u2192 {out.relative_to(BASE_DIR)}")
|
||||||
|
|
||||||
|
# Si pas de index.md, génère quand même la homepage
|
||||||
|
if 'index' not in all_pages:
|
||||||
|
content_html = generate_homepage_html(articles)
|
||||||
|
out = BASE_DIR / 'index.html'
|
||||||
|
out.write_text(
|
||||||
|
render_page('', content_html, site_name or 'Accueil',
|
||||||
|
site_name, tagline_html, menu_html, footer_html),
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
print(f" [homepage auto] \u2192 index.html")
|
||||||
|
|
||||||
|
print(f"\n{len(all_pages)} page(s) g\u00e9n\u00e9r\u00e9e(s) ({len(articles)} article(s)).")
|
||||||
|
print("Test local : python3 -m http.server 3000")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("Construction du site...\n")
|
||||||
|
build()
|
||||||
26
index.html
Normal file
26
index.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sébastien QUEROL</title>
|
||||||
|
<script src="/static/theme.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link id="hl-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
|
<link id="hl-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="not all">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" defer></script>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded", () => hljs.highlightAll())</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="site-nav"><div class="container nav-container"><a href="/" class="site-logo">Sébastien QUEROL</a><div class="nav-links"><ul><li><a href="/" class="internal">Accueil</a></li><li><a href="/racine/a-propos/" class="internal">A propos</a></li></ul></div><button class="theme-toggle" id="theme-toggle" aria-label="Basculer le thème">☾</button></div></nav>
|
||||||
|
<div class="site-tagline"><div class="container"><p>Le blog d'un SysAdmin</p></div></div>
|
||||||
|
<div class="tagline-divider-wrap"><div class="container"><hr class="tagline-divider"></div></div>
|
||||||
|
<main class="site-content">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<section class="home-featured"><h2 class="section-label">À la une</h2><article class="featured-card"><div class="card-body"><div class="card-meta"><span class="card-category">Racine</span> <span class="card-sep">·</span> <span class="card-date">07/03/2026</span> <span class="card-sep">·</span> <span class="card-author">Kawa</span></div><h2 class="featured-title"><a href="/racine/a-propos/">A propos</a></h2><p class="card-excerpt">Je suis Sebastien, un admin systeme dans une petite ESN. J'aime bidouiller pleins de trucs, vous trouverez sur ce site pleins de projets en cours, finis ou oublies (theme…</p><a href="/racine/a-propos/" class="card-readmore">Lire l'article →</a></div></article></section><section class="home-recent"><h2 class="section-label">Récents</h2><div class="articles-grid"><article class="article-card"><div class="card-image"><img src="/images/cover.jpg" alt="Exemple d'article — toutes les fonctionnalités" loading="lazy"></div><div class="card-body"><div class="card-meta"><span class="card-category">Blog</span> <span class="card-sep">·</span> <span class="card-date">01/01/2026</span> <span class="card-sep">·</span> <span class="card-author">Votre Nom</span></div><h3 class="card-title"><a href="/blog/exemple-article/">Exemple d'article — toutes les fonctionnalités</a></h3><p class="card-excerpt">Ceci est un article d'exemple montrant toutes les fonctionnalités disponibles dans PageMark. Supprimez ce fichier quand vous n'en avez plus besoin.</p></div></article></div></section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="site-footer"><div class="container"><p>© 2026 Sebastien QUEROL — Propulsé par <a href="https://github.com/">PageMark</a></p></div></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
md/categories.md
Normal file
8
md/categories.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
- News:/news
|
||||||
|
- DIY:/diy
|
||||||
|
- Technologie:/tech
|
||||||
|
- Dossier:/dossier
|
||||||
|
- Jeux:/jeux
|
||||||
|
- Films:/films
|
||||||
|
- Series:/series
|
||||||
|
- Videos:/videos
|
||||||
1
md/footer.md
Normal file
1
md/footer.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
© 2026 Sebastien QUEROL — Propulsé par [PageMark](https://github.com/)
|
||||||
3
md/header.md
Normal file
3
md/header.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Sébastien QUEROL
|
||||||
|
|
||||||
|
Le blog d'un SysAdmin
|
||||||
3
md/menu.md
Normal file
3
md/menu.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- [Accueil](/)
|
||||||
|
- [A propos](/racine/a-propos/)
|
||||||
|
- [Test](/news/technologie/test)
|
||||||
6
md/pages/a-propos.md
Normal file
6
md/pages/a-propos.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
::Date:07/03/2026
|
||||||
|
::Auteur:Kawa
|
||||||
|
::Categories:Racine
|
||||||
|
::Titre:A propos
|
||||||
|
|
||||||
|
Salut, moi c'est Kawa, j'aime les piles.
|
||||||
111
md/pages/exemple-article.md
Normal file
111
md/pages/exemple-article.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
::Titre:Exemple d'article — toutes les fonctionnalités
|
||||||
|
::Date:01/01/2026
|
||||||
|
::Auteur:Votre Nom
|
||||||
|
::Categories:Blog
|
||||||
|
::Image:/images/cover.jpg
|
||||||
|
|
||||||
|
Ceci est un **article d'exemple** montrant toutes les fonctionnalités disponibles dans PageMark. Supprimez ce fichier quand vous n'en avez plus besoin.
|
||||||
|
|
||||||
|
## Métadonnées
|
||||||
|
|
||||||
|
Les métadonnées se placent en tête de fichier avec la syntaxe `::Clé:Valeur`. Elles définissent le titre, la date, l'auteur et la catégorie de l'article.
|
||||||
|
|
||||||
|
## Mise en forme du texte
|
||||||
|
|
||||||
|
Le texte supporte toute la syntaxe Markdown :
|
||||||
|
|
||||||
|
- **Gras** avec `**texte**`
|
||||||
|
- *Italique* avec `*texte*`
|
||||||
|
- ~~Barré~~ avec `~~texte~~`
|
||||||
|
- `Code inline` avec des backticks
|
||||||
|
- [Lien](https://example.com) avec `[texte](url)`
|
||||||
|
|
||||||
|
## Titres et ancres
|
||||||
|
|
||||||
|
Les titres `h1` à `h4` génèrent automatiquement une ancre cliquable `#` au survol, permettant de créer des liens directs vers une section :
|
||||||
|
|
||||||
|
```
|
||||||
|
https://monsite.com/exemple-article#images
|
||||||
|
```
|
||||||
|
|
||||||
|
### Niveau 3
|
||||||
|
|
||||||
|
#### Niveau 4
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
Placez vos images dans `md/images/` et insérez-les ainsi :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
L'image de couverture (affichée dans les cards de la page d'accueil) se définit avec `::Image:/images/cover.jpg`.
|
||||||
|
|
||||||
|
## Blocs de code
|
||||||
|
|
||||||
|
Les blocs de code supportent la coloration syntaxique pour la plupart des langages :
|
||||||
|
|
||||||
|
```python
|
||||||
|
def saluer(nom: str) -> str:
|
||||||
|
return f"Bonjour, {nom} !"
|
||||||
|
|
||||||
|
print(saluer("PageMark"))
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Générer le site
|
||||||
|
python3 build.py
|
||||||
|
|
||||||
|
# Test local
|
||||||
|
python3 -m http.server 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
console.log("PageMark chargé !");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tableaux
|
||||||
|
|
||||||
|
| Fonctionnalité | Syntaxe | Résultat |
|
||||||
|
|-------------------|-----------------------|-----------------------|
|
||||||
|
| Titre | `::Titre:Mon titre` | h1 avant l'article |
|
||||||
|
| Date | `::Date:dd/mm/YYYY` | Tri chronologique |
|
||||||
|
| Catégorie | `::Categories:A-B` | Dossier `a/b/` |
|
||||||
|
| Image couverture | `::Image:/img/x.jpg` | Card sur l'accueil |
|
||||||
|
|
||||||
|
## Citations
|
||||||
|
|
||||||
|
> Les blockquotes s'écrivent avec `>` en début de ligne.
|
||||||
|
> Elles peuvent s'étendre sur plusieurs lignes.
|
||||||
|
|
||||||
|
## HTML personnalisé
|
||||||
|
|
||||||
|
Vous pouvez insérer du HTML directement dans le Markdown — utile pour des mises en forme avancées, des iframes, des widgets, etc.
|
||||||
|
|
||||||
|
<div style="background: var(--accent-soft); border-left: 3px solid var(--accent); padding: 1rem 1.4rem; border-radius: 0 8px 8px 0; margin: 1.5rem 0;">
|
||||||
|
<strong>Astuce :</strong> utilisez les variables CSS <code>var(--accent)</code>, <code>var(--bg)</code>, <code>var(--text)</code>, etc. pour que votre HTML s'adapte automatiquement au dark mode.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Listes
|
||||||
|
|
||||||
|
Liste non ordonnée :
|
||||||
|
|
||||||
|
- Élément A
|
||||||
|
- Élément B
|
||||||
|
- Sous-élément
|
||||||
|
- Sous-élément
|
||||||
|
|
||||||
|
Liste ordonnée :
|
||||||
|
|
||||||
|
1. Première étape
|
||||||
|
2. Deuxième étape
|
||||||
|
3. Troisième étape
|
||||||
|
|
||||||
|
Liste de tâches :
|
||||||
|
|
||||||
|
- [x] Tâche terminée
|
||||||
|
- [ ] Tâche en cours
|
||||||
|
- [ ] Tâche à faire
|
||||||
0
md/pages/index.md
Normal file
0
md/pages/index.md
Normal file
27
racine/a-propos/index.html
Normal file
27
racine/a-propos/index.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>A propos</title>
|
||||||
|
<script src="/static/theme.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link id="hl-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
|
<link id="hl-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="not all">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" defer></script>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded", () => hljs.highlightAll())</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="site-nav"><div class="container nav-container"><a href="/" class="site-logo">Sébastien QUEROL</a><div class="nav-links"><ul><li><a href="/" class="internal">Accueil</a></li><li><a href="/racine/a-propos/" class="internal">A propos</a></li></ul></div><button class="theme-toggle" id="theme-toggle" aria-label="Basculer le thème">☾</button></div></nav>
|
||||||
|
<div class="site-tagline"><div class="container"><p>Le blog d'un SysAdmin</p></div></div>
|
||||||
|
<div class="tagline-divider-wrap"><div class="container"><hr class="tagline-divider"></div></div>
|
||||||
|
<main class="site-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="article-meta"><h1 class="article-title">A propos</h1><div class="article-info"><span class="article-date">07/03/2026</span> <span class="article-sep">—</span> <span class="article-author">Kawa</span> <span class="article-sep">—</span> <span class="article-categories">Racine</span></div></div>
|
||||||
|
<p>Je suis Sebastien, un admin systeme dans une petite ESN.
|
||||||
|
J'aime bidouiller pleins de trucs, vous trouverez sur ce site pleins de projets en cours, finis ou oublies (theme recurrent, vous allez voir).</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="site-footer"><div class="container"><p>© 2026 Sebastien QUEROL — Propulsé par <a href="https://github.com/">PageMark</a></p></div></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
476
static/style.css
Normal file
476
static/style.css
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
/* ── Reset ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Variables ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-2: #f9f8f6;
|
||||||
|
--text: #111111;
|
||||||
|
--text-muted: #737373;
|
||||||
|
--border: #e8e5df;
|
||||||
|
--accent: #8000ff;
|
||||||
|
--accent-soft: #f2e8ff;
|
||||||
|
|
||||||
|
--font-serif: Georgia, 'Times New Roman', serif;
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
--font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
|
||||||
|
--max-w: 860px;
|
||||||
|
--nav-h: 56px;
|
||||||
|
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0e0e0e;
|
||||||
|
--bg-2: #1a1a1a;
|
||||||
|
--text: #e6e6e6;
|
||||||
|
--text-muted: #888888;
|
||||||
|
--border: #2e2e2e;
|
||||||
|
--accent: #a855f7;
|
||||||
|
--accent-soft: #1e0840;
|
||||||
|
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Base ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.8;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: var(--max-w);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Navigation ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
.site-nav {
|
||||||
|
background: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: var(--nav-h);
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (backdrop-filter: blur(8px)) {
|
||||||
|
.site-nav { backdrop-filter: blur(8px); }
|
||||||
|
:root { --nav-bg-a: rgba(255,255,255,0.88); }
|
||||||
|
[data-theme="dark"] { --nav-bg-a: rgba(14,14,14,0.88); }
|
||||||
|
.site-nav { background: var(--nav-bg-a); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo / nom du site */
|
||||||
|
.site-logo {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.site-logo:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Liens de navigation */
|
||||||
|
.nav-links ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.nav-links li { margin: 0; }
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
display: block;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
line-height: var(--nav-h);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.nav-links a:hover { color: var(--text); }
|
||||||
|
.nav-links a.external::after { content: ' \u2197'; font-size: 0.75em; }
|
||||||
|
|
||||||
|
/* Bouton thème */
|
||||||
|
.theme-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Tagline ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.site-tagline {
|
||||||
|
padding: 1.4rem 0 0;
|
||||||
|
}
|
||||||
|
.site-tagline p {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0 0 1.3rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline-divider-wrap { }
|
||||||
|
.tagline-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Contenu ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.site-content {
|
||||||
|
padding: 2.5rem 0 5rem;
|
||||||
|
min-height: calc(100vh - 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Section labels (À la une / Récents) ─────────────── */
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 0.65rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Featured card ───────────────────────────────────── */
|
||||||
|
|
||||||
|
.home-featured { margin-bottom: 3.5rem; }
|
||||||
|
|
||||||
|
.featured-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.featured-card:hover { border-color: var(--accent); }
|
||||||
|
|
||||||
|
.featured-card .card-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-card .card-body { padding: 1.6rem 1.8rem 1.8rem; }
|
||||||
|
|
||||||
|
.featured-title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1.85rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
line-height: 1.18;
|
||||||
|
margin: 0.5rem 0 0.8rem;
|
||||||
|
}
|
||||||
|
.featured-title a { color: var(--text); text-decoration: none; }
|
||||||
|
.featured-title a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Articles grid (récents) ─────────────────────────── */
|
||||||
|
|
||||||
|
.home-recent { margin-bottom: 2rem; }
|
||||||
|
|
||||||
|
.articles-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.article-card:hover { border-color: var(--accent); }
|
||||||
|
|
||||||
|
.article-card .card-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card .card-body { padding: 1rem 1.1rem 1.2rem; }
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0.4rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
.card-title a { color: var(--text); text-decoration: none; }
|
||||||
|
.card-title a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Card meta (partagé) ─────────────────────────────── */
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
.card-sep { color: var(--border); }
|
||||||
|
.card-author { font-weight: 600; color: var(--text); }
|
||||||
|
.card-date { font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
.card-category {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.15em 0.6em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-excerpt {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0.4rem 0 1rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-readmore {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.card-readmore:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.home-intro { margin-bottom: 2.5rem; }
|
||||||
|
.no-articles { font-family: var(--font-sans); color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* ── Métadonnées article ─────────────────────────────── */
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
line-height: 1.12;
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.83rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.article-sep { color: var(--border); }
|
||||||
|
.article-author { font-weight: 600; color: var(--text); }
|
||||||
|
.article-categories {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.2em 0.75em;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ancres de titre ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.heading-anchor {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.35em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:hover .heading-anchor,
|
||||||
|
h2:hover .heading-anchor,
|
||||||
|
h3:hover .heading-anchor,
|
||||||
|
h4:hover .heading-anchor { opacity: 0.6; }
|
||||||
|
|
||||||
|
.heading-anchor:hover { opacity: 1 !important; }
|
||||||
|
|
||||||
|
/* ── Typographie ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 2em 0 0.55em;
|
||||||
|
}
|
||||||
|
h1 { font-size: 2.2rem; margin-top: 0; letter-spacing: -0.03em; }
|
||||||
|
h2 { font-size: 1.45rem; letter-spacing: -0.02em; border-bottom: 1px solid var(--border); padding-bottom: 0.4em; }
|
||||||
|
h3 { font-size: 1.2rem; }
|
||||||
|
h4 { font-size: 1rem; }
|
||||||
|
|
||||||
|
p { margin: 0 0 1.2em; }
|
||||||
|
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
ul, ol { padding-left: 1.6em; margin: 0 0 1.2em; }
|
||||||
|
li { margin: 0.35em 0; }
|
||||||
|
li input[type="checkbox"] { margin-right: 0.4em; }
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
padding: 0.8em 1.3em;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
}
|
||||||
|
blockquote p { margin: 0; }
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 2.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Code ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85em;
|
||||||
|
background: var(--bg-2);
|
||||||
|
padding: 0.15em 0.45em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.2em 1.5em;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
pre code { background: none; border: none; padding: 0; font-size: 0.875rem; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .hljs { background: var(--bg-2); }
|
||||||
|
|
||||||
|
/* ── Images ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
}
|
||||||
|
figure { margin: 1.5em 0; text-align: center; }
|
||||||
|
figcaption { font-family: var(--font-sans); font-size: 0.82rem; color: var(--text-muted); margin-top: 0.4em; }
|
||||||
|
|
||||||
|
/* ── Tableaux ────────────────────────────────────────── */
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; font-family: var(--font-sans); font-size: 0.9rem; }
|
||||||
|
th, td { border: 1px solid var(--border); padding: 0.65em 1em; text-align: left; }
|
||||||
|
th { background: var(--bg-2); font-weight: 600; }
|
||||||
|
tr:nth-child(even) td { background: var(--bg-2); }
|
||||||
|
|
||||||
|
/* ── Pied de page ────────────────────────────────────── */
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 2rem 0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.site-footer p { margin: 0; }
|
||||||
|
.site-footer a { color: var(--text-muted); text-decoration: underline; }
|
||||||
|
.site-footer a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Responsive ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
body { font-size: 16px; }
|
||||||
|
h1 { font-size: 1.7rem; }
|
||||||
|
.article-title { font-size: 1.9rem; }
|
||||||
|
.featured-title { font-size: 1.5rem; }
|
||||||
|
.container { padding: 0 1rem; }
|
||||||
|
.articles-grid { grid-template-columns: 1fr; }
|
||||||
|
.nav-links a { padding: 0 0.5rem; font-size: 0.72rem; }
|
||||||
|
}
|
||||||
47
static/theme.js
Normal file
47
static/theme.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var STORAGE_KEY = 'pagemark-theme';
|
||||||
|
|
||||||
|
function getDefaultTheme() {
|
||||||
|
var now = new Date();
|
||||||
|
var minutes = now.getHours() * 60 + now.getMinutes();
|
||||||
|
var start = 8 * 60 + 30; // 08:30
|
||||||
|
var end = 17 * 60 + 30; // 17:30
|
||||||
|
return (minutes >= start && minutes < end) ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
|
// Swap highlight.js stylesheets via l'attribut media (plus fiable que disabled)
|
||||||
|
var light = document.getElementById('hl-light');
|
||||||
|
var dark = document.getElementById('hl-dark');
|
||||||
|
if (light) light.media = (theme === 'dark') ? 'not all' : '';
|
||||||
|
if (dark) dark.media = (theme === 'dark') ? '' : 'not all';
|
||||||
|
|
||||||
|
// Icône du bouton
|
||||||
|
var btn = document.getElementById('theme-toggle');
|
||||||
|
if (btn) btn.textContent = theme === 'dark' ? '\u2600' : '\u263e'; // ☀ / ☾
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer immédiatement pour éviter le flash
|
||||||
|
var stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
var theme = stored || getDefaultTheme();
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
var current = document.documentElement.getAttribute('data-theme');
|
||||||
|
var next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
localStorage.setItem(STORAGE_KEY, next);
|
||||||
|
applyTheme(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attacher le bouton sans onclick inline (compatible CSP)
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
applyTheme(theme); // sync icône + stylesheets hl.js
|
||||||
|
|
||||||
|
var btn = document.getElementById('theme-toggle');
|
||||||
|
if (btn) btn.addEventListener('click', toggle);
|
||||||
|
});
|
||||||
|
}());
|
||||||
Reference in New Issue
Block a user