Compare commits
	
		
			3 Commits
		
	
	
		
			3901bf8acf
			...
			7e1a5e659f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1a5e659f | |||
| f5a5aefd09 | |||
| f76420b2c3 | 
							
								
								
									
										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: []
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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()
 | 
				
			||||||
							
								
								
									
										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
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										184
									
								
								src/py/site_builder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/py/site_builder.py
									
									
									
									
									
										Normal 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.")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
		Reference in New Issue
	
	Block a user