Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/bloggereasy/multipage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Multi-page site generator for BloggerEasy.

Generates a coordinated set of pages (home, about, contact) from a single
configuration, producing Blogger XML themes for each page.
"""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from bloggereasy.integrations.sdk import generate_from_html
from bloggereasy.theme.presets import PRESETS, apply_preset


MULTIPAGE_TEMPLATES = {
"home": "templates/multipage/home.html",
"about": "templates/multipage/about.html",
"contact": "templates/multipage/contact.html",
}

DEFAULT_CONFIG: dict[str, Any] = {
"site_title": "My Blog",
"site_tagline": "Thoughts and stories",
"hero_text": "Welcome to our corner of the internet.",
"year": 2025,
"contact_email": "hello@example.com",
"contact_twitter": "@myblog",
"contact_github": "myblog",
"features": [
{"title": "Fast", "description": "Optimized for speed"},
{"title": "Responsive", "description": "Looks great everywhere"},
{"title": "Accessible", "description": "Built for everyone"},
],
"team_members": [
{"name": "Jane Doe", "role": "Founder", "bio": "Building since 2020."},
{"name": "John Smith", "role": "Engineer", "bio": "Full-stack developer."},
],
}


def _render_template(template_path: str, config: dict[str, Any]) -> str:
"""Simple mustache-style template rendering."""
text = Path(template_path).read_text(encoding="utf-8")
for key, value in config.items():
if isinstance(value, str):
text = text.replace("{{" + key + "}}", value)
return text


def generate_multipage(
config: dict[str, Any] | None = None,
template: str = "simple",
output_dir: Path | str | None = None,
) -> dict[str, Any]:
"""Generate a multi-page blog site with home, about, and contact pages.

Args:
config: Site configuration dict. Merged with DEFAULT_CONFIG.
template: Theme preset name from PRESETS.
output_dir: Directory for output XML files.

Returns:
Dict with per-page results and validation status.
"""
cfg = {**DEFAULT_CONFIG, **(config or {})}
out = Path(output_dir) if output_dir else Path("output")
out.mkdir(parents=True, exist_ok=True)

if template not in PRESETS:
template = "simple"

results: dict[str, Any] = {"template": template, "pages": {}}

for page_name, tpl_rel in MULTIPAGE_TEMPLATES.items():
tpl_path = Path(__file__).resolve().parents[2] / tpl_rel
if not tpl_path.exists():
results["pages"][page_name] = {"ok": False, "error": f"Template not found: {tpl_path}"}
continue

html_content = _render_template(str(tpl_path), cfg)
out_file = out / f"{page_name}.xml"

gen_result = generate_from_html(
html_content if isinstance(html_content, str) else str(html_content),
out_file,
template=template,
)
results["pages"][page_name] = {
"ok": gen_result.get("validation", {}).get("ok", False),
"output": str(out_file),
"additions": gen_result.get("additions", 0),
}

results["all_ok"] = all(p.get("ok") for p in results["pages"].values())
return results


def list_templates() -> list[str]:
"""List available multipage template names."""
return sorted(MULTIPAGE_TEMPLATES.keys())
35 changes: 35 additions & 0 deletions templates/multipage/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About — {{site_title}}</title>
<style>{{theme_css}}</style>
</head>
<body>
<header>
<h1>{{site_title}}</h1>
<p>{{site_tagline}}</p>
</header>
<nav>
<a href="index.html">Home</a>
<a href="about.html" class="active">About</a>
<a href="contact.html">Contact</a>
</nav>
<main class="container">
<h2>About {{site_title}}</h2>
<div class="about-content">
{{#team_members}}
<div class="team-card">
<h3>{{name}}</h3>
<p class="role">{{role}}</p>
<p>{{bio}}</p>
</div>
{{/team_members}}
</div>
</main>
<footer>
<p>&copy; {{year}} {{site_title}}. All rights reserved.</p>
</footer>
</body>
</html>
40 changes: 40 additions & 0 deletions templates/multipage/contact.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact — {{site_title}}</title>
<style>{{theme_css}}</style>
</head>
<body>
<header>
<h1>{{site_title}}</h1>
<p>{{site_tagline}}</p>
</header>
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="contact.html" class="active">Contact</a>
</nav>
<main class="container">
<h2>Get in Touch</h2>
<div class="contact-layout">
<form class="contact-form">
<label>Name<input type="text" name="name" required></label>
<label>Email<input type="email" name="email" required></label>
<label>Message<textarea name="message" rows="5"></textarea></label>
<button type="submit">Send Message</button>
</form>
<div class="contact-info">
<h3>Other Ways to Reach Us</h3>
<p>Email: {{contact_email}}</p>
<p>Twitter: {{contact_twitter}}</p>
<p>GitHub: {{contact_github}}</p>
</div>
</div>
</main>
<footer>
<p>&copy; {{year}} {{site_title}}. All rights reserved.</p>
</footer>
</body>
</html>
37 changes: 37 additions & 0 deletions templates/multipage/home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{site_title}} — Home</title>
<style>{{theme_css}}</style>
</head>
<body>
<header>
<h1>{{site_title}}</h1>
<p>{{site_tagline}}</p>
</header>
<nav>
<a href="index.html" class="active">Home</a>
<a href="about.html">About</a>
<a href="contact.html">Contact</a>
</nav>
<main class="container">
<section class="hero">
<h2>Welcome to {{site_title}}</h2>
<p>{{hero_text}}</p>
</section>
<section class="features">
{{#features}}
<div class="feature-card">
<h3>{{title}}</h3>
<p>{{description}}</p>
</div>
{{/features}}
</section>
</main>
<footer>
<p>&copy; {{year}} {{site_title}}. All rights reserved.</p>
</footer>
</body>
</html>
45 changes: 45 additions & 0 deletions tests/test_multipage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from pathlib import Path

from bloggereasy.multipage import generate_multipage, list_templates
from bloggereasy.theme.presets import PRESETS


def test_list_templates() -> None:
templates = list_templates()
assert "home" in templates
assert "about" in templates
assert "contact" in templates
assert len(templates) == 3


def test_generate_multipage_creates_all_pages(tmp_path: Path) -> None:
result = generate_multipage(output_dir=tmp_path)
assert result["all_ok"], f"Not all pages passed: {result}"
assert (tmp_path / "home.xml").exists()
assert (tmp_path / "about.xml").exists()
assert (tmp_path / "contact.xml").exists()


def test_generate_multipage_with_custom_config(tmp_path: Path) -> None:
config = {
"site_title": "Test Blog",
"site_tagline": "Testing",
"hero_text": "Hello Test",
}
result = generate_multipage(config=config, output_dir=tmp_path, template="simple")
assert result["all_ok"]
for page in result["pages"].values():
assert page["ok"]


def test_generate_multipage_rejects_bad_template(tmp_path: Path) -> None:
result = generate_multipage(template="nonexistent", output_dir=tmp_path)
assert result["template"] == "simple" # falls back


def test_multipage_supports_all_presets(tmp_path: Path) -> None:
for preset_name in ["simple", "dark", "magazine", "personal", "docs"]:
result = generate_multipage(template=preset_name, output_dir=tmp_path / preset_name)
assert result["all_ok"], f"{preset_name} failed: {result}"