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:
parent
176be89319
commit
68ca0ef6e7
8 changed files with 97 additions and 68 deletions
30
CLAUDE.md
30
CLAUDE.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'"""
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue