feat: foxhop.net support — /attachment/ static view, pandoc standalone fix, figure/figcaption tags, avatar null guard, html5lib body fragment, chaostheory palette in common.css

This commit is contained in:
russell@unturf.com 2026-04-08 15:05:22 -04:00
parent 176be89319
commit 68ca0ef6e7
8 changed files with 97 additions and 68 deletions

View file

@ -317,16 +317,26 @@ uwsgi. Config managed via salt pillar at `foxhop-pillar/caddy/remarkbox.sls`.
### Domain routing map
| Domain | DNS → | Handler | Backend |
|--------|-------|---------|---------|
| `remarkbox.com` | proxy (142.93.73.64) | redirect → www | — |
| `www.remarkbox.com` | proxy | file_server | `/opt/www/remarkbox` on proxy |
| `my.remarkbox.com` | proxy | reverse_proxy | origin → uwsgi :6001 |
| `meta.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
| `faq.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
| `demo.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
| `origin.remarkbox.com` | direct (162.243.167.224) | reverse_proxy | uwsgi :6001 |
| `westworld2.com` | origin server | reverse_proxy | uwsgi :6002 |
| Domain | DNS → | Handler | Backend | Status |
|--------|-------|---------|---------|--------|
| `remarkbox.com` | proxy (142.93.73.64) | redirect → www | — | live |
| `www.remarkbox.com` | proxy | file_server | `/opt/www/remarkbox` on proxy | live |
| `my.remarkbox.com` | proxy | reverse_proxy | origin → uwsgi :6001 | live |
| `meta.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
| `faq.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
| `demo.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
| `origin.remarkbox.com` | direct (162.243.167.224) | reverse_proxy | uwsgi :6001 | live |
| `westworld2.com` | proxy | **parked** redirect → unturf.com | — | parked 2026-04-08 |
| `www.foxhop.net` | proxy | **no Caddy block** — not yet routed | — | unrouted 2026-04-08 |
**Planned multi-tenant consolidation (foxhop.net + westworld2.com → origin:6001):**
- Merge `foxhop.net.sqlite` + `westworld2.com.sqlite` into origin DB
- Set `rb_namespace.theme = 'chaostheory'` (foxhop) & `'westworld'` (westworld2) in DB
- Remove `app.namespace` override from foxhop config — `request.domain` drives namespace
- Add proxy Caddy blocks: `www.foxhop.net` & `westworld2.com` → origin:6001
- Theme packages installed locally: `remarkbox_chaostheory`, `remarkbox_westworld`
- Salt pillar ref: `~/git/foxhop-states/uwsgi/configs/westworld2.com.ini`
- foxhop local dev: `~/git/remarkbox/foxhop-local.ini` (port 6004)
### Request flow for proxied domains (my, meta, faq)

View file

@ -361,7 +361,8 @@ def main(global_config, **settings):
return get_or_create_namespace(request.dbsession, namespace_name)
elif request.node:
return request.node.root.namespace
return get_or_create_namespace(request.dbsession, request.domain)
forced = request.app.get("namespace")
return get_or_create_namespace(request.dbsession, forced if forced else request.domain)
def add_mode(request):
"""return mode of 'embed' or 'basic'"""

View file

@ -137,7 +137,7 @@ def get_available_input_formats():
return frozenset()
def convert(source, from_format="markdown", to_format="html5", title=None):
def convert(source, from_format="markdown", to_format="html5", title=None, standalone=True):
"""Convert source text from one format to another via pandoc.
Args:
@ -145,6 +145,8 @@ def convert(source, from_format="markdown", to_format="html5", title=None):
from_format: Pandoc input format name.
to_format: Pandoc output format name.
title: Optional document title (sets pandoc metadata).
standalone: Pass --standalone to pandoc (full document). Set False
for body fragments required when storing HTML in data_html.
Returns:
bytes for binary formats (pdf, docx, etc.), str for text formats.
@ -155,7 +157,9 @@ def convert(source, from_format="markdown", to_format="html5", title=None):
"""
is_binary = to_format in BINARY_FORMATS
cmd = ["pandoc", "-f", from_format, "-t", to_format, "--standalone"]
cmd = ["pandoc", "-f", from_format, "-t", to_format]
if standalone:
cmd.append("--standalone")
if title:
cmd.extend(["--metadata", "title={}".format(title)])

