- 落地页 footer 两行:产品行新增「让运维更简单,让开发更高效」,法律行补 © 杭州日理万机智能科技有限公司 + 浙ICP备2026068677号(链 beian.miit.gov.cn)+ @日理万机 - 文档页 footer 同步补「许可和定价」入口与备案号 - site.css 新增 .foot-tag/.foot-legal/.nowrap(移动端 @日理万机 不再断行) - tagline 去掉「Grafana」「—— 不写 YAML、不装 kubectl/Helm」,删除 hero-meta 实现细节行,meta description 同步 - 重建 dist 产物
196 lines
11 KiB
Python
196 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""把 /srv/LokiStack/docs/user-guide/*.md 按 manifest.json 静态渲染成文档站,
|
|
输出到 /srv/LokiStack/homepage/dist/docs/ (与 landing 一起构成 dist 产物) // Logan.Li
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import markdown
|
|
|
|
SRC = Path("/srv/LokiStack/docs")
|
|
GUIDE = SRC / "user-guide"
|
|
OUT = Path("/srv/LokiStack/homepage/dist")
|
|
DOCS_OUT = OUT / "docs"
|
|
|
|
CSS = """
|
|
:root{--brand:#059669;--brand-d: #047857;--ink:#0f172a;--muted:#5b6676;--line:#e6e9ee;--bg:#fff;--side:#f7f9f8}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;font:15px/1.75 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;color:var(--ink);background:var(--bg)}
|
|
a{color:var(--brand-d);text-decoration:none}a:hover{text-decoration:underline}
|
|
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:18px;height:56px;padding:0 20px;background:rgba(255,255,255,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
|
.brand{display:flex;align-items:center;gap:9px;font-weight:800;font-size:15px;color:var(--ink)}
|
|
.brand i{width:26px;height:26px;border-radius:8px;background:var(--brand);color:#fff;display:grid;place-items:center;font-style:normal;font-weight:800;font-size:14px}
|
|
.topnav{display:flex;gap:16px;margin-left:auto;font-size:14px}
|
|
.topnav a{color:var(--muted)}.topnav a:hover{color:var(--brand-d)}
|
|
.layout{display:grid;grid-template-columns:250px minmax(0,1fr) 200px;gap:34px;max-width:1240px;margin:0 auto;padding:26px 20px 70px}
|
|
aside.side{position:sticky;top:80px;align-self:start;max-height:calc(100vh - 100px);overflow:auto;font-size:14px}
|
|
.side h4{margin:18px 0 6px;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:#93a0af;font-weight:700}
|
|
.side a{display:block;padding:5px 10px;border-radius:7px;color:#3b4656}
|
|
.side a:hover{background:var(--side);text-decoration:none}
|
|
.side a.on{background:#e8f7f1;color:var(--brand-d);font-weight:650}
|
|
article{min-width:0}
|
|
article h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
|
|
.meta{color:#98a2b3;font-size:12.5px;margin-bottom:22px}
|
|
article h2{font-size:21px;margin:32px 0 10px;padding-bottom:7px;border-bottom:1px solid var(--line)}
|
|
article h3{font-size:17px;margin:24px 0 8px}
|
|
article p{margin:12px 0}
|
|
article img{max-width:100%;height:auto;border:1px solid var(--line);border-radius:10px;margin:14px 0;box-shadow:0 1px 3px rgba(16,24,40,.06)}
|
|
article pre{background:#0f172a;color:#e6edf3;padding:14px 16px;border-radius:10px;overflow:auto;font-size:13px;line-height:1.6}
|
|
article code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}
|
|
article :not(pre)>code{background:#f1f5f9;padding:2px 5px;border-radius:5px;color:#0f766e}
|
|
article table{border-collapse:collapse;width:100%;margin:14px 0;font-size:14px}
|
|
article th,article td{border:1px solid var(--line);padding:8px 10px;text-align:left}
|
|
article th{background:var(--side)}
|
|
article blockquote{margin:14px 0;padding:10px 16px;border-left:3px solid var(--brand);background:#f4fbf8;color:#33415c;border-radius:0 8px 8px 0}
|
|
.toc{position:sticky;top:84px;align-self:start;font-size:13px;color:var(--muted)}
|
|
.toc b{display:block;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:#93a0af;margin-bottom:8px}
|
|
.toc a{display:block;padding:3px 0 3px 10px;border-left:2px solid var(--line);color:#5b6676}
|
|
.toc a:hover{color:var(--brand-d);border-color:var(--brand)}
|
|
.pager{display:flex;justify-content:space-between;gap:16px;margin-top:46px;padding-top:18px;border-top:1px solid var(--line);font-size:14px}
|
|
footer{border-top:1px solid var(--line);padding:20px;text-align:center;color:#98a2b3;font-size:12.5px}
|
|
@media (max-width:1080px){.layout{grid-template-columns:230px minmax(0,1fr)}.toc{display:none}}
|
|
@media (max-width:820px){.layout{grid-template-columns:1fr;padding-top:16px}aside.side{position:static;max-height:none;border-bottom:1px solid var(--line);padding-bottom:10px}.topnav{display:none}}
|
|
"""
|
|
|
|
H5_FIX = """<style id="h5-fix">@media (max-width:820px){body{-webkit-text-size-adjust:100%}nav a,nav button,.nav-links a,.nav-menu a,.nav-item,.topnav a,.menu a,.side a,.sidebar a,.docs-side a,.toc a,footer a,a.btn,.plan .btn,button.btn{display:inline-flex;align-items:center;min-height:44px}.side a,.sidebar a,.docs-side a{padding:10px 12px;border-radius:8px}.toc a{padding:8px 0}pre{max-width:100%;overflow-x:auto}.plans,.grid,.grid-2,.grid-3{grid-template-columns:1fr!important}.hero h1,.hero-title{font-size:clamp(24px,7vw,34px)!important}.tbl-scroll{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}}</style>
|
|
<script id="h5-fix-js">(function(){var f=function(){document.querySelectorAll("table").forEach(function(t){var p=t.parentElement;if(p&&p.classList&&p.classList.contains("tbl-scroll"))return;var w=document.createElement("div");w.className="tbl-scroll";p.insertBefore(w,t);w.appendChild(t);});};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",f);else f();})();</script>"""
|
|
|
|
PAGE = """<!doctype html>
|
|
<html lang="zh-CN"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>{title} · LokiStack 文档</title>
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23059669'/%3E%3Ctext x='32' y='44' font-size='34' font-weight='800' fill='%23fff' text-anchor='middle' font-family='sans-serif'%3EL%3C/text%3E%3C/svg%3E">
|
|
<link rel="stylesheet" href="/docs/assets/loki-docs.css">
|
|
{h5fix}
|
|
</head><body>
|
|
<div class="topbar">
|
|
<a class="brand" href="/"><i>L</i>LokiStack</a>
|
|
<span style="color:#98a2b3;font-size:13px">文档</span>
|
|
<nav class="topnav"><a href="/">官网</a><a href="/docs/" class="on">文档</a></nav>
|
|
</div>
|
|
<div class="layout">
|
|
<aside class="side">{side}</aside>
|
|
<article>
|
|
<h1>{h1}</h1>
|
|
<div class="meta">{group} · 更新于 {updated}</div>
|
|
{body}
|
|
<div class="pager">{pager}</div>
|
|
</article>
|
|
<nav class="toc"><b>本页目录</b>{toc}</nav>
|
|
</div>
|
|
<footer>LokiStack · <a href="/">返回官网</a> · <a href="/pricing">许可和定价</a> · © 日理万机 · <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">浙ICP备2026068677号</a></footer>
|
|
</body></html>
|
|
"""
|
|
|
|
|
|
def slugify(t):
|
|
return re.sub(r"[^\w\u4e00-\u9fa5]+", "-", t.lower()).strip("-")
|
|
|
|
|
|
def main():
|
|
manifest = json.loads((GUIDE / "manifest.json").read_text(encoding="utf-8"))
|
|
groups = manifest.get("groups", [])
|
|
version = manifest.get("productVersion", "")
|
|
|
|
pages = []
|
|
for g in groups:
|
|
for p in g.get("pages", []):
|
|
pages.append({
|
|
"group": g.get("name", ""),
|
|
"file": p.get("file", ""),
|
|
"slug": p.get("slug") or slugify(p.get("title", "")),
|
|
"title": p.get("title", ""),
|
|
"updated": p.get("updated", ""),
|
|
})
|
|
print(f" manifest: {len(groups)} 组 / {len(pages)} 页 (v{version})")
|
|
|
|
# 资源
|
|
DOCS_OUT.mkdir(parents=True, exist_ok=True)
|
|
# 文档资源:优先用 homepage/assets(站点自包含,不再依赖仓库 docs/ 目录) // Logan.Li
|
|
hp_assets = Path("/srv/LokiStack/homepage/assets")
|
|
if hp_assets.exists():
|
|
shutil.copytree(hp_assets, DOCS_OUT / "assets", dirs_exist_ok=True)
|
|
if (SRC / "assets").exists():
|
|
shutil.copytree(SRC / "assets", DOCS_OUT / "assets", dirs_exist_ok=True)
|
|
(DOCS_OUT / "assets").mkdir(parents=True, exist_ok=True)
|
|
(DOCS_OUT / "assets" / "loki-docs.css").write_text(CSS, encoding="utf-8")
|
|
print(f" assets: {(SRC / 'assets').exists() and '已复制' or '无'}(含 screenshots)")
|
|
|
|
md = markdown.Markdown(extensions=["extra", "toc", "sane_lists", "nl2br"],
|
|
extension_configs={"toc": {"permalink": False}})
|
|
|
|
# 侧栏(所有页共用一个 DOM)
|
|
side = ""
|
|
last_group = None
|
|
for p in pages:
|
|
if p["group"] != last_group:
|
|
side += f'<h4>{p["group"]}</h4>'
|
|
last_group = p["group"]
|
|
side += f'<a data-slug="{p["slug"]}" href="/docs/{p["slug"]}/">{p["title"]}</a>'
|
|
|
|
out_pages = []
|
|
for i, p in enumerate(pages):
|
|
src = GUIDE / p["file"]
|
|
if not src.exists():
|
|
print(f" ⚠️ 缺文件: {p['file']}")
|
|
continue
|
|
text = src.read_text(encoding="utf-8")
|
|
# 第一行若是 H1 就当标题用掉
|
|
h1 = p["title"]
|
|
m = re.match(r"^\s*#\s+(.+)$", text, re.M)
|
|
if m:
|
|
h1 = m.group(1).strip()
|
|
text = re.sub(r"^\s*#\s+.+\n", "", text, count=1)
|
|
md.reset()
|
|
body = md.convert(text)
|
|
toc_html = ""
|
|
for t in md.toc_tokens:
|
|
if t["level"] <= 3:
|
|
toc_html += f'<a href="#{t["id"]}">{t["name"]}</a>'
|
|
# 图片相对路径: docs 页里的 images 走 /docs/assets/screenshots/
|
|
body = re.sub(r'src="(?!https?:|/|data:)', 'src="/docs/assets/', body)
|
|
pager = ""
|
|
if i > 0:
|
|
pager += f'<a href="/docs/{pages[i-1]["slug"]}/">← {pages[i-1]["title"]}</a>'
|
|
else:
|
|
pager += "<span></span>"
|
|
if i < len(pages) - 1:
|
|
pager += f'<a href="/docs/{pages[i+1]["slug"]}/">{pages[i+1]["title"]} →</a>'
|
|
html = PAGE.format(h5fix=H5_FIX, title=h1, h1=h1, group=p["group"], updated=p["updated"] or "—",
|
|
body=body, side=side, toc=toc_html, pager=pager)
|
|
html = html.replace('href="/docs/%s/"' % p["slug"], 'href="/docs/%s/" class="on"' % p["slug"])
|
|
d = DOCS_OUT / p["slug"]
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
(d / "index.html").write_text(html, encoding="utf-8")
|
|
out_pages.append(p)
|
|
print(f" ✓ /docs/{p['slug']}/ ({len(html)}B)")
|
|
|
|
# /docs/ 入口 = 第一页内容
|
|
if out_pages:
|
|
first = out_pages[0]
|
|
idx = (DOCS_OUT / first["slug"] / "index.html").read_text(encoding="utf-8")
|
|
(DOCS_OUT / "index.html").write_text(idx, encoding="utf-8")
|
|
print(f" ✓ /docs/ → {first['title']}")
|
|
|
|
# 落地页与资源一并进 dist(与其它三站一致的“dist 即产物”结构)
|
|
hp = Path("/srv/LokiStack/homepage")
|
|
for page in ("index.html", "pricing.html"):
|
|
if (hp / page).exists():
|
|
shutil.copy2(hp / page, OUT / page)
|
|
if (hp / "assets").exists():
|
|
# 只同步顶层静态资源(site.css 等);截图仅由 docs 走 /docs/assets/screenshots/,避免 dist 里存两份副本 // Logan.Li
|
|
(OUT / "assets").mkdir(parents=True, exist_ok=True)
|
|
for item in (hp / "assets").iterdir():
|
|
if item.is_file():
|
|
shutil.copy2(item, OUT / "assets" / item.name)
|
|
print(f" 落地页/资源已同步进 dist")
|
|
print(f" 产物文件数: {sum(1 for _ in OUT.rglob('*') if _.is_file())}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|