Skip to content
Merged
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
42 changes: 40 additions & 2 deletions scripts/markdown_postprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,47 @@ def fix_ordered_list_markers(text: str) -> str:
return re.sub(r"^(\d+\.)\xa0", r"\1 ", text, flags=re.MULTILINE)


def _extract_description(text: str, title: str, doc_set_title: str = "", max_len: int = 160) -> str:
"""Extract a meta description from the first meaningful paragraph of text.

Skips headings, blank lines, admonitions, and code fences to find the first
real paragraph. Falls back to a constructed description from title + doc set.
"""
for line in text.split("\n"):
stripped = line.strip()
# Skip blanks, headings, admonitions, fences, HTML, front-matter markers
if (
not stripped
or stripped.startswith("#")
or stripped.startswith("!!!")
or stripped.startswith("```")
or stripped.startswith("<")
or stripped.startswith("---")
or stripped.startswith("| ")
):
continue
# Remove markdown links but keep text
desc = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", stripped)
# Remove inline code backticks
desc = desc.replace("`", "")
# Remove bold/italic markers
desc = re.sub(r"\*{1,2}([^*]+)\*{1,2}", r"\1", desc)
if len(desc) > max_len:
desc = desc[: max_len - 3].rsplit(" ", 1)[0] + "..."
return desc

# Fallback
if doc_set_title:
return f"EnergyPlus {doc_set_title} — {title}"
return f"EnergyPlus documentation — {title}"


def add_front_matter(text: str, title: str, doc_set_title: str = "") -> str:
"""Add YAML front matter with title and tags."""
lines = ["---", f"title: {title}"]
"""Add YAML front matter with title, description, and tags."""
description = _extract_description(text, title, doc_set_title)
# Escape any YAML-special characters in the description
safe_desc = description.replace('"', '\\"')
lines = ["---", f"title: {title}", f'description: "{safe_desc}"']
if doc_set_title:
lines.append("tags:")
lines.append(f" - {doc_set_title}")
Expand Down
147 changes: 145 additions & 2 deletions scripts/version_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,45 @@ def generate_root_landing(output_dir: Path, versions: list[str]) -> Path:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EnergyPlus Documentation</title>
<title>EnergyPlus Documentation — All Versions | idfkit-docs</title>
<meta name="description" content="Comprehensive EnergyPlus documentation for all versions (v8.9 through {latest_title}). Input/Output Reference, Engineering Reference, Getting Started guides, and more — searchable and hyperlinked.">
<meta name="keywords" content="EnergyPlus, documentation, building energy simulation, HVAC, Input Output Reference, Engineering Reference, energy modeling, idfkit">
<link rel="canonical" href="https://docs.idfkit.com/">

<!-- Open Graph -->
<meta property="og:title" content="EnergyPlus Documentation — All Versions">
<meta property="og:description" content="Comprehensive EnergyPlus documentation for all versions. Input/Output Reference, Engineering Reference, and more — searchable and hyperlinked.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://docs.idfkit.com/">
<meta property="og:site_name" content="idfkit">
<meta property="og:locale" content="en_US">

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="EnergyPlus Documentation — All Versions">
<meta name="twitter:description" content="Comprehensive EnergyPlus documentation for all versions. Input/Output Reference, Engineering Reference, and more.">

<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "EnergyPlus Documentation",
"url": "https://docs.idfkit.com/",
"description": "Comprehensive EnergyPlus documentation for all versions — searchable and hyperlinked.",
"publisher": {{
"@type": "Person",
"name": "Samuel Letellier-Duchesne",
"url": "https://samuelduchesne.com"
}},
"isPartOf": {{
"@type": "WebSite",
"name": "idfkit",
"url": "https://idfkit.com"
}}
}}
</script>

<style>
*,*::before,*::after{{box-sizing:border-box;margin:0;padding:0}}
:root{{
Expand Down Expand Up @@ -210,6 +248,107 @@ def generate_root_landing(output_dir: Path, versions: list[str]) -> Path:
return output_path


def generate_robots_txt(output_dir: Path) -> Path:
"""Generate robots.txt for the documentation site.

Args:
output_dir: Root output directory for the site

Returns:
Path to the generated robots.txt
"""
content = """\
User-agent: *
Allow: /

Sitemap: https://docs.idfkit.com/sitemap.xml
"""
output_path = output_dir / "robots.txt"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content)
return output_path


def generate_sitemap(output_dir: Path, versions: list[str]) -> Path:
"""Generate a sitemap.xml listing the root and all version landing pages.

Individual version sub-sitemaps (if generated by Zensical) are referenced
via a sitemap index. This function always generates at minimum the
top-level version landing pages.

Args:
output_dir: Root output directory for the site
versions: Sorted list of version tags (newest first)

Returns:
Path to the generated sitemap.xml
"""
from datetime import date

today = date.today().isoformat()
latest_short = version_to_short(LATEST_VERSION)

urls = []
# Root page
urls.append(
f" <url>\n"
f" <loc>https://docs.idfkit.com/</loc>\n"
f" <lastmod>{today}</lastmod>\n"
f" <changefreq>monthly</changefreq>\n"
f" <priority>1.0</priority>\n"
f" </url>"
)

for v in versions:
short = version_to_short(v)
priority = "0.9" if short == latest_short else "0.5"
urls.append(
f" <url>\n"
f" <loc>https://docs.idfkit.com/{short}/</loc>\n"
f" <lastmod>{today}</lastmod>\n"
f" <changefreq>monthly</changefreq>\n"
f" <priority>{priority}</priority>\n"
f" </url>"
)

# Check for per-version sitemap generated by Zensical
version_sitemap = output_dir / short / "sitemap.xml"
if version_sitemap.exists():
# Include entries from the per-version sitemap
import xml.etree.ElementTree as ET

try:
tree = ET.parse(version_sitemap) # noqa: S314 - trusted build output
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
for url_el in tree.findall(".//sm:url", ns):
loc_el = url_el.find("sm:loc", ns)
if loc_el is not None and loc_el.text:
loc = loc_el.text
# Rewrite relative URLs to absolute
if not loc.startswith("http"):
loc = f"https://docs.idfkit.com/{short}/{loc.lstrip('/')}"
urls.append(
f" <url>\n"
f" <loc>{loc}</loc>\n"
f" <lastmod>{today}</lastmod>\n"
f" <changefreq>monthly</changefreq>\n"
f" <priority>{priority}</priority>\n"
f" </url>"
)
except ET.ParseError as e:
print(f"Warning: Could not parse {version_sitemap}: {e}")

sitemap = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + "\n".join(urls) + "\n</urlset>\n"
)

output_path = output_dir / "sitemap.xml"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(sitemap)
return output_path


def _version_sort_key(v: str) -> tuple[int, ...]:
"""Parse a version tag into a sortable tuple of ints."""
return tuple(int(x) for x in v.lstrip("v").split("."))
Expand Down Expand Up @@ -248,6 +387,8 @@ def deploy_single_version(version: str, build_dir: Path, deploy_dir: Path) -> No
versions = _discover_versions(deploy_dir)
generate_versions_json(versions, deploy_dir)
generate_root_landing(deploy_dir, versions)
generate_robots_txt(deploy_dir)
generate_sitemap(deploy_dir, versions)


def merge_version_outputs(
Expand Down Expand Up @@ -275,7 +416,9 @@ def merge_version_outputs(
shutil.rmtree(target)
shutil.copytree(site_dir, target)

# Generate versions.json and landing page (newest first)
# Generate versions.json, landing page, robots.txt, and sitemap (newest first)
versions = sorted(version_build_dirs.keys(), key=_version_sort_key, reverse=True)
generate_versions_json(versions, deploy_dir)
generate_root_landing(deploy_dir, versions)
generate_robots_txt(deploy_dir)
generate_sitemap(deploy_dir, versions)
8 changes: 7 additions & 1 deletion zensical.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
[project]
site_name = "idfkit-docs"
site_url = "https://docs.idfkit.com"
site_description = "EnergyPlus documentation built with Zensical"
site_description = "Comprehensive EnergyPlus documentation — Input/Output Reference, Engineering Reference, and more. Searchable, hyperlinked, and covering all versions."
site_author = "Samuel Letellier-Duchesne"
copyright = "Maintained by <a href='https://samuelduchesne.com'>samuelduchesne</a>."
repo_url = "https://github.com/idfkit/idfkit-docs"

[project.plugins.meta]
# Enables per-page meta description from YAML front matter

[project.plugins.tags]
# Enables tag-based navigation and filtering

[project.theme]
variant = "modern"
features = [
Expand Down