Commit initial

This commit is contained in:
2026-03-07 11:50:19 +01:00
commit 18d97e0c5a
15 changed files with 1584 additions and 0 deletions

View 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
View 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 08h3017h30) + 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
![Description de l'image](/images/photo.jpg)
```
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

View 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">![Description de l'image](/images/photo.jpg)
</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) -&gt; str:
return f&quot;Bonjour, {nom} !&quot;
print(saluer(&quot;PageMark&quot;))
</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(&quot;DOMContentLoaded&quot;, () =&gt; {
console.log(&quot;PageMark chargé !&quot;);
});
</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>&gt;</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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
© 2026 Sebastien QUEROL — Propulsé par [PageMark](https://github.com/)

3
md/header.md Normal file
View File

@@ -0,0 +1,3 @@
# Sébastien QUEROL
Le blog d'un SysAdmin

3
md/menu.md Normal file
View File

@@ -0,0 +1,3 @@
- [Accueil](/)
- [A propos](/racine/a-propos/)
- [Test](/news/technologie/test)

6
md/pages/a-propos.md Normal file
View 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
View 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
![Description de l'image](/images/photo.jpg)
```
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
View File

View 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
View 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
View 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);
});
}());