Compare commits
14 Commits
7f86f8f522
...
v1.3.1
Author | SHA1 | Date | |
---|---|---|---|
41450837f2 | |||
4edeb8709a | |||
6fc573c510 | |||
43c007c1fe | |||
dfbd532efd | |||
efe1bbca29 | |||
7e1a5e659f | |||
f5a5aefd09 | |||
f76420b2c3 | |||
3901bf8acf | |||
39b24a05cb | |||
d379fc63d1 | |||
af6b2289e0 | |||
5728ebb649 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.*
|
.*
|
||||||
|
!.sh
|
||||||
!.gitignore
|
!.gitignore
|
||||||
output/
|
output/
|
||||||
__pycache__/
|
__pycache__/
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY ./src/ ./src/
|
||||||
|
COPY ./build.py ./build.py
|
||||||
|
COPY ./gallery.py ./gallery.py
|
||||||
|
COPY ./config /app/default
|
||||||
|
COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
RUN printf '#!/bin/sh\n/app/entrypoint.sh build\n' > /usr/local/bin/build && chmod +x /usr/local/bin/build && \
|
||||||
|
printf '#!/bin/sh\n/app/entrypoint.sh gallery\n' > /usr/local/bin/gallery && chmod +x /usr/local/bin/gallery
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
44
README.MD
44
README.MD
@ -1,11 +1,14 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/logo.svg" alt="Lumeex Screenshot" width="400"/>
|
||||||
|
</div>
|
||||||
|
<p/>
|
||||||
|
<div align="center">
|
||||||
|
<p>Yet another minimalist, lightweight photo gallery static site generator.</p>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
|
||||||
</div>
|
</div>
|
||||||
<div align="center">
|
|
||||||
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
**Lumeex** — A minimalist, lightweight photo gallery static site generator.
|
|
||||||
|
|
||||||
Lumeex is a static site generator designed to create minimalist photo galleries that highlight your artworks over the author. It empowers users to organize and explore images using tags, with each page load presenting photos in a random order to encourage discovery of new content.
|
Lumeex is a static site generator designed to create minimalist photo galleries that highlight your artworks over the author. It empowers users to organize and explore images using tags, with each page load presenting photos in a random order to encourage discovery of new content.
|
||||||
|
|
||||||
@ -20,9 +23,8 @@ The project includes two thoughtfully designed themes—one modern, one minimali
|
|||||||
|
|
||||||
## 📌 Table of Contents
|
## 📌 Table of Contents
|
||||||
|
|
||||||
- [✨ Features](#-features)
|
- [✨ Features](#-features)
|
||||||
- [🐍 Python Installation](#-python-installation)
|
- [🐳 Docker or 🐍 Python Installation](#-docker-or--python-installation)
|
||||||
- [⚙️ Configuration](#-configuration)
|
|
||||||
|
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
@ -54,28 +56,6 @@ The project includes two thoughtfully designed themes—one modern, one minimali
|
|||||||
- *(Optional)* Converts images to WebP format for optimized performance
|
- *(Optional)* Converts images to WebP format for optimized performance
|
||||||
- Outputs a complete static website ready to deploy on any web server
|
- Outputs a complete static website ready to deploy on any web server
|
||||||
|
|
||||||
|
## 🐳 Docker or 🐍 Python Installation
|
||||||
## 🐍 Python Installation
|
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
|
||||||
|
|
||||||
Run the Python scripts directly with the following prerequisites:
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Git
|
|
||||||
- Python 3.11 or above
|
|
||||||
|
|
||||||
### Installation Steps
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone https://git.djeex.fr/Djeex/lumeex.git
|
|
||||||
cd lumeex
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
You are now ready to use Lumeex!
|
|
||||||
|
|
||||||
### ⚙️ Configuration
|
|
||||||
For comprehensive documentation on configuration options, customization, and demos, please visit:
|
|
||||||
https://lumeex.djeex.fr
|
https://lumeex.djeex.fr
|
187
build.py
187
build.py
@ -1,187 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from src.py.site_builder import build
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copyfile
|
|
||||||
from PIL import Image
|
|
||||||
from src.py.utils import ensure_dir, copy_assets, load_yaml, load_theme_config
|
|
||||||
from src.py.css_generator import generate_css_variables, generate_fonts_css, generate_google_fonts_link
|
|
||||||
from src.py.image_processor import process_images, copy_original_images, convert_and_resize_image, generate_favicons_from_logo, generate_favicon_ico
|
|
||||||
from src.py.html_generator import render_template, render_gallery_images, generate_gallery_json_from_images, generate_robots_txt, generate_sitemap_xml
|
|
||||||
|
|
||||||
# Configure logging to display only the messages
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
|
||||||
|
|
||||||
# Define key directories used throughout the script
|
|
||||||
SRC_DIR = Path.cwd()
|
|
||||||
BUILD_DIR = SRC_DIR / "output"
|
|
||||||
TEMPLATE_DIR = SRC_DIR / "src/templates"
|
|
||||||
IMG_DIR = SRC_DIR / "config/photos"
|
|
||||||
JS_DIR = SRC_DIR / "src/public/js"
|
|
||||||
STYLE_DIR = SRC_DIR / "src/public/style"
|
|
||||||
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
|
|
||||||
SITE_FILE = SRC_DIR / "config/site.yaml"
|
|
||||||
THEMES_DIR = SRC_DIR / "config/themes"
|
|
||||||
|
|
||||||
def build():
|
|
||||||
logging.info("🚀 Starting build...")
|
|
||||||
ensure_dir(BUILD_DIR)
|
|
||||||
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
|
|
||||||
|
|
||||||
# Defining build vars
|
|
||||||
build_date = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
||||||
build_date_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
site_vars = load_yaml(SITE_FILE)
|
|
||||||
gallery_vars = load_yaml(GALLERY_FILE)
|
|
||||||
build_section = site_vars.get("build", {})
|
|
||||||
theme_name = site_vars.get("build", {}).get("theme", "default")
|
|
||||||
theme_vars, theme_dir = load_theme_config(theme_name, THEMES_DIR)
|
|
||||||
fonts_dir = theme_dir / "fonts"
|
|
||||||
theme_css_path = theme_dir / "theme.css"
|
|
||||||
canonical_url = site_vars.get("info", {}).get("canonical", "").rstrip("/")
|
|
||||||
canonical_home = f"{canonical_url}/"
|
|
||||||
canonical_legals = f"{canonical_url}/legals/"
|
|
||||||
|
|
||||||
# Copying theme.css if existing
|
|
||||||
if theme_css_path.exists():
|
|
||||||
dest_theme_css = BUILD_DIR / "style" / "theme.css"
|
|
||||||
dest_theme_css.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
copyfile(theme_css_path, dest_theme_css)
|
|
||||||
theme_css = f'<link rel="stylesheet" href="/style/theme.css?build_date={build_date}">'
|
|
||||||
logging.info(f"[✓] Theme CSS found, copied to build folder: {dest_theme_css}")
|
|
||||||
else:
|
|
||||||
theme_css = ""
|
|
||||||
logging.warning(f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection.")
|
|
||||||
|
|
||||||
preload_links = generate_fonts_css(fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts"))
|
|
||||||
generate_css_variables(theme_vars.get("colors", {}), BUILD_DIR / "style" / "colors.css")
|
|
||||||
generate_favicons_from_logo(theme_vars, theme_dir, BUILD_DIR / "img" / "favicon")
|
|
||||||
generate_favicon_ico(theme_vars, theme_dir, BUILD_DIR / "favicon.ico")
|
|
||||||
|
|
||||||
# Converting and resizing images if enabled
|
|
||||||
convert_images = build_section.get("convert_images", True)
|
|
||||||
resize_images = build_section.get("resize_images", True)
|
|
||||||
logging.info(f"[~] convert_images = {convert_images}")
|
|
||||||
logging.info(f"[~] resize_images = {resize_images}")
|
|
||||||
|
|
||||||
hero_images = gallery_vars.get("hero", {}).get("images", [])
|
|
||||||
gallery_images = gallery_vars.get("gallery", {}).get("images", [])
|
|
||||||
|
|
||||||
if convert_images:
|
|
||||||
process_images(hero_images, resize_images, IMG_DIR, BUILD_DIR)
|
|
||||||
process_images(gallery_images, resize_images, IMG_DIR, BUILD_DIR)
|
|
||||||
else:
|
|
||||||
copy_original_images(hero_images, IMG_DIR, BUILD_DIR)
|
|
||||||
copy_original_images(gallery_images, IMG_DIR, BUILD_DIR)
|
|
||||||
|
|
||||||
if "hero" not in site_vars:
|
|
||||||
site_vars["hero"] = {} # Initialize an empty hero section
|
|
||||||
|
|
||||||
# Adding menu
|
|
||||||
menu_html = "\n".join(
|
|
||||||
f'<li class="nav-item appear"><a href="{item["href"]}">{item["label"]}</a></li>'
|
|
||||||
for item in site_vars.get("menu", {}).get("items", [])
|
|
||||||
)
|
|
||||||
site_vars["hero"]["menu_items"] = menu_html
|
|
||||||
if "footer" in site_vars:
|
|
||||||
site_vars["footer"]["menu_items"] = menu_html
|
|
||||||
|
|
||||||
# Adding Google fonts if existing
|
|
||||||
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
|
|
||||||
logging.info(f"[✓] Google Fonts link generated:\n{google_fonts_link}")
|
|
||||||
|
|
||||||
# Generating thumbnail
|
|
||||||
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
|
|
||||||
if thumbnail_path:
|
|
||||||
src_thumb = IMG_DIR / thumbnail_path
|
|
||||||
dest_thumb_dir = BUILD_DIR / "img" / "social"
|
|
||||||
dest_thumb_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dest_thumb = dest_thumb_dir / Path(thumbnail_path).name
|
|
||||||
try:
|
|
||||||
img = Image.open(src_thumb)
|
|
||||||
img = img.convert("RGB")
|
|
||||||
img = img.resize((1200, 630), Image.LANCZOS)
|
|
||||||
img.save(dest_thumb, "JPEG", quality=90)
|
|
||||||
logging.info(f"[✓] Thumbnail resized and saved to {dest_thumb}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[✗] Failed to process thumbnail: {e}")
|
|
||||||
else:
|
|
||||||
logging.warning("[~] No thumbnail found in social section")
|
|
||||||
|
|
||||||
# Defining head variables
|
|
||||||
head_vars = dict(site_vars.get("info", {}))
|
|
||||||
head_vars.update(theme_vars.get("colors", {}))
|
|
||||||
head_vars.update(site_vars.get("social", {}))
|
|
||||||
head_vars["thumbnail"] = f"/img/social/{Path(thumbnail_path).name}" if thumbnail_path else ""
|
|
||||||
head_vars["google_fonts_link"] = google_fonts_link
|
|
||||||
head_vars["font_preloads"] = "\n".join(preload_links)
|
|
||||||
head_vars["theme_css"] = theme_css
|
|
||||||
head_vars["build_date"] = build_date
|
|
||||||
head_vars["canonical"] = canonical_home
|
|
||||||
|
|
||||||
# Render the home page
|
|
||||||
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
|
|
||||||
hero = render_template(TEMPLATE_DIR / "hero.html", {**site_vars["hero"], **head_vars})
|
|
||||||
footer = render_template(TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars})
|
|
||||||
gallery_html = render_gallery_images(gallery_images)
|
|
||||||
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
|
|
||||||
|
|
||||||
signature = f"<!-- Build with Lumeex v1.2 | https://git.djeex.fr/Djeex/lumeex | {build_date_version} -->"
|
|
||||||
body = f"""
|
|
||||||
<body>
|
|
||||||
<div class="page-loader"><div class="spinner"></div></div>
|
|
||||||
{hero}
|
|
||||||
{gallery}
|
|
||||||
{footer}
|
|
||||||
</body>
|
|
||||||
"""
|
|
||||||
output_file = BUILD_DIR / "index.html"
|
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
|
||||||
f.write(f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{body}\n</html>")
|
|
||||||
logging.info(f"[✓] HTML generated: {output_file}")
|
|
||||||
|
|
||||||
# Rendering legals page
|
|
||||||
head_vars["canonical"] = canonical_legals
|
|
||||||
|
|
||||||
legals_vars = site_vars.get("legals", {})
|
|
||||||
if legals_vars:
|
|
||||||
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
|
|
||||||
|
|
||||||
ip_paragraphs = legals_vars.get("intellectual_property", [])
|
|
||||||
paragraphs_html = "\n".join(f"<p>{item['paragraph']}</p>" for item in ip_paragraphs)
|
|
||||||
legals_context = {
|
|
||||||
"hoster_name": legals_vars.get("hoster_name", ""),
|
|
||||||
"hoster_adress": legals_vars.get("hoster_adress", ""),
|
|
||||||
"hoster_contact": legals_vars.get("hoster_contact", ""),
|
|
||||||
"intellectual_property": paragraphs_html,
|
|
||||||
}
|
|
||||||
legals_body = render_template(TEMPLATE_DIR / "legals.html", legals_context)
|
|
||||||
legals_html = f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{legals_body}\n{footer}\n</html>"
|
|
||||||
output_legals = BUILD_DIR / "legals" / "index.html"
|
|
||||||
output_legals.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(output_legals, "w", encoding="utf-8") as f:
|
|
||||||
f.write(legals_html)
|
|
||||||
logging.info(f"[✓] Legals page generated: {output_legals}")
|
|
||||||
else:
|
|
||||||
logging.warning("[~] No legals section found in site.yaml")
|
|
||||||
|
|
||||||
# Hero carrousel generator
|
|
||||||
if hero_images:
|
|
||||||
generate_gallery_json_from_images(hero_images, BUILD_DIR)
|
|
||||||
else:
|
|
||||||
logging.warning("[~] No hero images found, skipping JSON generation.")
|
|
||||||
|
|
||||||
# Sitemap and robot.txt generator
|
|
||||||
site_info = site_vars.get("info", {})
|
|
||||||
canonical_url = site_info.get("canonical", "").rstrip("/")
|
|
||||||
if canonical_url:
|
|
||||||
allowed_pages = ["/", "/legals/"]
|
|
||||||
generate_robots_txt(canonical_url, allowed_pages, BUILD_DIR)
|
|
||||||
generate_sitemap_xml(canonical_url, allowed_pages, BUILD_DIR)
|
|
||||||
else:
|
|
||||||
logging.warning("[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation.")
|
|
||||||
|
|
||||||
logging.info("✅ Build complete.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
build()
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
build()
|
@ -1,7 +1,4 @@
|
|||||||
# Use gallery.py to automatically add photos stored in your /config/photos/gallery folder
|
|
||||||
# Add tags to your photos as shown below
|
|
||||||
# remove the # before [] if you removed all images to use gallery.py again
|
|
||||||
hero:
|
hero:
|
||||||
images: []
|
images: []
|
||||||
gallery:
|
gallery:
|
||||||
images: []
|
images: []
|
||||||
|
78
docker/.sh/entrypoint.sh
Normal file
78
docker/.sh/entrypoint.sh
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CYAN="\033[1;36m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
copy_default_config() {
|
||||||
|
echo "Checking configuration directory..."
|
||||||
|
if [ ! -d "/app/config" ]; then
|
||||||
|
mkdir -p /app/config
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking if default config files need to be copied..."
|
||||||
|
files_copied=false
|
||||||
|
|
||||||
|
for file in /app/default/*; do
|
||||||
|
filename=$(basename "$file")
|
||||||
|
target="/app/config/$filename"
|
||||||
|
|
||||||
|
if [ ! -e "$target" ]; then
|
||||||
|
echo "Copying default config file: $filename"
|
||||||
|
cp -r "$file" "$target"
|
||||||
|
files_copied=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$files_copied" = true ]; then
|
||||||
|
echo "Default configuration files copied successfully."
|
||||||
|
else
|
||||||
|
echo "No default files needed to be copied."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start_server() {
|
||||||
|
# Clean up old FIFOs
|
||||||
|
[ -p /tmp/build_logs_fifo ] && rm /tmp/build_logs_fifo
|
||||||
|
[ -p /tmp/build_logs_fifo2 ] && rm /tmp/build_logs_fifo2
|
||||||
|
|
||||||
|
mkfifo /tmp/build_logs_fifo
|
||||||
|
mkfifo /tmp/build_logs_fifo2
|
||||||
|
|
||||||
|
cat /tmp/build_logs_fifo >&2 &
|
||||||
|
cat /tmp/build_logs_fifo2 >&2 &
|
||||||
|
|
||||||
|
echo "Starting HTTP server on port 3000..."
|
||||||
|
python3 -u -m http.server 3000 -d /app/output &
|
||||||
|
SERVER_PID=$!
|
||||||
|
trap "echo 'Stopping server...'; kill -TERM $SERVER_PID 2>/dev/null; wait $SERVER_PID; exit 0" SIGINT SIGTERM
|
||||||
|
wait $SERVER_PID
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
|
||||||
|
echo -e "${CYAN}│${NC} Lum${CYAN}eex${NC} - Version 1.3.1${NC} ${CYAN}│${NC}"
|
||||||
|
echo -e "${CYAN}├───────────────────────────────────────────┤${NC}"
|
||||||
|
echo -e "${CYAN}│${NC} Source: https://git.djeex.fr/Djeex/lumeex ${CYAN}│${NC}"
|
||||||
|
echo -e "${CYAN}│${NC} Mirror: https://github.com/Djeex/lumeex ${CYAN}│${NC}"
|
||||||
|
echo -e "${CYAN}│${NC} Documentation: https://lumeex.djeex.fr ${CYAN}│${NC}"
|
||||||
|
echo -e "${CYAN}╰───────────────────────────────────────────╯${NC}"
|
||||||
|
copy_default_config
|
||||||
|
start_server
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
build)
|
||||||
|
echo "Running build.py..."
|
||||||
|
python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
|
||||||
|
;;
|
||||||
|
gallery)
|
||||||
|
echo "Running gallery.py..."
|
||||||
|
python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
exec "$@"
|
||||||
|
;;
|
||||||
|
esac
|
10
docker/docker-compose.yaml
Normal file
10
docker/docker-compose.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
lumeex:
|
||||||
|
container_name: lmx
|
||||||
|
build: ..
|
||||||
|
volumes:
|
||||||
|
- ../config:/app/config # mount config directory
|
||||||
|
- ../output:/app/output # mount output directory
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
114
gallery.py
114
gallery.py
@ -1,113 +1,7 @@
|
|||||||
import yaml
|
import logging
|
||||||
import os
|
from src.py.gallery_builder import update_gallery, update_hero
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# YAML file paths
|
|
||||||
GALLERY_YAML = "config/gallery.yaml"
|
|
||||||
|
|
||||||
# Image directories
|
|
||||||
GALLERY_DIR = Path("config/photos/gallery")
|
|
||||||
HERO_DIR = Path("config/photos/hero")
|
|
||||||
|
|
||||||
def load_yaml(path):
|
|
||||||
print(f"[→] Loading {path}...")
|
|
||||||
if not os.path.exists(path):
|
|
||||||
print(f"[✗] File not found: {path}")
|
|
||||||
return {}
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
data = yaml.safe_load(f) or {}
|
|
||||||
images = data.get("images", []) or []
|
|
||||||
print(f"[✓] Loaded {len(images)} image(s) from {path}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
def save_yaml(data, path):
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
|
|
||||||
print(f"[✓] Saved updated YAML to {path}")
|
|
||||||
|
|
||||||
def get_all_image_paths(directory):
|
|
||||||
return sorted([
|
|
||||||
str(p.relative_to(directory.parent)).replace("\\", "/")
|
|
||||||
for p in directory.rglob("*")
|
|
||||||
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
|
|
||||||
])
|
|
||||||
|
|
||||||
def update_gallery():
|
|
||||||
print("\n=== Updating gallery.yaml (gallery section) ===")
|
|
||||||
gallery = load_yaml(GALLERY_YAML)
|
|
||||||
|
|
||||||
# Access the 'gallery' section within the gallery data, or initialize it if it doesn't exist
|
|
||||||
gallery_section = gallery.get("gallery", {})
|
|
||||||
|
|
||||||
# Access the 'images' list within the 'gallery' section, or initialize it if it doesn't exist
|
|
||||||
gallery_images = gallery_section.get("images", [])
|
|
||||||
|
|
||||||
all_images = set(get_all_image_paths(GALLERY_DIR))
|
|
||||||
known_images = {img["src"] for img in gallery_images}
|
|
||||||
|
|
||||||
# Add new images
|
|
||||||
new_images = [
|
|
||||||
{"src": path, "tags": []}
|
|
||||||
for path in all_images
|
|
||||||
if path not in known_images
|
|
||||||
]
|
|
||||||
if new_images:
|
|
||||||
gallery_images.extend(new_images)
|
|
||||||
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (gallery)")
|
|
||||||
|
|
||||||
# Remove deleted images
|
|
||||||
deleted_images = known_images - all_images
|
|
||||||
if deleted_images:
|
|
||||||
gallery_images = [img for img in gallery_images if img["src"] not in deleted_images]
|
|
||||||
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (gallery)")
|
|
||||||
|
|
||||||
# Update the 'gallery' section with the modified 'images' list
|
|
||||||
gallery_section["images"] = gallery_images
|
|
||||||
gallery["gallery"] = gallery_section
|
|
||||||
|
|
||||||
save_yaml(gallery, GALLERY_YAML)
|
|
||||||
|
|
||||||
if not new_images and not deleted_images:
|
|
||||||
print("[✓] No changes to gallery.yaml (gallery)")
|
|
||||||
|
|
||||||
def update_hero():
|
|
||||||
print("\n=== Updating gallery.yaml (hero section) ===")
|
|
||||||
gallery = load_yaml(GALLERY_YAML)
|
|
||||||
|
|
||||||
# Access the 'hero' section within the gallery data, or initialize it if it doesn't exist
|
|
||||||
hero_section = gallery.get("hero", {})
|
|
||||||
|
|
||||||
# Access the 'images' list within the 'hero' section, or initialize it if it doesn't exist
|
|
||||||
hero_images = hero_section.get("images", [])
|
|
||||||
|
|
||||||
all_images = set(get_all_image_paths(HERO_DIR))
|
|
||||||
known_images = {img["src"] for img in hero_images}
|
|
||||||
|
|
||||||
# Add new images
|
|
||||||
new_images = [
|
|
||||||
{"src": path}
|
|
||||||
for path in all_images
|
|
||||||
if path not in known_images
|
|
||||||
]
|
|
||||||
if new_images:
|
|
||||||
hero_images.extend(new_images)
|
|
||||||
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (hero)")
|
|
||||||
|
|
||||||
# Remove deleted images
|
|
||||||
deleted_images = known_images - all_images
|
|
||||||
if deleted_images:
|
|
||||||
hero_images = [img for img in hero_images if img["src"] not in deleted_images]
|
|
||||||
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (hero)")
|
|
||||||
|
|
||||||
# Update the 'hero' section with the modified 'images' list
|
|
||||||
hero_section["images"] = hero_images
|
|
||||||
gallery["hero"] = hero_section
|
|
||||||
|
|
||||||
save_yaml(gallery, GALLERY_YAML)
|
|
||||||
|
|
||||||
if not new_images and not deleted_images:
|
|
||||||
print("[✓] No changes to gallery.yaml (hero)")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
update_gallery()
|
update_gallery()
|
||||||
update_hero()
|
update_hero()
|
166
illustration/logo.svg
Normal file
166
illustration/logo.svg
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 6031 1000">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #55c3ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: url(#Dégradé_sans_nom_265);
|
||||||
|
stroke: url(#Dégradé_sans_nom_33);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11, .st12 {
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st2 {
|
||||||
|
fill: url(#Dégradé_sans_nom_269);
|
||||||
|
stroke: url(#Dégradé_sans_nom_334);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st3 {
|
||||||
|
fill: url(#Dégradé_sans_nom_268);
|
||||||
|
stroke: url(#Dégradé_sans_nom_333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st4 {
|
||||||
|
fill: url(#Dégradé_sans_nom_266);
|
||||||
|
stroke: url(#Dégradé_sans_nom_331);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st5 {
|
||||||
|
fill: url(#Dégradé_sans_nom_267);
|
||||||
|
stroke: url(#Dégradé_sans_nom_332);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st13 {
|
||||||
|
fill: url(#Dégradé_sans_nom_261);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st14 {
|
||||||
|
fill: url(#Dégradé_sans_nom_262);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st15 {
|
||||||
|
fill: url(#Dégradé_sans_nom_264);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st16 {
|
||||||
|
fill: url(#Dégradé_sans_nom_263);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st6 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2616);
|
||||||
|
stroke: url(#Dégradé_sans_nom_3311);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st7 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2615);
|
||||||
|
stroke: url(#Dégradé_sans_nom_3310);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st17 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st18 {
|
||||||
|
fill: url(#Dégradé_sans_nom_26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st8 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2610);
|
||||||
|
stroke: url(#Dégradé_sans_nom_335);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st9 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2613);
|
||||||
|
stroke: url(#Dégradé_sans_nom_338);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st10 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2614);
|
||||||
|
stroke: url(#Dégradé_sans_nom_339);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st11 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2611);
|
||||||
|
stroke: url(#Dégradé_sans_nom_336);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st12 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2612);
|
||||||
|
stroke: url(#Dégradé_sans_nom_337);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#55c3ec"/>
|
||||||
|
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#55c3ec"/>
|
||||||
|
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
</defs>
|
||||||
|
<g id="Calque_1">
|
||||||
|
<circle class="st17" cx="499.5" cy="499.5" r="499.5"/>
|
||||||
|
<g>
|
||||||
|
<path class="st17" d="M1404,957.4V45h191v755h399v157.4h-590Z"/>
|
||||||
|
<path class="st17" d="M2321.5,971.3c-49.3,0-91.5-10.2-126.5-30.7-35-20.4-61.6-49.6-79.7-87.6-18.1-37.9-27.2-83.2-27.2-135.9v-437.6h184.6v399c0,44.3,10.4,78.6,31.3,103.1,20.9,24.5,51.9,36.7,93.3,36.7s39.3-3.6,56-10.7c16.6-7.2,30.9-17.4,42.7-30.7,11.8-13.3,20.9-29.1,27.2-47.4s9.5-38.5,9.5-60.4v-389.5h184.6v677.8h-184.6v-111.9h-4.4c-11.4,25.7-26.7,48.1-45.8,67-19.2,19-42.2,33.5-68.9,43.6-26.8,10.1-57.4,15.2-92,15.2Z"/>
|
||||||
|
<path class="st17" d="M2837.5,957.4V279.6h184.6v113.8h3.8c13.9-38.8,37.6-69.8,71.1-93,33.5-23.2,73-34.8,118.6-34.8s60.1,5.5,85.4,16.4c25.3,11,46.7,26.8,64.2,47.4,17.5,20.7,29.8,46,37,75.9h3.8c10.1-28.7,25.4-53.4,45.8-74.3,20.4-20.9,44.7-37,72.7-48.4,28-11.4,58.7-17.1,92-17.1s83,9.5,116.3,28.5c33.3,19,59.2,45.5,77.8,79.7,18.5,34.1,27.8,74.2,27.8,120.1v463.5h-184.6v-416.7c0-26.6-4.3-48.8-13-66.7-8.6-17.9-21.1-31.6-37.3-41.1-16.2-9.5-36.4-14.2-60.4-14.2s-43.5,5.4-61,16.1c-17.5,10.7-31.1,25.6-40.8,44.6-9.7,19-14.5,41.1-14.5,66.4v411.6h-177.7v-422.4c0-24.4-4.4-45.3-13.3-62.6-8.9-17.3-21.4-30.6-37.6-39.8-16.2-9.3-35.7-13.9-58.5-13.9s-43.6,5.6-61.3,16.8c-17.7,11.2-31.5,26.5-41.4,45.8-9.9,19.4-14.9,41.7-14.9,67v409.1h-184.6Z"/>
|
||||||
|
<path class="st0" d="M4264,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6-36,12.6-78.3,19-126.8,19Z"/>
|
||||||
|
<path class="st0" d="M4982.9,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6s-78.3,19-126.8,19Z"/>
|
||||||
|
<path class="st0" d="M5337,957.4l212.5-337.7-210.6-340.2h208l122,228.9h3.8l120.1-228.9h201.1l-211.8,335.1,209.9,342.7h-200.4l-129-234.6h-3.8l-127.1,234.6h-194.8Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="Calque_2">
|
||||||
|
<g id="Calque_3">
|
||||||
|
<ellipse class="st18" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
|
||||||
|
<ellipse class="st13" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
|
||||||
|
<ellipse class="st14" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
|
||||||
|
<ellipse class="st16" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
|
||||||
|
<ellipse class="st15" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
|
||||||
|
<circle class="st1" cx="90.4" cy="576" r="22.4"/>
|
||||||
|
<circle class="st4" cx="175.6" cy="607.9" r="13.1"/>
|
||||||
|
<circle class="st5" cx="140.8" cy="691.6" r="28"/>
|
||||||
|
<circle class="st3" cx="829.7" cy="602.6" r="28"/>
|
||||||
|
<circle class="st2" cx="908.9" cy="562.1" r="13.1"/>
|
||||||
|
<circle class="st8" cx="840.9" cy="698.1" r="22.4"/>
|
||||||
|
<circle class="st11" cx="466.1" cy="876.5" r="22.5"/>
|
||||||
|
<circle class="st12" cx="538.6" cy="839.8" r="13.1"/>
|
||||||
|
<circle class="st9" cx="686.1" cy="170.1" r="28"/>
|
||||||
|
<circle class="st10" cx="733.7" cy="247.7" r="13.1"/>
|
||||||
|
<circle class="st7" cx="236.9" cy="206.5" r="21.1"/>
|
||||||
|
<circle class="st6" cx="315.4" cy="164.9" r="13.1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 13 KiB |
@ -17,24 +17,11 @@ const setupLoader = () => {
|
|||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const loader = document.querySelector('.page-loader');
|
const loader = document.querySelector('.page-loader');
|
||||||
if (loader) {
|
if (loader) loader.classList.add('hidden');
|
||||||
loader.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gallery randomizer to shuffle gallery sections on page load
|
|
||||||
const shuffleGallery = () => {
|
|
||||||
const gallery = document.querySelector('.gallery');
|
|
||||||
if (!gallery) return;
|
|
||||||
const sections = Array.from(gallery.querySelectorAll('.section'));
|
|
||||||
while (sections.length) {
|
|
||||||
const randomIndex = Math.floor(Math.random() * sections.length);
|
|
||||||
gallery.appendChild(sections.splice(randomIndex, 1)[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hero background randomizer
|
// Hero background randomizer
|
||||||
const randomizeHeroBackground = () => {
|
const randomizeHeroBackground = () => {
|
||||||
const heroBg = document.querySelector(".hero-background");
|
const heroBg = document.querySelector(".hero-background");
|
||||||
@ -65,32 +52,74 @@ const randomizeHeroBackground = () => {
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Gallery randomizer to shuffle gallery sections on page load
|
||||||
|
const shuffleGallery = () => {
|
||||||
|
const gallery = document.querySelector('.gallery');
|
||||||
|
if (!gallery) return;
|
||||||
|
const sections = Array.from(gallery.querySelectorAll('.section'));
|
||||||
|
while (sections.length) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * sections.length);
|
||||||
|
gallery.appendChild(sections.splice(randomIndex, 1)[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Tags filter functionality
|
// Tags filter functionality
|
||||||
const setupTagFilter = () => {
|
const setupTagFilter = () => {
|
||||||
|
const galleryContainer = document.querySelector('#gallery');
|
||||||
const allSections = document.querySelectorAll('.section[data-tags]');
|
const allSections = document.querySelectorAll('.section[data-tags]');
|
||||||
const allTags = document.querySelectorAll('.tag');
|
const allTags = document.querySelectorAll('.tag');
|
||||||
let activeTags = [];
|
let activeTags = [];
|
||||||
|
let lastClickedTag = null; // mémorise le dernier tag cliqué
|
||||||
|
|
||||||
const applyFilter = () => {
|
const applyFilter = () => {
|
||||||
|
let filteredSections = [];
|
||||||
|
let matchingSection = null;
|
||||||
|
|
||||||
allSections.forEach((section) => {
|
allSections.forEach((section) => {
|
||||||
const sectionTags = section.dataset.tags.toLowerCase().split(/\s+/);
|
const sectionTags = section.dataset.tags.toLowerCase().split(/\s+/);
|
||||||
const hasAllTags = activeTags.every((tag) => sectionTags.includes(tag));
|
const hasAllTags = activeTags.every((tag) => sectionTags.includes(tag));
|
||||||
section.style.display = hasAllTags ? '' : 'none';
|
section.style.display = hasAllTags ? '' : 'none';
|
||||||
|
|
||||||
|
if (hasAllTags) {
|
||||||
|
filteredSections.push(section);
|
||||||
|
if (lastClickedTag && sectionTags.includes(lastClickedTag) && !matchingSection) {
|
||||||
|
matchingSection = section;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Réorganise : la photo correspondante au dernier tag cliqué en premier
|
||||||
|
if (matchingSection && galleryContainer.contains(matchingSection)) {
|
||||||
|
galleryContainer.prepend(matchingSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Met à jour le style des tags
|
||||||
allTags.forEach((tagEl) => {
|
allTags.forEach((tagEl) => {
|
||||||
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
|
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
|
||||||
tagEl.classList.toggle('active', activeTags.includes(tagText));
|
tagEl.classList.toggle('active', activeTags.includes(tagText));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Met à jour l'URL
|
||||||
const base = window.location.pathname;
|
const base = window.location.pathname;
|
||||||
const query = activeTags.length > 0 ? `?tag=${activeTags.join(',')}` : '';
|
const query = activeTags.length > 0 ? `?tag=${activeTags.join(',')}` : '';
|
||||||
window.history.pushState({}, '', base + query);
|
window.history.pushState({}, '', base + query);
|
||||||
|
|
||||||
|
// Scroll jusqu'à la galerie
|
||||||
|
if (galleryContainer) {
|
||||||
|
galleryContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
allTags.forEach((tagEl) => {
|
allTags.forEach((tagEl) => {
|
||||||
tagEl.addEventListener('click', () => {
|
tagEl.addEventListener('click', () => {
|
||||||
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
|
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
|
||||||
activeTags = activeTags.includes(tagText)
|
lastClickedTag = tagText; // mémorise le dernier tag cliqué
|
||||||
? activeTags.filter((t) => t !== tagText)
|
|
||||||
: [...activeTags, tagText];
|
if (activeTags.includes(tagText)) {
|
||||||
|
activeTags = activeTags.filter((t) => t !== tagText);
|
||||||
|
} else {
|
||||||
|
activeTags.push(tagText);
|
||||||
|
}
|
||||||
applyFilter();
|
applyFilter();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -100,29 +129,24 @@ const setupTagFilter = () => {
|
|||||||
const urlTags = params.get('tag');
|
const urlTags = params.get('tag');
|
||||||
if (urlTags) {
|
if (urlTags) {
|
||||||
activeTags = urlTags.split(',').map((t) => t.toLowerCase());
|
activeTags = urlTags.split(',').map((t) => t.toLowerCase());
|
||||||
|
lastClickedTag = activeTags[activeTags.length - 1] || null;
|
||||||
applyFilter();
|
applyFilter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable right-click context menu and image dragging
|
// Disable right click and drag
|
||||||
const disableRightClickAndDrag = () => {
|
const disableRightClickAndDrag = () => {
|
||||||
document.addEventListener("contextmenu", (e) => e.preventDefault());
|
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||||
document.addEventListener("dragstart", (e) => {
|
document.addEventListener('dragstart', (e) => e.preventDefault());
|
||||||
if (e.target.tagName === "IMG") {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll-to-top button functionality
|
// Scroll to top button
|
||||||
const setupScrollToTopButton = () => {
|
const setupScrollToTopButton = () => {
|
||||||
const scrollBtn = document.getElementById("scrollToTop");
|
const scrollToTopButton = document.querySelector('.scroll-to-top');
|
||||||
window.addEventListener("scroll", () => {
|
if (!scrollToTopButton) return;
|
||||||
scrollBtn.style.display = window.scrollY > 300 ? "block" : "none";
|
scrollToTopButton.addEventListener('click', () => {
|
||||||
});
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
scrollBtn.addEventListener("click", () => {
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -333,8 +333,12 @@ h2 {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections */
|
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
.section {
|
.section {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
margin:auto;
|
margin:auto;
|
||||||
@ -508,4 +512,9 @@ h2 {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
margin: 10% 5% 0 5%;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
109
src/py/gallery_builder.py
Normal file
109
src/py/gallery_builder.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# YAML file paths
|
||||||
|
GALLERY_YAML = "config/gallery.yaml"
|
||||||
|
|
||||||
|
# Image directories
|
||||||
|
GALLERY_DIR = Path("config/photos/gallery")
|
||||||
|
HERO_DIR = Path("config/photos/hero")
|
||||||
|
|
||||||
|
def load_yaml(path):
|
||||||
|
print(f"[→] Loading {path}...")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"[✗] File not found: {path}")
|
||||||
|
return {}
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
images = data.get("images", []) or []
|
||||||
|
print(f"[✓] Loaded {len(images)} image(s) from {path}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save_yaml(data, path):
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
|
||||||
|
print(f"[✓] Saved updated YAML to {path}")
|
||||||
|
|
||||||
|
def get_all_image_paths(directory):
|
||||||
|
return sorted([
|
||||||
|
str(p.relative_to(directory.parent)).replace("\\", "/")
|
||||||
|
for p in directory.rglob("*")
|
||||||
|
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
|
||||||
|
])
|
||||||
|
|
||||||
|
def update_gallery():
|
||||||
|
print("\n=== Updating gallery.yaml (gallery section) ===")
|
||||||
|
gallery = load_yaml(GALLERY_YAML)
|
||||||
|
|
||||||
|
# Access the 'gallery' section within the gallery data, or initialize it if it doesn't exist
|
||||||
|
gallery_section = gallery.get("gallery", {})
|
||||||
|
|
||||||
|
# Access the 'images' list within the 'gallery' section, or initialize it if it doesn't exist
|
||||||
|
gallery_images = gallery_section.get("images", [])
|
||||||
|
|
||||||
|
all_images = set(get_all_image_paths(GALLERY_DIR))
|
||||||
|
known_images = {img["src"] for img in gallery_images}
|
||||||
|
|
||||||
|
# Add new images
|
||||||
|
new_images = [
|
||||||
|
{"src": path, "tags": []}
|
||||||
|
for path in all_images
|
||||||
|
if path not in known_images
|
||||||
|
]
|
||||||
|
if new_images:
|
||||||
|
gallery_images.extend(new_images)
|
||||||
|
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (gallery)")
|
||||||
|
|
||||||
|
# Remove deleted images
|
||||||
|
deleted_images = known_images - all_images
|
||||||
|
if deleted_images:
|
||||||
|
gallery_images = [img for img in gallery_images if img["src"] not in deleted_images]
|
||||||
|
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (gallery)")
|
||||||
|
|
||||||
|
# Update the 'gallery' section with the modified 'images' list
|
||||||
|
gallery_section["images"] = gallery_images
|
||||||
|
gallery["gallery"] = gallery_section
|
||||||
|
|
||||||
|
save_yaml(gallery, GALLERY_YAML)
|
||||||
|
|
||||||
|
if not new_images and not deleted_images:
|
||||||
|
print("[✓] No changes to gallery.yaml (gallery)")
|
||||||
|
|
||||||
|
def update_hero():
|
||||||
|
print("\n=== Updating gallery.yaml (hero section) ===")
|
||||||
|
gallery = load_yaml(GALLERY_YAML)
|
||||||
|
|
||||||
|
# Access the 'hero' section within the gallery data, or initialize it if it doesn't exist
|
||||||
|
hero_section = gallery.get("hero", {})
|
||||||
|
|
||||||
|
# Access the 'images' list within the 'hero' section, or initialize it if it doesn't exist
|
||||||
|
hero_images = hero_section.get("images", [])
|
||||||
|
|
||||||
|
all_images = set(get_all_image_paths(HERO_DIR))
|
||||||
|
known_images = {img["src"] for img in hero_images}
|
||||||
|
|
||||||
|
# Add new images
|
||||||
|
new_images = [
|
||||||
|
{"src": path}
|
||||||
|
for path in all_images
|
||||||
|
if path not in known_images
|
||||||
|
]
|
||||||
|
if new_images:
|
||||||
|
hero_images.extend(new_images)
|
||||||
|
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (hero)")
|
||||||
|
|
||||||
|
# Remove deleted images
|
||||||
|
deleted_images = known_images - all_images
|
||||||
|
if deleted_images:
|
||||||
|
hero_images = [img for img in hero_images if img["src"] not in deleted_images]
|
||||||
|
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (hero)")
|
||||||
|
|
||||||
|
# Update the 'hero' section with the modified 'images' list
|
||||||
|
hero_section["images"] = hero_images
|
||||||
|
gallery["hero"] = hero_section
|
||||||
|
|
||||||
|
save_yaml(gallery, GALLERY_YAML)
|
||||||
|
|
||||||
|
if not new_images and not deleted_images:
|
||||||
|
print("[✓] No changes to gallery.yaml (hero)")
|
@ -36,16 +36,30 @@ def generate_gallery_json_from_images(images, output_dir):
|
|||||||
|
|
||||||
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
|
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
|
||||||
robots_lines = ["User-agent: *"]
|
robots_lines = ["User-agent: *"]
|
||||||
for path in allowed_paths:
|
|
||||||
robots_lines.append(f"Allow: {path}")
|
# Block everything by default
|
||||||
robots_lines.append("Disallow: /")
|
robots_lines.append("Disallow: /")
|
||||||
|
|
||||||
|
# Explicitly allow certain paths
|
||||||
|
for path in allowed_paths:
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = "/" + path
|
||||||
|
robots_lines.append(f"Allow: {path}")
|
||||||
|
|
||||||
robots_lines.append("")
|
robots_lines.append("")
|
||||||
robots_lines.append(f"Sitemap: {canonical_url}/sitemap.xml")
|
robots_lines.append(f"Sitemap: {canonical_url.rstrip('/')}/sitemap.xml")
|
||||||
|
|
||||||
content = "\n".join(robots_lines)
|
content = "\n".join(robots_lines)
|
||||||
output_path = output_dir / "robots.txt"
|
output_path = Path(output_dir) / "robots.txt"
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
try:
|
||||||
logging.info(f"[✓] robots.txt generated at {output_path}")
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
logging.info(f"[✓] robots.txt generated at {output_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[✗] Failed to write robots.txt: {e}")
|
||||||
|
|
||||||
def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
|
def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
|
||||||
urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||||
|
@ -1,73 +1,123 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image
|
from PIL import Image, features
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
|
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
|
||||||
|
"""Convert an image to WebP (or JPEG fallback) and optionally resize it."""
|
||||||
try:
|
try:
|
||||||
|
if not input_path.exists():
|
||||||
|
logging.error(f"[✗] Image file not found: {input_path}")
|
||||||
|
return
|
||||||
|
|
||||||
img = Image.open(input_path)
|
img = Image.open(input_path)
|
||||||
if img.mode != "RGB":
|
if img.mode != "RGB":
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
if resize:
|
if resize:
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
if width > max_width:
|
if width > max_width:
|
||||||
new_height = int((max_width / width) * height)
|
new_height = int((max_width / width) * height)
|
||||||
img = img.resize((max_width, new_height), Image.LANCZOS)
|
img = img.resize((max_width, new_height), Image.LANCZOS)
|
||||||
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
img.save(output_path, "WEBP", quality=100)
|
|
||||||
logging.info(f"[✓] Processed: {input_path} → {output_path}")
|
# Check WebP support, otherwise fallback to JPEG
|
||||||
|
fmt = "WEBP" if features.check("webp") else "JPEG"
|
||||||
|
if fmt == "JPEG":
|
||||||
|
output_path = output_path.with_suffix(".jpg")
|
||||||
|
|
||||||
|
img.save(output_path, fmt, quality=90 if fmt == "JPEG" else 100)
|
||||||
|
logging.info(f"[✓] Processed image: {input_path} → {output_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[✗] Failed to process {input_path}: {e}")
|
logging.error(f"[✗] Error processing image {input_path}: {e}")
|
||||||
|
|
||||||
def process_images(images, resize_images, img_dir, build_dir):
|
def process_images(images, resize_images, img_dir, build_dir):
|
||||||
|
"""Process a list of image references and update paths to optimized versions."""
|
||||||
for img in images:
|
for img in images:
|
||||||
src_path = img_dir / img["src"]
|
src_path = img_dir / img["src"]
|
||||||
webp_path = build_dir / "img" / Path(img["src"]).with_suffix(".webp")
|
webp_path = build_dir / "img" / Path(img["src"]).with_suffix(".webp")
|
||||||
convert_and_resize_image(src_path, webp_path, resize=resize_images)
|
convert_and_resize_image(src_path, webp_path, resize=resize_images)
|
||||||
img["src"] = str(Path(img["src"]).with_suffix(".webp"))
|
|
||||||
|
if webp_path.exists():
|
||||||
|
img["src"] = str(Path(img["src"]).with_suffix(".webp"))
|
||||||
|
else:
|
||||||
|
# Fallback if WebP not created
|
||||||
|
jpg_path = webp_path.with_suffix(".jpg")
|
||||||
|
if jpg_path.exists():
|
||||||
|
img["src"] = str(Path(img["src"]).with_suffix(".jpg"))
|
||||||
|
|
||||||
def copy_original_images(images, img_dir, build_dir):
|
def copy_original_images(images, img_dir, build_dir):
|
||||||
|
"""Copy original image files without processing."""
|
||||||
for img in images:
|
for img in images:
|
||||||
src_path = img_dir / img["src"]
|
src_path = img_dir / img["src"]
|
||||||
dest_path = build_dir / "img" / img["src"]
|
dest_path = build_dir / "img" / img["src"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not src_path.exists():
|
||||||
|
logging.error(f"[✗] Original image not found: {src_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
copyfile(src_path, dest_path)
|
copyfile(src_path, dest_path)
|
||||||
logging.info(f"[✓] Copied original: {src_path} → {dest_path}")
|
logging.info(f"[✓] Copied original: {src_path} → {dest_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[✗] Failed to copy {src_path}: {e}")
|
logging.error(f"[✗] Error copying {src_path}: {e}")
|
||||||
|
|
||||||
|
def get_favicon_path(theme_vars, theme_dir):
|
||||||
|
"""Retrieve the favicon path from theme variables, ensuring it exists."""
|
||||||
|
fav_path = theme_vars.get("favicon", {}).get("path")
|
||||||
|
if not fav_path:
|
||||||
|
logging.warning("[~] No favicon path defined in theme.yaml")
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = Path(fav_path)
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = theme_dir / path
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
logging.error(f"[✗] Favicon not found: {path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
|
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
|
||||||
|
"""Generate multiple PNG favicons from a single source image."""
|
||||||
logo_path = get_favicon_path(theme_vars, theme_dir)
|
logo_path = get_favicon_path(theme_vars, theme_dir)
|
||||||
if not logo_path:
|
if not logo_path:
|
||||||
logging.warning("[~] No favicon path defined, skipping favicon PNGs.")
|
logging.warning("[~] PNG favicons not generated.")
|
||||||
return
|
return
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
specs = [(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
|
try:
|
||||||
(192, "favicon-192.png"), (196, "favicon-196.png"), (152, "favicon-152.png"), (180, "favicon-180.png")]
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
img = Image.open(logo_path).convert("RGBA")
|
specs = [
|
||||||
for size, name in specs:
|
(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
|
||||||
img.resize((size, size), Image.LANCZOS).save(output_dir / name, format="PNG")
|
(192, "favicon-192.png"), (196, "favicon-196.png"),
|
||||||
logging.info(f"[✓] Favicons generated in {output_dir}")
|
(152, "favicon-152.png"), (180, "favicon-180.png")
|
||||||
|
]
|
||||||
|
img = Image.open(logo_path).convert("RGBA")
|
||||||
|
for size, name in specs:
|
||||||
|
img.resize((size, size), Image.LANCZOS).save(output_dir / name, format="PNG")
|
||||||
|
|
||||||
|
logging.info(f"[✓] PNG favicons generated in {output_dir}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[✗] Error generating PNG favicons: {e}")
|
||||||
|
|
||||||
def generate_favicon_ico(theme_vars, theme_dir, output_path):
|
def generate_favicon_ico(theme_vars, theme_dir, output_path):
|
||||||
|
"""Generate a multi-size favicon.ico from a source image."""
|
||||||
logo_path = get_favicon_path(theme_vars, theme_dir)
|
logo_path = get_favicon_path(theme_vars, theme_dir)
|
||||||
if not logo_path:
|
if not logo_path:
|
||||||
logging.warning("[~] No favicon path defined, skipping .ico generation.")
|
logging.warning("[~] favicon.ico not generated.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img = Image.open(logo_path).convert("RGBA")
|
img = Image.open(logo_path).convert("RGBA")
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
img.save(output_path, format="ICO", sizes=[(16, 16), (32, 32), (48, 48)])
|
img.save(output_path, format="ICO", sizes=[(16, 16), (32, 32), (48, 48)])
|
||||||
logging.info(f"[✓] favicon.ico generated at {output_path}")
|
logging.info(f"[✓] favicon.ico generated in {output_path}")
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[✗] Failed to generate favicon.ico: {e}")
|
|
||||||
|
|
||||||
def get_favicon_path(theme_vars, theme_dir):
|
except Exception as e:
|
||||||
fav_path = theme_vars.get("favicon", {}).get("path")
|
logging.error(f"[✗] Error generating favicon.ico: {e}")
|
||||||
if not fav_path:
|
|
||||||
return None
|
|
||||||
path = Path(fav_path)
|
|
||||||
if not path.is_absolute():
|
|
||||||
path = theme_dir / path
|
|
||||||
return path if path.exists() else None
|
|
||||||
|
189
src/py/site_builder.py
Normal file
189
src/py/site_builder.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import copyfile
|
||||||
|
from PIL import Image
|
||||||
|
from .utils import ensure_dir, copy_assets, load_yaml, load_theme_config
|
||||||
|
from .css_generator import generate_css_variables, generate_fonts_css, generate_google_fonts_link
|
||||||
|
from .image_processor import process_images, copy_original_images, convert_and_resize_image, generate_favicons_from_logo, generate_favicon_ico
|
||||||
|
from .html_generator import render_template, render_gallery_images, generate_gallery_json_from_images, generate_robots_txt, generate_sitemap_xml
|
||||||
|
|
||||||
|
# Configure logging to display only the messages
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
|
||||||
|
# Define key directories used throughout the script
|
||||||
|
SRC_DIR = Path.cwd()
|
||||||
|
BUILD_DIR = SRC_DIR / "output"
|
||||||
|
TEMPLATE_DIR = SRC_DIR / "src/templates"
|
||||||
|
IMG_DIR = SRC_DIR / "config/photos"
|
||||||
|
JS_DIR = SRC_DIR / "src/public/js"
|
||||||
|
STYLE_DIR = SRC_DIR / "src/public/style"
|
||||||
|
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
|
||||||
|
SITE_FILE = SRC_DIR / "config/site.yaml"
|
||||||
|
THEMES_DIR = SRC_DIR / "config/themes"
|
||||||
|
|
||||||
|
def build():
|
||||||
|
build_version = "v1.3.1"
|
||||||
|
logging.info("\n")
|
||||||
|
logging.info("=" * 24)
|
||||||
|
logging.info(f"🚀 Lumeex builder {build_version}")
|
||||||
|
logging.info("=" * 24)
|
||||||
|
logging.info("\n === Starting build === ")
|
||||||
|
ensure_dir(BUILD_DIR)
|
||||||
|
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
|
||||||
|
|
||||||
|
# Defining build vars
|
||||||
|
build_date = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
build_date_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
site_vars = load_yaml(SITE_FILE)
|
||||||
|
gallery_vars = load_yaml(GALLERY_FILE)
|
||||||
|
build_section = site_vars.get("build", {})
|
||||||
|
theme_name = site_vars.get("build", {}).get("theme", "default")
|
||||||
|
theme_vars, theme_dir = load_theme_config(theme_name, THEMES_DIR)
|
||||||
|
fonts_dir = theme_dir / "fonts"
|
||||||
|
theme_css_path = theme_dir / "theme.css"
|
||||||
|
canonical_url = site_vars.get("info", {}).get("canonical", "").rstrip("/")
|
||||||
|
canonical_home = f"{canonical_url}/"
|
||||||
|
canonical_legals = f"{canonical_url}/legals/"
|
||||||
|
|
||||||
|
# Copying theme.css if existing
|
||||||
|
if theme_css_path.exists():
|
||||||
|
dest_theme_css = BUILD_DIR / "style" / "theme.css"
|
||||||
|
dest_theme_css.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
copyfile(theme_css_path, dest_theme_css)
|
||||||
|
theme_css = f'<link rel="stylesheet" href="/style/theme.css?build_date={build_date}">'
|
||||||
|
logging.info(f"[✓] Theme CSS found, copied to build folder: {dest_theme_css}")
|
||||||
|
else:
|
||||||
|
theme_css = ""
|
||||||
|
logging.warning(f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection.")
|
||||||
|
|
||||||
|
preload_links = generate_fonts_css(fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts"))
|
||||||
|
generate_css_variables(theme_vars.get("colors", {}), BUILD_DIR / "style" / "colors.css")
|
||||||
|
generate_favicons_from_logo(theme_vars, theme_dir, BUILD_DIR / "img" / "favicon")
|
||||||
|
generate_favicon_ico(theme_vars, theme_dir, BUILD_DIR / "favicon.ico")
|
||||||
|
|
||||||
|
# Converting and resizing images if enabled
|
||||||
|
convert_images = build_section.get("convert_images", True)
|
||||||
|
resize_images = build_section.get("resize_images", True)
|
||||||
|
logging.info(f"[~] convert_images = {convert_images}")
|
||||||
|
logging.info(f"[~] resize_images = {resize_images}")
|
||||||
|
|
||||||
|
hero_images = gallery_vars.get("hero", {}).get("images", [])
|
||||||
|
gallery_images = gallery_vars.get("gallery", {}).get("images", [])
|
||||||
|
|
||||||
|
if convert_images:
|
||||||
|
process_images(hero_images, resize_images, IMG_DIR, BUILD_DIR)
|
||||||
|
process_images(gallery_images, resize_images, IMG_DIR, BUILD_DIR)
|
||||||
|
else:
|
||||||
|
copy_original_images(hero_images, IMG_DIR, BUILD_DIR)
|
||||||
|
copy_original_images(gallery_images, IMG_DIR, BUILD_DIR)
|
||||||
|
|
||||||
|
if "hero" not in site_vars:
|
||||||
|
site_vars["hero"] = {} # Initialize an empty hero section
|
||||||
|
|
||||||
|
# Adding menu
|
||||||
|
menu_html = "\n".join(
|
||||||
|
f'<li class="nav-item appear"><a href="{item["href"]}">{item["label"]}</a></li>'
|
||||||
|
for item in site_vars.get("menu", {}).get("items", [])
|
||||||
|
)
|
||||||
|
site_vars["hero"]["menu_items"] = menu_html
|
||||||
|
if "footer" in site_vars:
|
||||||
|
site_vars["footer"]["menu_items"] = menu_html
|
||||||
|
|
||||||
|
# Adding Google fonts if existing
|
||||||
|
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
|
||||||
|
logging.info(f"[✓] Google Fonts link generated")
|
||||||
|
|
||||||
|
# Generating thumbnail
|
||||||
|
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
|
||||||
|
if thumbnail_path:
|
||||||
|
src_thumb = IMG_DIR / thumbnail_path
|
||||||
|
dest_thumb_dir = BUILD_DIR / "img" / "social"
|
||||||
|
dest_thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest_thumb = dest_thumb_dir / Path(thumbnail_path).name
|
||||||
|
try:
|
||||||
|
img = Image.open(src_thumb)
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img = img.resize((1200, 630), Image.LANCZOS)
|
||||||
|
img.save(dest_thumb, "JPEG", quality=90)
|
||||||
|
logging.info(f"[✓] Thumbnail resized and saved to {dest_thumb}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[✗] Failed to process thumbnail: {e}")
|
||||||
|
else:
|
||||||
|
logging.warning("[~] No thumbnail found in social section")
|
||||||
|
|
||||||
|
# Defining head variables
|
||||||
|
head_vars = dict(site_vars.get("info", {}))
|
||||||
|
head_vars.update(theme_vars.get("colors", {}))
|
||||||
|
head_vars.update(site_vars.get("social", {}))
|
||||||
|
head_vars["thumbnail"] = f"/img/social/{Path(thumbnail_path).name}" if thumbnail_path else ""
|
||||||
|
head_vars["google_fonts_link"] = google_fonts_link
|
||||||
|
head_vars["font_preloads"] = "\n".join(preload_links)
|
||||||
|
head_vars["theme_css"] = theme_css
|
||||||
|
head_vars["build_date"] = build_date
|
||||||
|
head_vars["canonical"] = canonical_home
|
||||||
|
|
||||||
|
# Render the home page
|
||||||
|
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
|
||||||
|
hero = render_template(TEMPLATE_DIR / "hero.html", {**site_vars["hero"], **head_vars})
|
||||||
|
footer = render_template(TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars})
|
||||||
|
gallery_html = render_gallery_images(gallery_images)
|
||||||
|
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
|
||||||
|
|
||||||
|
signature = f"<!-- Build with Lumeex {build_version} | https://git.djeex.fr/Djeex/lumeex | {build_date_version} -->"
|
||||||
|
body = f"""
|
||||||
|
<body>
|
||||||
|
<div class="page-loader"><div class="spinner"></div></div>
|
||||||
|
{hero}
|
||||||
|
{gallery}
|
||||||
|
{footer}
|
||||||
|
</body>
|
||||||
|
"""
|
||||||
|
output_file = BUILD_DIR / "index.html"
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{body}\n</html>")
|
||||||
|
logging.info(f"[✓] HTML generated: {output_file}")
|
||||||
|
|
||||||
|
# Rendering legals page
|
||||||
|
head_vars["canonical"] = canonical_legals
|
||||||
|
|
||||||
|
legals_vars = site_vars.get("legals", {})
|
||||||
|
if legals_vars:
|
||||||
|
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
|
||||||
|
|
||||||
|
ip_paragraphs = legals_vars.get("intellectual_property", [])
|
||||||
|
paragraphs_html = "\n".join(f"<p>{item['paragraph']}</p>" for item in ip_paragraphs)
|
||||||
|
legals_context = {
|
||||||
|
"hoster_name": legals_vars.get("hoster_name", ""),
|
||||||
|
"hoster_adress": legals_vars.get("hoster_adress", ""),
|
||||||
|
"hoster_contact": legals_vars.get("hoster_contact", ""),
|
||||||
|
"intellectual_property": paragraphs_html,
|
||||||
|
}
|
||||||
|
legals_body = render_template(TEMPLATE_DIR / "legals.html", legals_context)
|
||||||
|
legals_html = f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{legals_body}\n{footer}\n</html>"
|
||||||
|
output_legals = BUILD_DIR / "legals" / "index.html"
|
||||||
|
output_legals.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(output_legals, "w", encoding="utf-8") as f:
|
||||||
|
f.write(legals_html)
|
||||||
|
logging.info(f"[✓] Legals page generated: {output_legals}")
|
||||||
|
else:
|
||||||
|
logging.warning("[~] No legals section found in site.yaml")
|
||||||
|
|
||||||
|
# Hero carrousel generator
|
||||||
|
if hero_images:
|
||||||
|
generate_gallery_json_from_images(hero_images, BUILD_DIR)
|
||||||
|
else:
|
||||||
|
logging.warning("[~] No hero images found, skipping JSON generation.")
|
||||||
|
|
||||||
|
# Sitemap and robot.txt generator
|
||||||
|
site_info = site_vars.get("info", {})
|
||||||
|
canonical_url = site_info.get("canonical", "").rstrip("/")
|
||||||
|
if canonical_url:
|
||||||
|
allowed_pages = ["/", "/legals/"]
|
||||||
|
generate_robots_txt(canonical_url, allowed_pages, BUILD_DIR)
|
||||||
|
generate_sitemap_xml(canonical_url, allowed_pages, BUILD_DIR)
|
||||||
|
else:
|
||||||
|
logging.warning("[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation.")
|
||||||
|
|
||||||
|
logging.info("✅ Build complete.")
|
||||||
|
|
@ -19,10 +19,25 @@ def load_theme_config(theme_name, themes_dir):
|
|||||||
theme_vars = yaml.safe_load(f)
|
theme_vars = yaml.safe_load(f)
|
||||||
return theme_vars, theme_dir
|
return theme_vars, theme_dir
|
||||||
|
|
||||||
def ensure_dir(path):
|
def clear_dir(path: Path):
|
||||||
if path.exists():
|
if not path.exists():
|
||||||
rmtree(path)
|
path.mkdir(parents=True)
|
||||||
path.mkdir(parents=True)
|
return
|
||||||
|
|
||||||
|
# Remove all files and subdirectories inside path, but not path itself
|
||||||
|
for child in path.iterdir():
|
||||||
|
if child.is_file() or child.is_symlink():
|
||||||
|
child.unlink() # delete file or symlink
|
||||||
|
elif child.is_dir():
|
||||||
|
rmtree(child) # delete directory and contents
|
||||||
|
|
||||||
|
# Then replace your ensure_dir with this:
|
||||||
|
|
||||||
|
def ensure_dir(path: Path):
|
||||||
|
if not path.exists():
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
else:
|
||||||
|
clear_dir(path)
|
||||||
|
|
||||||
def copy_assets(js_dir, style_dir, build_dir):
|
def copy_assets(js_dir, style_dir, build_dir):
|
||||||
for folder in [js_dir, style_dir]:
|
for folder in [js_dir, style_dir]:
|
||||||
|
Reference in New Issue
Block a user