patx/projectpay
add dark logo env variable (optional)
Commit 4484faf · patx · 2026-07-06T17:13:15-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 070b9f0..132f805 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ ProjectPay is a small web app for creating project payment pages and collecting
MONGODB_DB=projectpay
APP_BASE_URL=http://127.0.0.1:8000
PUBLIC_LOGO_URL=
+ PUBLIC_DARK_LOGO_URL=
PUBLIC_LOGO_ALT=ProjectPay Logo
STRIPE_SECRET_KEY=
@@ -77,6 +78,7 @@ ProjectPay is a small web app for creating project payment pages and collecting
| `ADMIN_PASSWORD` | Required outside development | `admin` in development only | Password for the admin dashboard. Production startup fails if this is missing or set to `admin`. |
| `APP_BASE_URL` | Required outside development | Derived from request headers in development only | Public base URL used to generate customer payment links and Stripe return URLs. |
| `PUBLIC_LOGO_URL` | Optional | unset | Logo image URL shown on public project payment pages. No logo is shown when this is empty. |
+| `PUBLIC_DARK_LOGO_URL` | Optional | `PUBLIC_LOGO_URL` | Dark-mode logo image URL shown on public project payment pages. When empty, the normal logo is used. |
| `PUBLIC_LOGO_ALT` | Optional | `ProjectPay Logo` | Alt text for the public payment page logo. |
| `MONGODB_URI` | Optional | `mongodb://localhost:27017` | MongoDB connection string. |
| `MONGODB_DB` | Optional | `invoice_maker` | MongoDB database name. Use `projectpay` or another explicit name for new installs. |
@@ -164,7 +166,7 @@ On Heroku dynos, login rate limiting uses the final `X-Forwarded-For` value adde
## Customization
-- Set `PUBLIC_LOGO_URL` and `PUBLIC_LOGO_ALT` to brand the public payment page.
+- Set `PUBLIC_LOGO_URL`, `PUBLIC_DARK_LOGO_URL`, and `PUBLIC_LOGO_ALT` to brand the public payment page.
- Update styles in `templates/base.html`.
- The current default database name is `invoice_maker`; set `MONGODB_DB=projectpay` for a fresh ProjectPay deployment.
- Currency formatting is optimized for USD. Non-USD currencies display as `CURRENCY amount`.
diff --git a/app.py b/app.py
index d3141fe..3df2211 100644
--- a/app.py
+++ b/app.py
@@ -201,6 +201,9 @@ class InvoiceApp(App):
)
self._validate_config()
self.public_logo_url = os.getenv("PUBLIC_LOGO_URL", "").strip()
+ self.public_dark_logo_url = (
+ os.getenv("PUBLIC_DARK_LOGO_URL", "").strip() or self.public_logo_url
+ )
self.public_logo_alt = os.getenv("PUBLIC_LOGO_ALT", "ProjectPay Logo").strip()
self.currency = os.getenv("STRIPE_CURRENCY", "usd").upper()
self.stripe_secret_key = os.getenv("STRIPE_SECRET_KEY", "")
@@ -904,6 +907,7 @@ class InvoiceApp(App):
kwargs.setdefault("csrf_token", self._csrf_token())
kwargs.setdefault("currency", self.currency)
kwargs.setdefault("public_logo_url", self.public_logo_url)
+ kwargs.setdefault("public_dark_logo_url", self.public_dark_logo_url)
kwargs.setdefault("public_logo_alt", self.public_logo_alt)
return await self._render_template(template, **kwargs)
diff --git a/templates/public_project.html b/templates/public_project.html
index dfb941e..e5d304e 100644
--- a/templates/public_project.html
+++ b/templates/public_project.html
@@ -4,7 +4,12 @@
{% block content %}
{% if public_logo_url %}
<div class="public-logo">
- <img src="{{ public_logo_url }}" alt="{{ public_logo_alt }}">
+ <picture>
+ {% if public_dark_logo_url and public_dark_logo_url != public_logo_url %}
+ <source srcset="{{ public_dark_logo_url }}" media="(prefers-color-scheme: dark)">
+ {% endif %}
+ <img src="{{ public_logo_url }}" alt="{{ public_logo_alt }}">
+ </picture>
</div>
{% endif %}
<div class="header-row">
diff --git a/tests/test_security_hardening.py b/tests/test_security_hardening.py
index 9bfac84..5a1cbd7 100644
--- a/tests/test_security_hardening.py
+++ b/tests/test_security_hardening.py
@@ -145,6 +145,21 @@ class SecurityHardeningTests(unittest.TestCase):
def test_direct_run_host_defaults_to_loopback(self):
self.assertEqual(invoice_app.DEFAULT_HOST, "127.0.0.1")
+ def test_public_dark_logo_url_defaults_to_public_logo_url(self):
+ app = self.make_app(PUBLIC_LOGO_URL="https://example.com/logo.svg")
+
+ self.assertEqual(app.public_dark_logo_url, "https://example.com/logo.svg")
+
+ def test_public_dark_logo_url_can_be_configured(self):
+ app = self.make_app(
+ PUBLIC_LOGO_URL="https://example.com/logo.svg",
+ PUBLIC_DARK_LOGO_URL="https://example.com/logo-dark.svg",
+ )
+
+ self.assertEqual(
+ app.public_dark_logo_url, "https://example.com/logo-dark.svg"
+ )
+
def test_development_base_url_uses_request_headers_only_in_development(self):
app = self.make_app(APP_BASE_URL="")
request = SimpleNamespace(