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 routing map
|
||||||
|
|
||||||
| Domain | DNS → | Handler | Backend |
|
| Domain | DNS → | Handler | Backend | Status |
|
||||||
|--------|-------|---------|---------|
|
|--------|-------|---------|---------|--------|
|
||||||
| `remarkbox.com` | proxy (142.93.73.64) | redirect → www | — |
|
| `remarkbox.com` | proxy (142.93.73.64) | redirect → www | — | live |
|
||||||
| `www.remarkbox.com` | proxy | file_server | `/opt/www/remarkbox` on proxy |
|
| `www.remarkbox.com` | proxy | file_server | `/opt/www/remarkbox` on proxy | live |
|
||||||
| `my.remarkbox.com` | proxy | reverse_proxy | origin → uwsgi :6001 |
|
| `my.remarkbox.com` | proxy | reverse_proxy | origin → uwsgi :6001 | live |
|
||||||
| `meta.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
|
| `meta.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
|
||||||
| `faq.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
|
| `faq.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
|
||||||
| `demo.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 |
|
| `demo.remarkbox.com` | CNAME → my → proxy | reverse_proxy | origin → uwsgi :6001 | live |
|
||||||
| `origin.remarkbox.com` | direct (162.243.167.224) | reverse_proxy | uwsgi :6001 |
|
| `origin.remarkbox.com` | direct (162.243.167.224) | reverse_proxy | uwsgi :6001 | live |
|
||||||
| `westworld2.com` | origin server | reverse_proxy | uwsgi :6002 |
|
| `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)
|
### 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)
|
return get_or_create_namespace(request.dbsession, namespace_name)
|
||||||
elif request.node:
|
elif request.node:
|
||||||
return request.node.root.namespace
|
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):
|
def add_mode(request):
|
||||||
"""return mode of 'embed' or 'basic'"""
|
"""return mode of 'embed' or 'basic'"""
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ def get_available_input_formats():
|
||||||
return frozenset()
|
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.
|
"""Convert source text from one format to another via pandoc.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -145,6 +145,8 @@ def convert(source, from_format="markdown", to_format="html5", title=None):
|
||||||
from_format: Pandoc input format name.
|
from_format: Pandoc input format name.
|
||||||
to_format: Pandoc output format name.
|
to_format: Pandoc output format name.
|
||||||
title: Optional document title (sets pandoc metadata).
|
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:
|
Returns:
|
||||||
bytes for binary formats (pdf, docx, etc.), str for text formats.
|
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
|
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:
|
if title:
|
||||||
cmd.extend(["--metadata", "title={}".format(title)])
|
cmd.extend(["--metadata", "title={}".format(title)])
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ def default_cleaner(tag_acl=None):
|
||||||
if tag_acl is None:
|
if tag_acl is None:
|
||||||
tag_acl = {}
|
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
|
tags = maybe_safe_tags + list(tag_acl.keys()) + markdown_tags
|
||||||
attrs = markdown_attrs
|
attrs = markdown_attrs
|
||||||
|
|
@ -224,4 +225,9 @@ def clean_raw_html(raw_html, cleaner=None):
|
||||||
# protect links from abuse.
|
# protect links from abuse.
|
||||||
soup = protect_links(soup, cleaner)
|
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")
|
return soup.decode(eventual_encoding="utf-8")
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,9 @@ class Node(RBase, Base):
|
||||||
def avatar_uri(self, **kwargs):
|
def avatar_uri(self, **kwargs):
|
||||||
"""Return invatar URI. Optionally return gravatar URI."""
|
"""Return invatar URI. Optionally return gravatar URI."""
|
||||||
u = self.user if self.user else self.user_surrogate
|
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:
|
if self.disabled:
|
||||||
kwargs["text"] = "-"
|
kwargs["text"] = "-"
|
||||||
kwargs["bg"] = "#aaaaaa"
|
kwargs["bg"] = "#aaaaaa"
|
||||||
|
|
@ -338,7 +341,7 @@ class Node(RBase, Base):
|
||||||
from remarkbox.lib.pandoc import convert
|
from remarkbox.lib.pandoc import convert
|
||||||
from remarkbox.lib.render import make_cleaner_from_namespace
|
from remarkbox.lib.render import make_cleaner_from_namespace
|
||||||
from remarkbox.lib.sanitize_html import default_cleaner, clean_raw_html
|
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:
|
if namespace:
|
||||||
cleaner = make_cleaner_from_namespace(namespace)
|
cleaner = make_cleaner_from_namespace(namespace)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
# shared routes
|
# shared routes
|
||||||
config.add_static_view("static", "static", cache_max_age=3600)
|
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("favicon", "/favicon.ico")
|
||||||
config.add_route("robots", "/robots.txt")
|
config.add_route("robots", "/robots.txt")
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
/* Variables - Light Mode (default) */
|
/* Variables - Chaostheory Dark (default) */
|
||||||
:root {
|
:root {
|
||||||
--color: #222222;
|
--color: #dddddd;
|
||||||
--color-muted: #3f3f3f;
|
--color-muted: #aaaaaa;
|
||||||
|
|
||||||
--color-code: #000000;
|
--color-code: #eeeeee;
|
||||||
|
|
||||||
--background: #ffffff;
|
--background: #1c4e63;
|
||||||
--background-faint: #fafafa;
|
--background-faint: #184254;
|
||||||
|
|
||||||
--primary: #d40000; /* Remarkbox logo color */
|
--primary: #6dcff6; /* Chaostheory ice blue */
|
||||||
--primary-hover: #e4241a;
|
--primary-hover: #9de3fb;
|
||||||
--primary-inverse: #ffffff;
|
--primary-inverse: #111111;
|
||||||
|
|
||||||
--border: #bbbbbb;
|
--border: #336173;
|
||||||
--border-faint: #e4e4e4;
|
--border-faint: #2a5468;
|
||||||
|
|
||||||
--color-mod: #aa0000; /* Consistency with Remarkbox style */
|
--color-mod: #6dcff6;
|
||||||
|
|
||||||
--line-height: 1.6;
|
--line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode Variables */
|
/* Deep Dark Mode */
|
||||||
:root.dark-mode {
|
:root.dark-mode {
|
||||||
--color: #e8e8e8;
|
--color: #dddddd;
|
||||||
--color-muted: #b0b0b0;
|
--color-muted: #999999;
|
||||||
|
|
||||||
--color-code: #e8e8e8;
|
--color-code: #eeeeee;
|
||||||
|
|
||||||
--background: #1a1a1a;
|
--background: #111111;
|
||||||
--background-faint: #2a2a2a;
|
--background-faint: #0a0a0a;
|
||||||
|
|
||||||
--primary: #ff5555; /* Brighter red for dark mode */
|
--primary: #6dcff6;
|
||||||
--primary-hover: #ff7777;
|
--primary-hover: #9de3fb;
|
||||||
--primary-inverse: #1a1a1a;
|
--primary-inverse: #111111;
|
||||||
|
|
||||||
--border: #444444;
|
--border: #1c4e63;
|
||||||
--border-faint: #333333;
|
--border-faint: #184254;
|
||||||
|
|
||||||
--color-mod: #ff5555;
|
--color-mod: #6dcff6;
|
||||||
|
|
||||||
--line-height: 1.6;
|
--line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +166,7 @@ blockquote {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
border-left: 3px solid #ccc;
|
border-left: 3px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.link,
|
button.link,
|
||||||
|
|
@ -271,15 +271,15 @@ form.node-action {
|
||||||
|
|
||||||
.focused {
|
.focused {
|
||||||
animation: fade 4s forwards;
|
animation: fade 4s forwards;
|
||||||
background-color: rgba(237, 237, 237, 1);
|
background-color: rgba(109, 207, 246, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
from {
|
from {
|
||||||
background-color: rgba(237, 237, 237, 1);
|
background-color: rgba(109, 207, 246, 0.15);
|
||||||
}
|
}
|
||||||
to {
|
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-danger,
|
||||||
.alert-error {
|
.alert-error {
|
||||||
color: #721c24;
|
color: #ffaaaa;
|
||||||
background-color: #f8d7da;
|
background-color: #4a1a1a;
|
||||||
border-color: #f5c6cb;
|
border-color: #7a2020;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
color: #856404;
|
color: #ffe08a;
|
||||||
background-color: #fff3cd;
|
background-color: #3a2e00;
|
||||||
border-color: #ffeeba;
|
border-color: #6a5200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info,
|
.alert-info,
|
||||||
.alert-notice {
|
.alert-notice {
|
||||||
color: #0c5460;
|
color: #6dcff6;
|
||||||
background-color: #d1ecf1;
|
background-color: #184254;
|
||||||
border-color: #bee5eb;
|
border-color: #336173;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
color: #155724;
|
color: #aaffbb;
|
||||||
background-color: #d4edda;
|
background-color: #0e3020;
|
||||||
border-color: #c3e6cb;
|
border-color: #1a5c30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ALERT END */
|
/* ALERT END */
|
||||||
|
|
@ -363,15 +363,15 @@ form.node-action {
|
||||||
border-radius: 1.2ex;
|
border-radius: 1.2ex;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
color: #cccccc;
|
color: var(--color-muted);
|
||||||
background: white;
|
background: var(--background-faint);
|
||||||
border: 1px solid #cccccc;
|
border: 1px solid var(--border);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-mark-circle:hover:before {
|
.question-mark-circle:hover:before {
|
||||||
color: white;
|
color: var(--primary-inverse);
|
||||||
background: #cccccc;
|
background: var(--primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +391,7 @@ form.node-action {
|
||||||
|
|
||||||
.load-more,
|
.load-more,
|
||||||
.load-more:visited {
|
.load-more:visited {
|
||||||
color: #336699;
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-right {
|
.button-right {
|
||||||
|
|
@ -495,7 +495,8 @@ form.node-action {
|
||||||
.my-namespaces-div {
|
.my-namespaces-div {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #ffffff;
|
background-color: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
|
@ -797,7 +798,7 @@ div.user-watching {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: block;
|
display: block;
|
||||||
background: #020304;
|
background: var(--primary-inverse);
|
||||||
transition: 0.5s;
|
transition: 0.5s;
|
||||||
}
|
}
|
||||||
.burger-label span:first-child {
|
.burger-label span:first-child {
|
||||||
|
|
@ -822,7 +823,7 @@ div.user-watching {
|
||||||
transform: rotate(-405deg);
|
transform: rotate(-405deg);
|
||||||
}
|
}
|
||||||
.burger-checkbox ~ .phone-menu {
|
.burger-checkbox ~ .phone-menu {
|
||||||
background: white;
|
background: var(--background-faint);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
@ -906,11 +907,11 @@ section.new-card,
|
||||||
section.payment-card,
|
section.payment-card,
|
||||||
section.join-or-log-in,
|
section.join-or-log-in,
|
||||||
section.well {
|
section.well {
|
||||||
background-color: white;
|
background-color: var(--background-faint);
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 1px 3px 0 #e6ebf1;
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
||||||
-webkit-transition: box-shadow 150ms ease;
|
-webkit-transition: box-shadow 150ms ease;
|
||||||
transition: box-shadow 150ms ease;
|
transition: box-shadow 150ms ease;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
{% macro node_avatar(node, size=request.avatar_size, align='left') %}
|
{% macro node_avatar(node, size=request.avatar_size, align='left') %}
|
||||||
|
{% set _uri = node.avatar_uri(size=size) %}
|
||||||
|
{% if _uri %}
|
||||||
<img
|
<img
|
||||||
src = "{{ node.avatar_uri(size=size) }}"
|
src = "{{ _uri }}"
|
||||||
class = "avatar nested-avatar"
|
class = "avatar nested-avatar"
|
||||||
align = "{{ align }}"
|
align = "{{ align }}"
|
||||||
/>
|
/>
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro user_avatar(user, size) %}
|
{% macro user_avatar(user, size) %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue