Merge pull request 'errors' (#5) from errors into main

Reviewed-on: #5
This commit is contained in:
2025-08-12 19:35:54 +02:00
7 changed files with 398 additions and 331 deletions

187
build.py
View File

@ -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()

View File

@ -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: []

View File

@ -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()

109
src/py/gallery_builder.py Normal file
View 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)")

View File

@ -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'

View File

@ -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

184
src/py/site_builder.py Normal file
View File

@ -0,0 +1,184 @@
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():
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.")