View file

@ -50,7 +50,8 @@ def default_cleaner(tag_acl=None):
if tag_acl is None:
tag_acl = {}
maybe_safe_tags = ["pre", "table", "tr", "td"]
maybe_safe_tags = ["pre", "table", "tr", "td", "th", "thead", "tbody",
"figure", "figcaption", "dl", "caption"]
tags = maybe_safe_tags + list(tag_acl.keys()) + markdown_tags
attrs = markdown_attrs
@ -224,4 +225,9 @@ def clean_raw_html(raw_html, cleaner=None):
# protect links from abuse.
soup = protect_links(soup, cleaner)
# html5lib always wraps output in <html><head><body> — extract body
# contents only so data_html stores a fragment, not a full document.
body = soup.find("body")
if body is not None:
return body.decode_contents(eventual_encoding="utf-8")
return soup.decode(eventual_encoding="utf-8")

View file

@ -237,6 +237,9 @@ class Node(RBase, Base):
def avatar_uri(self, **kwargs):
"""Return invatar URI. Optionally return gravatar URI."""
u = self.user if self.user else self.user_surrogate
if u is None:
# Node has no user or surrogate (e.g. imported wiki pages).
return None
if self.disabled:
kwargs["text"] = "-"
kwargs["bg"] = "#aaaaaa"
@ -338,7 +341,7 @@ class Node(RBase, Base):
from remarkbox.lib.pandoc import convert
from remarkbox.lib.render import make_cleaner_from_namespace
from remarkbox.lib.sanitize_html import default_cleaner, clean_raw_html
html = convert(data, from_format=self.source_format, to_format="html5")
html = convert(data, from_format=self.source_format, to_format="html5", standalone=False)
if namespace:
cleaner = make_cleaner_from_namespace(namespace)
else:

View file

@ -1,6 +1,7 @@
def includeme(config):
# shared routes
config.add_static_view("static", "static", cache_max_age=3600)
config.add_static_view("attachment", "remarkbox:static/attachment", cache_max_age=86400)
config.add_route("favicon", "/favicon.ico")
config.add_route("robots", "/robots.txt")

View file

