Connexion
FR
#!/usr/bin/env python3"""Writes the web site's pages from i18n.py: site/index.html (English), site/<code>/index.html, sitemap.xml, robots.txt.    python3 pages.py <site dir>Absolute URLs are https://{{DOMAIN}}/... (bin/jumphp deploy / the playground replace {{DOMAIN}})."""import htmlimport jsonimport osimport syssys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))from i18n import T, ORDER  # noqa: E402from i18n_more import M  # noqa: E402for _code in ORDER:    T[_code].update(M[_code])    T[_code]['caps'] = {**T[_code]['caps'], **T[_code].pop('caps2')}    T[_code]['faq'] = T[_code]['faq'] + T[_code].pop('faq2')site = sys.argv[1]def french_spaces(value):    """French typography: a no-break space before : ; ? ! and inside « »."""    if isinstance(value, str):        for mark in (' :', ' ;', ' ?', ' !'):            value = value.replace(mark, '\u202f' + mark[1] if mark != ' :' else '\u00a0:')        return value.replace('« ', '«\u00a0').replace(' »', '\u00a0»')    if isinstance(value, list):        return [french_spaces(v) for v in value]    if isinstance(value, tuple):        return tuple(french_spaces(v) for v in value)    if isinstance(value, dict):        return {k: french_spaces(v) for k, v in value.items()}    return valueT['fr'] = french_spaces(T['fr'])MAIN = ORDER[0]BASE = 'https://{{DOMAIN}}/'SHOTS = ['general', 'console', 'performance', 'search', 'logs', 'snapshots', 'new-vm', 'menu']MORE_SHOTS = ['dashboard', 'updates', 'new-pool', 'cross-pool', 'new-sr-nfs', 'new-network', 'folders', 'alerts-dialog']GROUP_ICONS = ['&#9638;', '&#9707;', '&#8635;', '&#9873;']ICONS = ['&#9776;', '&#9638;', '&#9000;', '&#8776;', '&#9654;', '&#9719;', '&#9881;', '&#8635;']CONNECT_ICONS = ['&#8644;', '&gt;_', '&#9673;', '&#8962;']def e(text):    return html.escape(text, quote=True)def hreflang(code):    return 'zh-Hans' if code == 'zh' else codedef url(code):    return BASE if code == MAIN else f'{BASE}{code}/'CSS = """:root{--dark:#1b335a;--light:#2f5d94;--accent:#2f6fd6;--grey:#eef1f5;--line:#dbe1ea;--text:#1d2633;--muted:#5b6778}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans",Ubuntu,sans-serif;color:var(--text);background:var(--grey)}img{max-width:100%;height:auto;display:block}a{color:var(--accent)}.wrap{max-width:1120px;margin:0 auto;padding:0 16px}header.top{background:var(--dark);color:#fff;position:relative;z-index:5;box-shadow:0 1px 0 rgba(255,255,255,.08)}header.top .wrap{display:flex;align-items:center;gap:16px;height:56px}.brand{display:flex;align-items:center;gap:10px;color:#fff;text-decoration:none;font-weight:700;white-space:nowrap}.brand img{width:28px;height:28px;border-radius:6px}nav.links{display:flex;gap:18px;margin-left:auto}nav.links a{color:#d6e2f3;text-decoration:none;font-size:15px}nav.links a:hover{color:#fff}details.langs{position:relative;margin-left:8px}details.langs summary{list-style:none;cursor:pointer;color:#fff;border:1px solid rgba(255,255,255,.35);border-radius:8px;padding:4px 10px;font-size:14px;white-space:nowrap}details.langs summary::-webkit-details-marker{display:none}details.langs ul{position:absolute;right:0;top:36px;margin:0;padding:6px;list-style:none;background:#fff;border:1px solid var(--line);border-radius:10px;box-shadow:0 10px 30px rgba(20,40,80,.2);min-width:170px;columns:2;column-gap:4px}details.langs li a{display:block;padding:5px 10px;border-radius:6px;color:var(--text);text-decoration:none;font-size:14px}details.langs li a:hover,details.langs li a[aria-current]{background:var(--grey);color:var(--accent)}.hero{background:linear-gradient(120deg,var(--dark),var(--light));color:#fff;padding:56px 0 0;overflow:hidden}.tag{display:inline-block;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.25);border-radius:999px;padding:3px 12px;font-size:14px;color:#e3ecf8}.hero h1{font-size:44px;line-height:1.15;margin:16px 0 14px;max-width:820px;overflow-wrap:break-word}.hero p.lead{font-size:19px;color:#d9e4f3;max-width:760px;margin:0 0 26px}.cta{display:inline-block;background:var(--accent);color:#fff;text-decoration:none;font-weight:700;font-size:19px;padding:14px 30px;border-radius:10px;box-shadow:0 6px 18px rgba(10,30,70,.35)}.cta:hover{background:#255fbe}.hero .cta{background:#fff;color:var(--dark)}.hero .cta:hover{background:#e8effa}.hint{font-size:15px;color:#c6d5ea;margin:12px 0 0}.shot-frame{margin:40px auto 0;max-width:1000px;background:#fff;border-radius:12px 12px 0 0;padding:8px 8px 0;box-shadow:0 -4px 40px rgba(0,0,0,.25)}.shot-frame img{border-radius:6px 6px 0 0}section{padding:64px 0}section.white{background:#fff}h2{font-size:30px;line-height:1.25;margin:0 0 8px;color:var(--dark)}.sub{color:var(--muted);margin:0 0 28px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:16px}.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:20px}section.white .card{background:var(--grey)}.card h3{margin:0 0 6px;font-size:18px;color:var(--dark);display:flex;align-items:center;gap:10px}.card h3 span{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;background:var(--accent);color:#fff;font-size:16px;flex:none}.card p{margin:0;color:var(--muted);font-size:15px}.gallery{display:grid;grid-template-columns:repeat(2,1fr);gap:20px}figure{margin:0;background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden}figcaption{padding:12px 16px;font-size:15px;color:var(--text)}.demo{background:linear-gradient(120deg,var(--dark),var(--light));color:#fff;text-align:center}.demo h2{color:#fff}.demo p{max-width:760px;margin:0 auto 26px;color:#d9e4f3}.demo .cta{background:#fff;color:var(--dark)}.group{margin:0 0 28px}.group .grid{grid-template-columns:repeat(auto-fill,minmax(300px,1fr))}.gh{font-size:21px;color:var(--dark);margin:0 0 12px;display:flex;align-items:center;gap:10px}.gh span{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:9px;background:var(--dark);color:#fff;font-size:17px}.card h4{margin:0 0 6px;font-size:17px;color:var(--dark)}.faq details{background:#fff;border:1px solid var(--line);border-radius:10px;margin:0 0 10px;padding:0 18px}.faq summary{cursor:pointer;font-weight:600;padding:14px 0;color:var(--dark)}.faq details p{margin:0 0 16px;color:var(--muted)}.again{text-align:center;margin-top:32px}footer{background:var(--dark);color:#c6d5ea;padding:32px 0;font-size:14px}footer p{margin:0 0 10px}footer .langlist{display:flex;flex-wrap:wrap;gap:6px 14px}footer .langlist a{color:#e3ecf8;text-decoration:none}@media (max-width:820px){nav.links{display:none}details.langs{margin-left:auto}.gallery{grid-template-columns:1fr}.hero h1{font-size:32px}.hero p.lead{font-size:17px}h2{font-size:25px}section{padding:44px 0}.hero{padding-top:36px}}@media (max-width:420px){.brand span{font-size:15px}.cta{font-size:17px;padding:13px 22px;display:block;text-align:center}.hero h1{font-size:28px}}"""def page(code):    t = T[code]    up = '' if code == MAIN else '../'    play = f'{up}play/'    img = f'{up}images/'    alternates = '\n'.join(f'<link rel="alternate" hreflang="{hreflang(c)}" href="{url(c)}">' for c in ORDER)    alternates += f'\n<link rel="alternate" hreflang="x-default" href="{BASE}">'    lang_items = '\n'.join(        f'<li><a href="{up}{"" if c == MAIN else c + "/"}" hreflang="{hreflang(c)}" lang="{hreflang(c)}"'        + (' aria-current="page"' if c == code else '') + f'>{e(T[c]["name"])}</a></li>'        for c in ORDER)    foot_langs = ' '.join(        f'<a href="{up}{"" if c == MAIN else c + "/"}" hreflang="{hreflang(c)}" lang="{hreflang(c)}">{e(T[c]["name"])}</a>' for c in ORDER)    app_ld = {        '@context': 'https://schema.org', '@type': 'SoftwareApplication',        'name': 'XenCenter (xenadmin) XCP - Jumphp', 'alternateName': 'XenCenter for JumPHP',        'description': t['desc'], 'url': url(code), 'inLanguage': hreflang(code),        'applicationCategory': 'UtilitiesApplication', 'applicationSubCategory': 'Virtualization management',        'operatingSystem': 'Linux, Windows, macOS, Web', 'isAccessibleForFree': True,        'offers': {'@type': 'Offer', 'price': '0', 'priceCurrency': 'USD'},        'image': f'{BASE}images/og.png',        'screenshot': [f'{BASE}images/{s}.jpg' for s in SHOTS + MORE_SHOTS],        'featureList': [f[0] for f in t['features']] + [i[0] for _, items in t['groups'] for i in items],        'potentialAction': {'@type': 'UseAction', 'target': f'{BASE}play/'},    }    faq_ld = {        '@context': 'https://schema.org', '@type': 'FAQPage', 'inLanguage': hreflang(code),        'mainEntity': [{'@type': 'Question', 'name': q, 'acceptedAnswer': {'@type': 'Answer', 'text': a}} for q, a in t['faq']],    }    ld = lambda d: json.dumps(d, ensure_ascii=False, indent=1).replace('</', '<\\/')    features = '\n'.join(        f'<div class="card"><h3><span aria-hidden="true">{ICONS[i]}</span>{e(ti)}</h3><p>{e(d)}</p></div>'        for i, (ti, d) in enumerate(t['features']))    connect = '\n'.join(        f'<div class="card"><h3><span aria-hidden="true">{CONNECT_ICONS[i]}</span>{e(ti)}</h3><p>{e(d)}</p></div>'        for i, (ti, d) in enumerate(t['connect']))    gallery = '\n'.join(        f'<figure><img src="{img}{s}.jpg" width="1280" height="800" decoding="async" alt="{e(t["caps"][s])}">'        f'<figcaption>{e(t["caps"][s])}</figcaption></figure>' for s in SHOTS)    groups = '\n'.join(        f'<div class="group"><h3 class="gh"><span aria-hidden="true">{GROUP_ICONS[g]}</span>{e(title)}</h3><div class="grid">'        + ''.join(f'<div class="card"><h4>{e(ti)}</h4><p>{e(d)}</p></div>' for ti, d in items) + '</div></div>'        for g, (title, items) in enumerate(t['groups']))    gallery2 = '\n'.join(        f'<figure><img src="{img}{s}.jpg" width="1280" height="800" decoding="async" alt="{e(t["caps"][s])}">'        f'<figcaption>{e(t["caps"][s])}</figcaption></figure>' for s in MORE_SHOTS)    faq = '\n'.join(f'<details><summary>{e(q)}</summary><p>{e(a)}</p></details>' for q, a in t['faq'])    og_alt = '\n'.join(f'<meta property="og:locale:alternate" content="{T[c]["locale"]}">' for c in ORDER if c != code)    return f"""<!doctype html><html lang="{hreflang(code)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{e(t['title'])}</title><meta name="description" content="{e(t['desc'])}"><link rel="canonical" href="{url(code)}">{alternates}<link rel="icon" type="image/png" href="{img}favicon.png"><link rel="apple-touch-icon" href="{img}icon-192.png"><meta name="theme-color" content="#1b335a"><meta property="og:type" content="website"><meta property="og:site_name" content="XenCenter for JumPHP"><meta property="og:title" content="{e(t['h1'])}"><meta property="og:description" content="{e(t['desc'])}"><meta property="og:url" content="{url(code)}"><meta property="og:image" content="{BASE}images/og.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:locale" content="{t['locale']}">{og_alt}<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="{e(t['h1'])}"><meta name="twitter:description" content="{e(t['desc'])}"><meta name="twitter:image" content="{BASE}images/og.png"><script type="application/ld+json">{ld(app_ld)}</script><script type="application/ld+json">{ld(faq_ld)}</script><style>{CSS}</style></head><body><header class="top"><div class="wrap"><a class="brand" href="{up or './'}"><img src="{img}favicon.png" width="28" height="28" alt=""><span>XenCenter · JumPHP</span></a><nav class="links" aria-label="Sections"><a href="#features">{e(t['nav_features'])}</a><a href="#screenshots">{e(t['nav_shots'])}</a><a href="#connect">{e(t['nav_connect'])}</a><a href="#faq">{e(t['nav_faq'])}</a></nav><details class="langs"><summary aria-label="{e(t['lang_label'])}">{e(t['name'])} &#9662;</summary><ul>{lang_items}</ul></details></div></header><main><div class="hero"><div class="wrap"><span class="tag">{e(t['tag'])}</span><h1>{e(t['h1'])}</h1><p class="lead">{e(t['lead'])}</p><a class="cta" href="{play}">{e(t['cta'])} &#8594;</a><p class="hint">{e(t['hint'])}</p><div class="shot-frame"><img src="{img}general.jpg" width="1280" height="800" alt="{e(t['alt_hero'])}"></div></div></div><section id="features"><div class="wrap"><h2>{e(t['features_h'])}</h2><p class="sub">{e(t['lead'])}</p><div class="grid">{features}</div></div></section><section id="more" class="white"><div class="wrap"><h2>{e(t['more_h'])}</h2><p class="sub">{e(t['more_lead'])}</p>{groups}</div></section><section id="screenshots"><div class="wrap"><h2>{e(t['shots_h'])}</h2><p class="sub">{e(t['shots_lead'])}</p><div class="gallery">{gallery}{gallery2}</div></div></section><section id="connect" class="white"><div class="wrap"><h2>{e(t['connect_h'])}</h2><p class="sub">{e(t['connect_lead'])}</p><div class="grid">{connect}</div></div></section><section class="demo"><div class="wrap"><h2>{e(t['demo_h'])}</h2><p>{e(t['demo_p'])}</p><a class="cta" href="{play}">{e(t['cta2'])} &#8594;</a></div></section><section id="faq" class="faq"><div class="wrap"><h2>{e(t['faq_h'])}</h2>{faq}<p class="again"><a class="cta" href="{play}">{e(t['cta'])} &#8594;</a></p></div></section></main><footer><div class="wrap"><p>{e(t['credit'])}</p><p>{e(t['made'])}</p><nav class="langlist" aria-label="{e(t['lang_label'])}">{foot_langs}</nav></div></footer></body></html>"""for code in ORDER:    folder = site if code == MAIN else os.path.join(site, code)    os.makedirs(folder, exist_ok=True)    with open(os.path.join(folder, 'index.html'), 'w', encoding='utf-8') as f:        f.write(page(code))links = '\n'.join(f'    <xhtml:link rel="alternate" hreflang="{hreflang(c)}" href="{url(c)}"/>' for c in ORDER)links += f'\n    <xhtml:link rel="alternate" hreflang="x-default" href="{BASE}"/>'urls = '\n'.join(f'  <url>\n    <loc>{url(c)}</loc>\n{links}\n  </url>' for c in ORDER)with open(os.path.join(site, 'sitemap.xml'), 'w', encoding='utf-8') as f:    f.write('<?xml version="1.0" encoding="UTF-8"?>\n'            '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n'            f'{urls}\n</urlset>\n')with open(os.path.join(site, 'robots.txt'), 'w', encoding='utf-8') as f:    f.write(f'User-agent: *\nAllow: /\n\nSitemap: {BASE}sitemap.xml\n')print('pages written:', ', '.join(ORDER))