From 8bb94a39609f57be2c486f579849eebb04606808 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 25 Sep 2026 16:31:52 +0200 Subject: feat(hub): friendlier welcome page, link previews, robots.txt and favicon Welcome page: privacy said once, a three-step "how it works", a documentation box, download (green) and legal links under the sign-in form, on a dark gradient backdrop covering the whole page. Link previews: Open Graph tags in the app shell, rendered for identity.id, with the square icon as image. robots.txt, favicon and touch icon served at the origin root. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 43 ++++- packages/meshbay-hub/src/meshbay_hub/app.py | 4 + .../src/meshbay_hub/static/apple-touch-icon.png | Bin 0 -> 34590 bytes .../src/meshbay_hub/static/auth-page.js | 86 +++++++-- .../meshbay-hub/src/meshbay_hub/static/favicon.ico | Bin 0 -> 7291 bytes .../src/meshbay_hub/static/locales/de.js | 30 ++- .../src/meshbay_hub/static/locales/en.js | 30 ++- .../src/meshbay_hub/static/locales/es.js | 30 ++- .../src/meshbay_hub/static/locales/fr.js | 30 ++- .../src/meshbay_hub/static/locales/it.js | 30 ++- .../src/meshbay_hub/static/locales/ja.js | 30 ++- .../src/meshbay_hub/static/locales/nl.js | 30 ++- .../src/meshbay_hub/static/locales/pl.js | 30 ++- .../src/meshbay_hub/static/locales/pt-BR.js | 30 ++- .../src/meshbay_hub/static/locales/zh-CN.js | 30 ++- .../src/meshbay_hub/static/og-image.jpg | Bin 0 -> 33661 bytes .../meshbay-hub/src/meshbay_hub/static/robots.txt | 9 + .../meshbay-hub/src/meshbay_hub/static/style.css | 205 ++++++++++++++++----- 18 files changed, 482 insertions(+), 165 deletions(-) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/apple-touch-icon.png create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/favicon.ico create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/og-image.jpg create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/robots.txt (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 626c639..c9cb8d5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -12,6 +12,7 @@ The root route (/) returns the SPA HTML shell. """ import hashlib +import html from pathlib import Path from fastapi import APIRouter @@ -142,17 +143,49 @@ CSP = "; ".join([ @router.get("/app", response_class=HTMLResponse) async def app_root(): - return HTMLResponse(_HTML, headers=_NO_STORE) + return HTMLResponse(_shell, headers=_NO_STORE) @router.get("/app/{path:path}", response_class=HTMLResponse) async def app_catchall(path: str): - return HTMLResponse(_HTML, headers=_NO_STORE) + return HTMLResponse(_shell, headers=_NO_STORE) @router.get("/", response_class=HTMLResponse) async def index(): - return HTMLResponse(_HTML, headers=_NO_STORE) + return HTMLResponse(_shell, headers=_NO_STORE) + + +# What a messenger draws for a link to this hub: an invitation, or the address +# itself. Signal and WhatsApp read these tags and nothing else, and resolve only +# an absolute og:image, so the shell is rendered for the hub's public name — +# `identity.id`, the name mail links already use. Short on purpose: a preview +# shows a line or two of the description and cuts the rest. The image is the +# square icon, which is the shape a thumbnail is cut to anyway. +PREVIEW_DESCRIPTION = "Your files stay at home. Reach them from anywhere." + + +def _preview_tags(hub_id: str) -> str: + origin = html.escape(f"https://{hub_id}", quote=True) + return f"""\ + + + + + + + + + + + +""" + + +def configure(hub_id: str) -> None: + """Render the shell for this hub's public name (`identity.id`).""" + global _shell + _shell = _HTML.replace("{preview}", _preview_tags(hub_id)) _HTML = """\ @@ -171,6 +204,8 @@ _HTML = """\ MeshBay +{preview} + @@ -205,3 +240,5 @@ _HTML = """\ """.replace("{v}", ASSET_V) + +_shell = _HTML.replace("{preview}", _preview_tags("meshbay.org")) diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py index b1d9626..7ccf598 100644 --- a/packages/meshbay-hub/src/meshbay_hub/app.py +++ b/packages/meshbay-hub/src/meshbay_hub/app.py @@ -37,6 +37,7 @@ from meshbay_hub.api.signaling import router as signaling_router from meshbay_hub.api.users import router as users_router from meshbay_hub.api.users import set_config as users_set_config from meshbay_hub.api.webapp import ASSET_V, CSP, STATIC_DIR +from meshbay_hub.api.webapp import configure as webapp_configure from meshbay_hub.api.webapp import router as webapp_router from meshbay_hub.auth import generate_hub_keypair, load_hub_keypair from meshbay_hub.config import HubConfig @@ -97,6 +98,9 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI: from meshbay_hub.config import load_config if cfg is None: cfg = load_config() + # Here rather than in the lifespan: the shell is served by routes that exist + # as soon as the app does, and a test client need not start it to read them. + webapp_configure(cfg.identity.id) @asynccontextmanager async def lifespan(app: FastAPI): diff --git a/packages/meshbay-hub/src/meshbay_hub/static/apple-touch-icon.png b/packages/meshbay-hub/src/meshbay_hub/static/apple-touch-icon.png new file mode 100644 index 0000000..aacd0fa Binary files /dev/null and b/packages/meshbay-hub/src/meshbay_hub/static/apple-touch-icon.png differ diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index 70c48f2..2164eae 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -207,7 +207,9 @@ export function LoginPage({ onLogin }) { return html`
+
+
+ ${!platform.isNative && html`<${WelcomeLinks} />`} +
${!platform.isNative && html`<${WelcomePitch} />`}
@@ -244,20 +248,49 @@ export function LoginPage({ onLogin }) { // application the reader has already downloaded it, and the links point at the // project's own site rather than at whichever hub the application is set to. // -// Every sentence here is held to MESHBAY_DESIGN.md §2.3. "Data never transits -// the hub" is a claim the design makes; "the hub cannot read anything" is one it -// forbids (T3), which is why the list below says what never *reaches* the hub -// and stops there. "End-to-end" means device to node, as §2.3 defines it. +// Written for somebody who is not in IT: what it does for them first, how it +// works in three steps, and privacy said once, plainly. Every sentence is held +// to MESHBAY_DESIGN.md §2.3 — "your content never passes through meshbay.org" +// is a claim the design makes; "meshbay.org cannot read anything" is one it +// forbids (T3). "Encrypted all the way" means device to node, as §2.3 defines. const WELCOME_APPS = [ ['chat', 'welcome.app_chat'], ['image', 'welcome.app_photos'], ['video', 'welcome.app_media'], ['play', 'welcome.app_video'], ['music', 'welcome.app_music'], ]; +const WELCOME_STEPS = [ + ['home', 'welcome.step_home'], ['globe', 'welcome.step_anywhere'], + ['user', 'welcome.step_share'], +]; const WELCOME_USES = [ ['chat', 'welcome.use_chat'], ['image', 'welcome.use_photos'], ['cast', 'welcome.use_media'], ['pencil', 'welcome.use_apps'], ]; -const WELCOME_NEVER = ['welcome.never_transit', 'welcome.never_stored', 'welcome.never_e2e']; +const WELCOME_BADGES = ['welcome.badge_free', 'welcome.badge_open', + 'welcome.badge_no_ads', 'welcome.badge_no_tracking']; + +const REPO = 'https://git.meshbay.org/meshbay.git/about/'; +const WELCOME_DOCS = [ + ['folder', 'welcome.docs_source', [['welcome.docs_repo', REPO]]], + ['user', 'welcome.docs_user', [ + ['welcome.docs_quickstart', `${REPO}docs/QUICKSTART.md`], + ['welcome.docs_userguide', `${REPO}docs/USERGUIDE.md`]]], + ['gear', 'welcome.docs_devel', [ + ['welcome.docs_design', `${REPO}docs/MESHBAY_DESIGN.md`], + ['welcome.docs_protocol', `${REPO}docs/MESHBAY_NODE_PROTOCOL.md`]]], +]; + +// Under the sign-in form rather than at the foot of the text: on a desktop the +// text column runs far below the form, and these were the last thing on it. +function WelcomeLinks() { + return html` + + `; +} function WelcomePitch() { return html` @@ -268,8 +301,13 @@ function WelcomePitch() { ${WELCOME_APPS.map(([icon, key]) => html`
  • <${Icon} name=${icon} />${t(key)}
  • `)} -

    ${t('welcome.groups')}

    -

    <${Icon} name="lock" />${t('welcome.e2e')}

    + +

    ${t('welcome.how_title')}

    +
      + ${WELCOME_STEPS.map(([icon, key]) => html` +
    1. <${Icon} name=${icon} /> + ${t(key)}
    2. `)} +

    ${t('welcome.uses_title')}

    -
    -

    ${t('welcome.hub_title')}

    -

    ${t('welcome.hub_body')}

    -

    ${t('welcome.hub_never')}

    -
      - ${WELCOME_NEVER.map(key => html` -
    • <${Icon} name="check" />${t(key)}
    • `)} -
    -

    ${t('welcome.hub_free')}

    +
    + <${Icon} name="shield" /> +
    +

    ${t('welcome.private_title')}

    +

    ${t('welcome.private_body')}

    +
      + ${WELCOME_BADGES.map(key => html` +
    • <${Icon} name="check" />${t(key)}
    • `)} +
    +
    -