@ -1,43 +1,43 @@
/* Variables - Light Mode (default) */
/* Variables - Chaostheory Dark (default) */
:root {
--color: #222222;
--color-muted: #3f3f3f;
--color: #dddddd;
--color-muted: #aaaaaa;
--color-code: #000000;
--color-code: #eeeeee;
--background: #ffffff;
--background-faint: #fafafa;
--background: #1c4e63;
--background-faint: #184254;
--primary: #d40000; /* Remarkbox logo color */
--primary-hover: #e4241a;
--primary-inverse: #ffffff;
--primary: #6dcff6; /* Chaostheory ice blue */
--primary-hover: #9de3fb;
--primary-inverse: #111111;
--border: #bbbbbb;
--border-faint: #e4e4e4;
--border: #336173;
--border-faint: #2a5468;
--color-mod: #aa0000; /* Consistency with Remarkbox style */
--color-mod: #6dcff6;
--line-height: 1.6;
}
/* Dark Mode Variables */
/* Deep Dark Mode */
:root.dark-mode {
--color: #e8e8e8;
--color-muted: #b0b0b0;
--color: #dddddd;
--color-muted: #999999;
--color-code: #e8e8e8;
--color-code: #eeeeee;
--background: #1a1a1a;
--background-faint: #2a2a2a;
--background: #111111;
--background-faint: #0a0a0a;
--primary: #ff5555; /* Brighter red for dark mode */
--primary-hover: #ff7777;
--primary-inverse: #1a1a1a;
--primary: #6dcff6;
--primary-hover: #9de3fb;
--primary-inverse: #111111;
--border: #444444;
--border-faint: #333333;
--border: #1c4e63;
--border-faint: #184254;
--color-mod: #ff5555;
--color-mod: #6dcff6;
--line-height: 1.6;
}
@ -166,7 +166,7 @@ blockquote {
margin-bottom: 10px;
margin-left: 15px;
padding-left: 15px;
border-left: 3px solid #ccc;
border-left: 3px solid var(--border);
}
button.link,
@ -271,15 +271,15 @@ form.node-action {
.focused {
animation: fade 4s forwards;
background-color: rgba(237, 237, 237, 1);
background-color: rgba(109, 207, 246, 0.15);
}
@keyframes fade {
from {
background-color: rgba(237, 237, 237, 1);
background-color: rgba(109, 207, 246, 0.15);
}
to {
background-color: rgba(237, 237, 237, 0);
background-color: rgba(109, 207, 246, 0);
}
}
@ -310,28 +310,28 @@ form.node-action {
.alert-danger,
.alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
color: #ffaaaa;
background-color: #4a1a1a;
border-color: #7a2020;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
color: #ffe08a;
background-color: #3a2e00;
border-color: #6a5200;
}
.alert-info,
.alert-notice {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
color: #6dcff6;
background-color: #184254;
border-color: #336173;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
color: #aaffbb;
background-color: #0e3020;
border-color: #1a5c30;
}
/* ALERT END */
@ -363,15 +363,15 @@ form.node-action {
border-radius: 1.2ex;
margin-right: 4px;
padding: 1px;
color: #cccccc;
background: white;
border: 1px solid #cccccc;
color: var(--color-muted);
background: var(--background-faint);
border: 1px solid var(--border);
text-decoration: none;
}
.question-mark-circle:hover:before {
color: white;
background: #cccccc;
color: var(--primary-inverse);
background: var(--primary);
text-decoration: none;
}
@ -391,7 +391,7 @@ form.node-action {
.load-more,
.load-more:visited {
color: #336699;
color: var(--primary);
}
.button-right {
@ -495,7 +495,8 @@ form.node-action {
.my-namespaces-div {
display: none;
position: absolute;
background-color: #ffffff;
background-color: var(--background);
border: 1px solid var(--border);
z-index: 1;
overflow: hidden;
padding-top: 10px;
@ -797,7 +798,7 @@ div.user-watching {
margin-top: -1px;
left: 0;
display: block;
background: #020304;
background: var(--primary-inverse);
transition: 0.5s;
}
.burger-label span:first-child {
@ -822,7 +823,7 @@ div.user-watching {
transform: rotate(-405deg);
}
.burger-checkbox ~ .phone-menu {
background: white;
background: var(--background-faint);
position: fixed;
bottom: 0;
right: 0;
@ -906,11 +907,11 @@ section.new-card,
section.payment-card,
section.join-or-log-in,
section.well {
background-color: white;
background-color: var(--background-faint);
padding: 10px 12px;
border-radius: 4px;
border: 1px solid transparent;
box-shadow: 0 1px 3px 0 #e6ebf1;
border: 1px solid var(--border);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
text-align: center;

View file

@ -1,9 +1,12 @@
{% macro node_avatar(node, size=request.avatar_size, align='left') %}
{% set _uri = node.avatar_uri(size=size) %}
{% if _uri %}
<img
src = "{{ node.avatar_uri(size=size) }}"
src = "{{ _uri }}"
class = "avatar nested-avatar"
align = "{{ align }}"
/>
{% endif %}
{% endmacro %}
{% macro user_avatar(user, size) %}