Documentation editorial: Material theme hardening, metadata, and content polish

- Enable navigation.instant, prefetch, progress, content.code.select, content.tabs.link
- Fix edit_uri (main→v2), copyright year (2024→2024-2026), consent banner config
- Add abbreviations glossary (47 acronyms with hover tooltips via snippets auto-append)
- Add tags to all 72 doc pages with consistent taxonomy (audience/module/type)
- Add status:new badges to 16 recent feature pages, search:boost to 7 entry pages
- Rewrite Architecture page with 5 Mermaid diagrams and full component documentation
- Rewrite Troubleshooting page from 5 to 13 sections with actionable checklists
- Fix broken links (Monitoring/Contributing pointed to blog placeholder)
- Expand Admin Guide roles table from 5 to 11 roles
- Create custom 404 page, blog with authors and inaugural v2 announcement post
- Fresh Playwright screenshots for login, dashboard, campaigns, users, settings, locations, shifts
- Remove 5 test/dev files and orphan template override
- Add planning document (DOCS_NEXT_STEPS.md) for future editorial reference

Bunker Admin
This commit is contained in:
bunker-admin 2026-03-23 12:36:10 -06:00
parent bb1935027d
commit 68ba45a689
94 changed files with 1204 additions and 547 deletions

305
mkdocs/DOCS_NEXT_STEPS.md Normal file
View File

@ -0,0 +1,305 @@
# Documentation Next Steps — Editorial & Material Theme Enhancement Plan
**Date:** 2026-03-22
**Branch:** v2
**Status:** Planning → Execution
---
## Executive Summary
The MkDocs documentation site is comprehensive (70+ pages, all with proper frontmatter, no stubs) and already uses many Material theme features well (grid cards, admonitions, code copy, mermaid diagrams, social cards, blog plugin, dark/light toggle). This plan focuses on **activating dormant Material theme capabilities** and **editorial polish** to take the docs from "complete" to "professional-grade."
---
## Step 1: mkdocs.yml Configuration Hardening
**Goal:** Enable Material theme features that are available but not configured.
### 1a. Add missing theme features
```yaml
features:
# Already enabled (keep):
- announce.dismiss
- content.action.edit
- content.action.view
- content.code.annotate
- content.code.copy
- content.tooltips
- navigation.footer
- navigation.indexes
- navigation.path
- navigation.prune
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- navigation.tracking
- search.highlight
- search.share
- search.suggest
- toc.follow
# ADD these:
- navigation.instant # SPA-like navigation (no full page reload)
- navigation.instant.prefetch # Prefetch pages on hover
- navigation.instant.progress # Show loading progress bar
- content.code.select # Line selection in code blocks
- content.tabs.link # Linked content tabs (sync across page)
```
### 1b. Fix consent banner
The copyright references `#__consent` but no consent config exists. Add:
```yaml
extra:
consent:
title: Cookie consent
description: >
We use cookies to recognize your repeated visits and preferences,
as well as to measure the effectiveness of our documentation.
With your consent, you help us improve.
actions:
- accept
- reject
- manage
```
### 1c. Fix copyright year
Change `2024``20242026` in the copyright line.
### 1d. Fix edit_uri branch
Change `edit_uri: src/branch/main/mkdocs/docs``edit_uri: src/branch/v2/mkdocs/docs`
### 1e. Add abbreviations snippets
Create `docs/includes/abbreviations.md` and reference via snippets:
```yaml
markdown_extensions:
- pymdownx.snippets:
auto_append:
- includes/abbreviations.md
```
Common abbreviations: API, JWT, RBAC, CSV, ORM, SMTP, CORS, SSL, TLS, DNS, CRUD, SSO, SPA, CLI, GUI, QR, GPS, GDPR, CDN, VPS, CGNAT, NAR, CRM, OG, DDoS, SSE, UUID, FOSS, HSTS, CSP
### 1f. Add privacy plugin (optional, external resource proxying)
```yaml
plugins:
- privacy
```
---
## Step 2: Page-Level Metadata Enhancement
**Goal:** Add `tags`, `status`, and `search.boost` metadata to every page.
### 2a. Tags
The tags plugin is loaded but zero pages use tags. Add contextually appropriate tags to every page. Example tag taxonomy:
- **Audience:** `admin`, `volunteer`, `user`, `operator`, `developer`
- **Module:** `influence`, `map`, `media`, `payments`, `broadcast`, `social`
- **Type:** `guide`, `reference`, `tutorial`, `concept`, `troubleshooting`
- **Feature:** `campaigns`, `canvassing`, `shifts`, `gallery`, `landing-pages`, `newsletter`, `sms`, `chat`, `events`
### 2b. Status badges
Material supports `status: new` and `status: deprecated` on pages, shown as badges in the nav sidebar. Apply:
- `status: new` — Recent features (Gallery Ads, People CRM, Achievements, Social Calendar, CrowdSec, SMS, Docs Comments, Payments)
- `status: deprecated` — Legacy/archival content if any
Requires adding to `extra:` in mkdocs.yml:
```yaml
extra:
status:
new: Recently added
deprecated: Legacy
```
### 2c. Search boost
Boost important entry-point pages so they rank higher:
```yaml
---
search:
boost: 2
---
```
Apply to: Getting Started index, Installation, First Steps, Features at a Glance, FAQ/Troubleshooting.
---
## Step 3: Abbreviations Glossary
**Goal:** Create a shared abbreviations file so that hovering over acronyms shows their meaning.
Create `mkdocs/docs/includes/abbreviations.md`:
```markdown
*[API]: Application Programming Interface
*[JWT]: JSON Web Token
*[RBAC]: Role-Based Access Control
*[CORS]: Cross-Origin Resource Sharing
*[SMTP]: Simple Mail Transfer Protocol
*[CSV]: Comma-Separated Values
*[ORM]: Object-Relational Mapping
*[SSL]: Secure Sockets Layer
*[TLS]: Transport Layer Security
*[DNS]: Domain Name System
*[CRUD]: Create, Read, Update, Delete
*[SSO]: Single Sign-On
*[SPA]: Single Page Application
*[CLI]: Command Line Interface
*[GUI]: Graphical User Interface
*[QR]: Quick Response (code)
*[GPS]: Global Positioning System
*[GDPR]: General Data Protection Regulation
*[CDN]: Content Delivery Network
*[VPS]: Virtual Private Server
*[CGNAT]: Carrier-Grade Network Address Translation
*[NAR]: National Address Register
*[CRM]: Customer Relationship Management
*[OG]: Open Graph
*[DDoS]: Distributed Denial of Service
*[SSE]: Server-Sent Events
*[UUID]: Universally Unique Identifier
*[FOSS]: Free and Open Source Software
*[HSTS]: HTTP Strict Transport Security
*[CSP]: Content Security Policy
*[BullMQ]: Bull Message Queue
*[FFprobe]: FFmpeg Probe (media metadata tool)
*[FFmpeg]: Fast Forward Moving Picture Experts Group
*[XMPP]: Extensible Messaging and Presence Protocol
```
---
## Step 4: Custom 404 Page
**Goal:** Branded 404 page instead of browser default.
Create `mkdocs/docs/404.md`:
```markdown
---
template: main.html
title: Page Not Found
hide:
- navigation
- toc
- footer
search:
exclude: true
---
# Page Not Found
The page you're looking for doesn't exist or has been moved.
[Go to Documentation Home](docs/index.md){ .md-button .md-button--primary }
[Search](docs/index.md){ .md-button }
```
---
## Step 5: Content Fixes — Broken Links & Stale Warnings
**Goal:** Fix all broken links, placeholder content, and stale warnings.
### 5a. Fix broken "Coming soon" links
- `docs/index.md`: Monitoring card links to `../blog/index.md` → link to `admin/services/monitoring.md`
- `docs/index.md`: Contributing card links to `../blog/index.md` → create a contributing stub or remove placeholder
### 5b. Fix Architecture page
Remove "Under Construction" admonition. Flesh out with:
- Mermaid system diagram
- Database entity relationship summary
- Authentication flow (already started)
- Request lifecycle
### 5c. Fix Troubleshooting page
Remove "Under Construction" admonition. Add more entries from CLAUDE.md and production experience.
### 5d. Fix Admin Guide roles table
Only lists 5 roles but there are 11. Add missing roles:
BROADCAST_ADMIN, CONTENT_ADMIN, MEDIA_ADMIN, PAYMENTS_ADMIN, EVENTS_ADMIN, SOCIAL_ADMIN
### 5e. Social icons semantic fix
GitHub icon links to Gitea — change icon to `fontawesome/brands/gitea` or `simple/gitea` if available, or use a generic `fontawesome/solid/code-branch`.
---
## Step 6: Cleanup Test/Orphan Files
**Goal:** Remove files that shouldn't be in the docs directory.
- `docs/test.md` (41KB test file)
- `docs/test-page.md`
- `docs/testing.md`
- `docs/lander.md` (override template pointer — keep if used by index.md)
- `docs/main.md` (override template pointer — keep if used)
Verify `lander.md` and `main.md` are still needed by checking if any page uses `template: lander.html` or `template: main.html`.
---
## Step 7: Announcement Bar
**Goal:** Use Material's announcement bar for version/status info.
Create `mkdocs/docs/overrides/main.html` addition (or modify existing) with announcement block:
```html
{% block announce %}
Changemaker Lite v2 — <a href="docs/getting-started/index.md">Get started in 5 minutes</a>
{% endblock %}
```
Note: The existing `main.html` override is 35KB — check if it already has an announce block before modifying.
---
## Step 8: Blog Seeding
**Goal:** Create 12 initial blog posts so the blog section isn't empty.
Suggested posts:
1. **"Introducing Changemaker Lite v2"** — Overview of the rebuild, what's new, philosophy
2. **"Why Self-Hosted Campaign Tools Matter in 2026"** — Draws from the Philosophy page
---
## Step 9: Screenshot Audit
**Goal:** Verify all `![...](../../assets/images/screenshots/...)` references resolve to actual files. Take new screenshots with Playwright where missing or outdated.
Pages referencing screenshots:
- Getting Started index (dashboard.png)
- First Steps (login.png, dashboard.png, settings-organization.png, campaigns.png, locations.png, shifts.png)
- Dashboard (dashboard.png)
- People & Access (users.png)
- Settings (settings.png)
- Campaigns (campaigns.png)
---
## Priority Order
| Priority | Step | Impact | Effort |
|----------|------|--------|--------|
| **P0** | Step 1 (mkdocs.yml fixes) | High — broken consent, wrong branch, missing SPA nav | Low |
| **P0** | Step 5 (broken links/content) | High — broken UX | Medium |
| **P0** | Step 6 (cleanup test files) | Medium — professional appearance | Low |
| **P1** | Step 3 (abbreviations) | Medium — reader comprehension | Low |
| **P1** | Step 4 (404 page) | Medium — UX polish | Low |
| **P1** | Step 2 (tags/status/boost) | Medium — discoverability | Medium-High |
| **P2** | Step 7 (announcement bar) | Low-Medium — branding | Low |
| **P2** | Step 8 (blog seeding) | Low — completeness | Medium |
| **P2** | Step 9 (screenshot audit) | Medium — visual credibility | High |
---
## Material Theme Reference
Key Material docs for implementers:
- [Setting up tags](https://squidfunk.github.io/mkdocs-material/setup/setting-up-tags/)
- [Setting up navigation](https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/)
- [Page status](https://squidfunk.github.io/mkdocs-material/reference/index/#setting-the-page-status)
- [Search boosting](https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/#search-boosting)
- [Abbreviations](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations)
- [Cookie consent](https://squidfunk.github.io/mkdocs-material/setup/ensuring-data-privacy/#cookie-consent)
- [Custom 404](https://www.mkdocs.org/user-guide/custom-themes/#custom-theme)
- [Announcement bar](https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/#announcement-bar)

25
mkdocs/docs/404.md Normal file
View File

@ -0,0 +1,25 @@
---
template: main.html
title: Page Not Found
hide:
- navigation
- toc
- footer
search:
exclude: true
---
<div style="text-align: center; padding: 4rem 1rem;" markdown>
# :material-map-marker-question: Page Not Found
The page you're looking for doesn't exist or has been moved.
[Back to Documentation :material-arrow-left:](docs/index.md){ .md-button .md-button--primary }
[Search the Docs :material-magnify:](docs/index.md){ .md-button }
---
*If you followed a link here, please [report the broken link](https://gitea.bnkops.com/admin/changemaker.lite/issues/new){ target="_blank" } so we can fix it.*
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,5 @@
authors:
admin:
name: Bunker Operations
description: Changemaker Lite core team
avatar: https://gitea.bnkops.com/avatars/1

View File

@ -0,0 +1,71 @@
---
date: 2026-03-22
authors:
- admin
categories:
- Announcements
- Platform
tags:
- v2
- release
- self-hosted
- FOSS
---
# Introducing Changemaker Lite v2
Changemaker Lite v2 is a ground-up rebuild of the platform — same mission, entirely new architecture. After 14 phases of development, the platform is ready for production use.
<!-- more -->
## What Changed
V1 was two independent Express apps stitched together with NocoDB as a data layer. It worked, but scaling features meant fighting the architecture at every turn.
V2 is a unified TypeScript stack:
- **Dual API architecture** — Express.js for the main platform, Fastify for the media library, sharing a single PostgreSQL 16 database via Prisma ORM
- **React admin GUI** — Vite + Ant Design + Zustand, serving admin, public, and volunteer interfaces from one build
- **30+ Docker services** — from core infrastructure to monitoring, communication, and developer tools
- **JWT authentication** with refresh token rotation, role-based access control (11 roles), and a comprehensive security audit
## What's New
The feature set has grown substantially:
- **Advocacy campaigns** with postal code → representative lookup, email sending, response walls, and moderation
- **Map & canvassing** with multi-provider geocoding, polygon territories, GPS-tracked volunteer sessions, and walking route generation
- **Media manager** with video upload, FFprobe metadata extraction, scheduled publishing, analytics, and a public gallery
- **Landing page builder** powered by GrapesJS with drag-and-drop editing
- **Payments** via encrypted Stripe integration — products, donations, and subscription plans
- **SMS campaigns** via a Termux Android bridge
- **Team communication** with self-hosted Rocket.Chat and Jitsi Meet
- **People CRM** aggregating contacts across all modules with duplicate detection and merge
- **Volunteer social features** — friend system, achievements, leaderboards, and a personal calendar
- **One-command install**`curl | bash` pulls a release tarball and runs the config wizard
## Why Self-Hosted
Every subscription to corporate campaign software funds infrastructure you don't control. Your voter lists, canvassing outcomes, and communication patterns become assets on someone else's balance sheet.
Changemaker Lite costs roughly the price of a VPS — often under $50/month for the full stack. But the real value isn't cost savings. It's **control.** No vendor can cut off your access. No acquisition can change your terms.
Read more in our [Philosophy](../../docs/phil.md) page.
## Get Started
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
```
Or follow the [Getting Started guide](../../docs/getting-started/index.md) for a walkthrough.
## What's Next
Phase 15 (Testing & Polish) is underway. We're also working on:
- Social Calendar Phase B (shared views, availability finder)
- Expanded test coverage
- Performance optimization for large location datasets
Follow this blog for updates, or subscribe to the [newsletter](https://listmonk.bnkops.com/subscription/form).

View File

@ -2,6 +2,11 @@
title: Advocacy Campaigns
description: Help supporters contact elected representatives through email campaigns with postal code lookup and response tracking.
icon: material/email-fast
tags:
- guide
- admin
- influence
- campaigns
---
# Advocacy Campaigns

View File

@ -2,6 +2,11 @@
title: Email Queue
description: Monitor and manage the BullMQ advocacy email delivery queue.
icon: material/email-sync
tags:
- guide
- admin
- influence
- email
---
# Email Queue

View File

@ -2,6 +2,10 @@
title: Advocacy
description: Manage email campaigns, moderate responses, and monitor email delivery.
icon: material/email-fast
tags:
- guide
- admin
- influence
---
# Advocacy

View File

@ -2,6 +2,10 @@
title: Representatives
description: Represent API integration for postal code to representative lookup with caching.
icon: material/account-tie
tags:
- guide
- admin
- influence
---
# Representatives

View File

@ -2,6 +2,11 @@
title: Response Moderation
description: Moderate the public response wall where supporters share how representatives replied.
icon: material/message-reply-text
tags:
- guide
- admin
- influence
- moderation
---
# Response Moderation

View File

@ -2,6 +2,11 @@
title: Email Templates
description: Create reusable email templates with variable substitution for campaign communications.
icon: material/email-edit
tags:
- guide
- admin
- broadcast
- email
---
# Email Templates

View File

@ -2,6 +2,10 @@
title: Broadcast
description: Newsletter sync, email templates, and SMS campaigns for reaching supporters.
icon: material/bullhorn
tags:
- guide
- admin
- broadcast
---
# Broadcast

View File

@ -2,6 +2,11 @@
title: Newsletter (Listmonk)
description: Integrated opt-in mailing lists and newsletter campaigns powered by Listmonk.
icon: material/newspaper-variant
tags:
- guide
- admin
- broadcast
- email
---
# Newsletter (Listmonk)

View File

@ -2,6 +2,12 @@
title: SMS Campaigns
description: Complete guide to setting up the Termux Android SMS bridge for text message outreach, contact management, and response tracking.
icon: material/message-text
status: new
tags:
- guide
- admin
- broadcast
- sms
---
# SMS Campaigns

View File

@ -2,6 +2,9 @@
title: Dashboard
description: Admin dashboard with live stats, activity feed, upcoming shifts, and service health indicators.
icon: material/view-dashboard
tags:
- guide
- admin
---
# Dashboard

View File

@ -2,6 +2,9 @@
title: Admin Guide
description: Day-to-day administration of users, campaigns, content, maps, media, and platform services.
icon: material/shield-account
tags:
- guide
- admin
---
# Admin Guide
@ -82,8 +85,14 @@ The admin panel at `/app` is your command center for managing the entire platfor
| Role | Access Level |
|------|-------------|
| `SUPER_ADMIN` | Full platform access |
| `INFLUENCE_ADMIN` | Campaigns, responses, email queue |
| `MAP_ADMIN` | Locations, areas, shifts, canvassing |
| `SUPER_ADMIN` | Full platform access — implicitly bypasses all role checks |
| `INFLUENCE_ADMIN` | Campaigns, responses, representatives, email queue |
| `MAP_ADMIN` | Locations, areas, shifts, canvassing, data quality |
| `BROADCAST_ADMIN` | Newsletter sync, email templates |
| `CONTENT_ADMIN` | Landing pages, homepage, navigation, documentation |
| `MEDIA_ADMIN` | Video library, analytics, gallery, moderation, ads |
| `PAYMENTS_ADMIN` | Products, donations, plans, Stripe configuration |
| `EVENTS_ADMIN` | Gancio event sync and calendar management |
| `SOCIAL_ADMIN` | Social connections, achievements, calendar layers |
| `USER` | Volunteer portal only |
| `TEMP` | Limited volunteer access (auto-created) |
| `TEMP` | Limited volunteer access (auto-created on shift signup) |

View File

@ -2,6 +2,11 @@
title: Areas (Cuts)
description: Draw polygon canvassing territories on the map and organize locations into manageable regions.
icon: material/vector-polygon
tags:
- guide
- admin
- map
- canvassing
---
# Areas (Cuts)

View File

@ -2,6 +2,11 @@
title: Canvassing
description: Canvass dashboard, walk sheets, session management, and contact export for door-to-door outreach.
icon: material/walk
tags:
- guide
- admin
- map
- canvassing
---
# Canvassing

View File

@ -2,6 +2,11 @@
title: Data Quality
description: Geocoding quality metrics, provider distribution, and confidence analysis.
icon: material/chart-box
tags:
- guide
- admin
- map
- analytics
---
# Data Quality

View File

@ -2,6 +2,10 @@
title: Map & Canvassing
description: Location management, canvassing territories, volunteer shifts, and door-to-door outreach coordination.
icon: material/map-marker-multiple
tags:
- guide
- admin
- map
---
# Map & Canvassing

View File

@ -2,6 +2,11 @@
title: Locations
description: Import, geocode, and manage addresses for canvassing and public map display.
icon: material/map-marker-plus
tags:
- guide
- admin
- map
- locations
---
# Locations

View File

@ -2,6 +2,11 @@
title: Map Settings
description: Configure map center coordinates, default zoom level, and QR code links for walk sheets.
icon: material/cog
tags:
- guide
- admin
- map
- configuration
---
# Map Settings

View File

@ -2,6 +2,11 @@
title: Shifts
description: Schedule volunteer time slots with recurring patterns, calendar views, and area assignments.
icon: material/calendar-clock
tags:
- guide
- admin
- map
- shifts
---
# Shifts

View File

@ -2,6 +2,11 @@
title: Gallery Ads
description: Promotional cards with audience targeting, scheduling, and click-through analytics.
icon: material/advertisements
status: new
tags:
- guide
- admin
- media
---
# Gallery Ads

View File

@ -2,6 +2,11 @@
title: Analytics
description: Video engagement metrics including views, watch time, completion rates, and traffic sources.
icon: material/chart-line
tags:
- guide
- admin
- media
- analytics
---
# Analytics

View File

@ -2,6 +2,11 @@
title: Curated Gallery
description: Manage playlists, the shorts feed, and featured content for the public video gallery.
icon: material/playlist-play
tags:
- guide
- admin
- media
- gallery
---
# Curated Gallery

View File

@ -2,6 +2,10 @@
title: Media
description: Video and photo library, analytics, playlists, comment moderation, and gallery ad management.
icon: material/play-box-multiple
tags:
- guide
- admin
- media
---
# Media

View File

@ -2,6 +2,11 @@
title: Library
description: Upload and manage videos and photos with metadata extraction, scheduled publishing, and preview links.
icon: material/folder-play
tags:
- guide
- admin
- media
- videos
---
# Library

View File

@ -2,6 +2,11 @@
title: Moderation
description: Review and manage comments across media content with word filters and moderation tools.
icon: material/shield-check
tags:
- guide
- admin
- media
- moderation
---
# Moderation

View File

@ -2,6 +2,11 @@
title: Donations
description: Create branded donation pages with fundraising goals, suggested amounts, and tracking.
icon: material/hand-heart
status: new
tags:
- guide
- admin
- payments
---
# Donations

View File

@ -2,6 +2,11 @@
title: Payments
description: Stripe-powered products, donations, subscription plans, and payment configuration.
icon: material/credit-card
status: new
tags:
- guide
- admin
- payments
---
# Payments

View File

@ -2,6 +2,11 @@
title: Plans
description: Recurring subscription plans with monthly and yearly billing via Stripe.
icon: material/card-account-details-star
status: new
tags:
- guide
- admin
- payments
---
# Plans

View File

@ -2,6 +2,11 @@
title: Products
description: Manage merchandise and one-time purchase items with inventory and Stripe checkout.
icon: material/shopping
status: new
tags:
- guide
- admin
- payments
---
# Products

View File

@ -2,6 +2,12 @@
title: Payment Settings
description: Configure Stripe API keys with encrypted storage for secure payment processing.
icon: material/credit-card-settings
status: new
tags:
- guide
- admin
- payments
- configuration
---
# Payment Settings

View File

@ -2,6 +2,11 @@
title: People & Access
description: Manage users, roles, and the unified People CRM.
icon: material/account-group
status: new
tags:
- guide
- admin
- CRM
---
# People & Access

View File

@ -2,6 +2,12 @@
title: CrowdSec & Security
description: CrowdSec Manager web UI, Tinyauth forward-auth, CrowdSec tuning, and Cloudflare Turnstile captcha on the Pangolin server.
icon: material/shield-lock
status: new
tags:
- guide
- admin
- services
- security
---
# CrowdSec Manager & Security Configuration

View File

@ -2,6 +2,10 @@
title: Services
description: Tunnel management, monitoring, and third-party service integrations.
icon: material/server-network
tags:
- guide
- admin
- services
---
# Services

View File

@ -2,6 +2,11 @@
title: Integrations
description: Chat, video conferencing, password manager, whiteboard, Git hosting, automation, and QR services.
icon: material/puzzle
tags:
- guide
- admin
- services
- integrations
---
# Integrations

View File

@ -2,6 +2,11 @@
title: Monitoring
description: Prometheus metrics, Grafana dashboards, and Alertmanager for platform observability.
icon: material/chart-timeline-variant
tags:
- guide
- admin
- services
- monitoring
---
# Monitoring

View File

@ -2,6 +2,11 @@
title: Tunnel (Pangolin)
description: Manage the Pangolin tunnel for exposing services to the internet without port forwarding.
icon: material/tunnel
tags:
- guide
- admin
- services
- networking
---
# Tunnel (Pangolin)

View File

@ -2,6 +2,11 @@
title: User Provisioning
description: Automatic account creation and sync across integrated services.
icon: material/account-sync
status: new
tags:
- guide
- admin
- services
---
# User Provisioning

View File

@ -2,6 +2,10 @@
title: Platform Settings
description: Multi-tab admin settings for organization branding, theme colors, email configuration, feature toggles, and notification controls.
icon: material/cog
tags:
- guide
- admin
- configuration
---
# Platform Settings

View File

@ -2,6 +2,10 @@
title: Documentation
description: MkDocs site management, page analytics, comment moderation, and documentation settings.
icon: material/book-open-variant
tags:
- guide
- admin
- content
---
# Documentation

View File

@ -2,6 +2,10 @@
title: Public Homepage
description: Dynamic landing page that aggregates campaigns, shifts, media, events, and platform stats for public visitors.
icon: material/home-variant
tags:
- guide
- admin
- content
---
# Public Homepage

View File

@ -2,6 +2,10 @@
title: Web Content
description: Landing pages, homepage configuration, navigation, and documentation management.
icon: material/web
tags:
- guide
- admin
- content
---
# Web Content

View File

@ -2,6 +2,11 @@
title: Landing Pages
description: Build campaign microsites with a drag-and-drop GrapesJS visual editor.
icon: material/application-edit
tags:
- guide
- admin
- content
- landing-pages
---
# Landing Pages

View File

@ -2,6 +2,10 @@
title: Navigation Settings
description: Customize the public-facing navigation menu from the admin panel.
icon: material/menu
tags:
- guide
- admin
- content
---
# Navigation Settings

View File

@ -2,6 +2,10 @@
title: API Reference
description: Complete REST API reference for both the Express API (port 4000) and Fastify Media API (port 4100).
icon: material/api
tags:
- reference
- developer
- API
---
# API Reference

View File

@ -2,38 +2,213 @@
title: Architecture
description: System architecture, dual API design, database schema, and authentication flow.
icon: material/sitemap
tags:
- reference
- developer
- architecture
---
# Architecture
Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database.
Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database, a React single-page application, and Nginx for subdomain routing across 30+ services.
!!! warning "Under Construction"
Detailed architecture documentation is being written. Check back soon.
---
## System Overview
## System Diagram
```mermaid
graph LR
Browser["Browser"] --> Nginx["Nginx<br/>(reverse proxy)"]
Nginx --> Admin["React Admin GUI<br/>port 3000"]
Nginx --> API["Express API<br/>port 4000"]
Nginx --> MediaAPI["Fastify Media API<br/>port 4100"]
Nginx --> MkDocs["MkDocs<br/>port 4003/4004"]
Nginx --> Services["Other Services<br/>(Gitea, NocoDB, etc.)"]
API --> PostgreSQL[("PostgreSQL 16<br/>30+ tables")]
MediaAPI --> PostgreSQL
API --> Redis[("Redis<br/>cache + queues")]
API --> BullMQ["BullMQ<br/>(email, video jobs)"]
BullMQ --> Redis
subgraph Tunnel ["Public Access"]
Newt["Newt Client"] --> Pangolin["Pangolin Server"]
end
Newt --> Nginx
```
Browser ──► Nginx (reverse proxy) ──┬──► Express API (port 4000) ──► PostgreSQL
├──► Fastify Media API (port 4100) ──┘
├──► React Admin GUI (port 3000)
└──► MkDocs / Other Services
```
---
## Key Components
| Component | Technology | Role |
|-----------|-----------|------|
| Main API | Express.js + Prisma | Auth, campaigns, map, shifts, pages |
| Media API | Fastify + Prisma | Video library, analytics, uploads |
| Admin GUI | React + Ant Design | Single-page admin application |
| Database | PostgreSQL 16 | Shared by both APIs (30+ tables) |
| Cache | Redis | Rate limiting, job queues, geocoding |
| Proxy | Nginx | Subdomain routing, security headers |
| **Main API** | Express.js + TypeScript + Prisma | Auth, campaigns, map, shifts, pages, canvassing, email |
| **Media API** | Fastify + TypeScript + Prisma | Video library, analytics, uploads, scheduling |
| **Admin GUI** | React 19 + Vite + Ant Design + Zustand | Admin dashboard, public pages, volunteer portal, media gallery |
| **Database** | PostgreSQL 16 | Shared by both APIs (30+ models via Prisma) |
| **Cache** | Redis 7 | Rate limiting, BullMQ job queues, geocoding cache |
| **Proxy** | Nginx | Subdomain routing, security headers, WebSocket upgrade |
| **Tunnel** | Pangolin + Newt | Expose services without port forwarding |
| **Monitoring** | Prometheus + Grafana + Alertmanager | Metrics collection, dashboards, alerting |
---
## Dual API Design
The platform runs two independent API servers sharing one PostgreSQL database:
=== "Express API (port 4000)"
The main API handles all core platform logic:
- **Authentication** — JWT access/refresh tokens, RBAC middleware
- **Modules** — Influence (campaigns, responses), Map (locations, cuts, shifts, canvassing), Pages, Email Templates, Settings, Users, Payments, Social, Calendar
- **Services** — Email queue (BullMQ), geocoding queue, Listmonk sync, Pangolin client, user provisioning
- **ORM** — Prisma with 30+ models and migration history
=== "Fastify Media API (port 4100)"
A separate server optimized for media handling:
- **Video CRUD** — Upload with FFprobe metadata extraction
- **Scheduled Publishing** — BullMQ queue with timezone support
- **Analytics** — View tracking, watch time, completion rates (GDPR-compliant)
- **Public Gallery** — Playlists, reactions, comments, SSE chat
- **ORM** — Prisma (migrated from Drizzle, Feb 2026)
Both servers connect to the same database and share the same Prisma schema. This separation allows the media API to handle large file uploads and streaming independently from the main API's request/response cycle.
---
## Authentication Flow
- JWT access tokens (15 min) + refresh tokens (7 days)
- Refresh token rotation with atomic database transaction
- Role-based access control (5 roles)
- Rate limiting on auth endpoints (10/min per IP)
```mermaid
sequenceDiagram
participant Client
participant API
participant DB
participant Redis
Client->>API: POST /api/auth/login {email, password}
API->>Redis: Check rate limit (10/min per IP)
Redis-->>API: OK
API->>DB: Verify bcrypt password
DB-->>API: User record
API->>DB: Create refresh token
API-->>Client: {accessToken (15min), refreshToken (7d)}
Note over Client: Authenticated requests
Client->>API: GET /api/campaigns<br/>Authorization: Bearer <accessToken>
API->>API: Verify JWT + check role (RBAC)
API-->>Client: 200 OK
Note over Client: Token expired
Client->>API: POST /api/auth/refresh {refreshToken}
API->>DB: Atomic rotation (delete old, create new)
API-->>Client: {new accessToken, new refreshToken}
```
### Security Features
- **Password policy** — 12+ characters, uppercase, lowercase, digit (schema-enforced)
- **Refresh token rotation** — Atomic Prisma transaction prevents race conditions
- **User enumeration prevention** — Returns 401 (not 404) for missing users
- **Rate limiting** — 10 requests/minute on auth endpoints via Redis
- **11 roles**`SUPER_ADMIN` (implicit bypass), 8 module-specific admin roles, `USER`, `TEMP`
- **Encryption** — AES-256-GCM for sensitive DB fields (`ENCRYPTION_KEY` env var)
---
## Request Lifecycle
```mermaid
graph TD
A["Incoming Request"] --> B["Nginx"]
B -->|"Host: api.domain"| C["Express API"]
B -->|"Host: media.domain"| D["Fastify Media API"]
B -->|"Host: app.domain"| E["React Admin GUI"]
C --> F["Rate Limiter (Redis)"]
F --> G["Auth Middleware (JWT)"]
G --> H["Role Check (RBAC)"]
H --> I["Validation (Zod)"]
I --> J["Route Handler"]
J --> K["Service Layer"]
K --> L["Prisma ORM"]
L --> M[("PostgreSQL")]
J --> N["Response + Metrics"]
```
---
## Database Schema
The database contains **30+ Prisma models** organized by module:
| Module | Key Models |
|--------|-----------|
| **Auth** | `User`, `RefreshToken` |
| **Influence** | `Campaign`, `CampaignEmail`, `CampaignResponse`, `Representative`, `PostalCode` |
| **Map** | `Location`, `Address`, `Cut`, `Shift`, `ShiftSignup` |
| **Canvass** | `CanvassSession`, `CanvassVisit`, `TrackingSession`, `TrackingPoint` |
| **Pages** | `Page`, `PageBlock`, `EmailTemplate` |
| **Media** | `Video`, `VideoReaction`, `VideoComment`, `VideoView`, `Playlist`, `PlaylistVideo` |
| **Payments** | `StripeProduct`, `StripePrice`, `StripeDonationPage`, `StripeOrder` |
| **Social** | `Friendship`, `SocialNotification`, `CalendarLayer`, `CalendarItem` |
| **SMS** | `SmsContactList`, `SmsCampaign`, `SmsMessage`, `SmsConversation` |
| **People** | `Contact`, `ContactAddress`, `ContactEmail`, `ContactPhone`, `ContactConnection` |
| **Settings** | `SiteSettings`, `MapSettings` |
---
## Docker Compose Architecture
Services are organized into categories with dependency management:
```mermaid
graph TD
subgraph Core ["Core (always started)"]
PG["PostgreSQL"] --> API["Express API"]
Redis --> API
PG --> Media["Fastify Media API"]
API --> Admin["React Admin"]
Admin --> Nginx
API --> Nginx
Media --> Nginx
end
subgraph Communication ["Communication (optional)"]
RC["Rocket.Chat"] --> MongoDB
Jitsi["Jitsi Meet (4 containers)"]
Gancio["Gancio Events"]
end
subgraph Monitoring ["Monitoring (profile)"]
Prometheus --> Grafana
Prometheus --> Alertmanager
cAdvisor --> Prometheus
NodeExporter --> Prometheus
end
subgraph Tunnel ["Tunnel"]
Newt --> Nginx
end
```
Docker healthchecks ensure proper startup order: PostgreSQL and Redis must be healthy before the API starts. The API runs migrations and seeding automatically via its entrypoint script.
---
## Subdomain Routing
Nginx routes requests based on the `Host` header. All services run on the `changemaker-lite` Docker bridge network.
| Pattern | Target |
|---------|--------|
| `app.DOMAIN` | Admin GUI (admin + public + volunteer + gallery) |
| `api.DOMAIN` | Express API |
| `media.DOMAIN` | Fastify Media API |
| `DOMAIN` (root) | MkDocs static site |
| `*.DOMAIN` | 15+ additional service subdomains |
See [Services](../services/index.md) for the complete subdomain table.

View File

@ -2,6 +2,11 @@
title: Deployment
description: Deploy Changemaker Lite to production with Docker, SSL, backups, and monitoring.
icon: material/docker
tags:
- guide
- operator
- deployment
- docker
---
# Deployment

View File

@ -2,6 +2,11 @@
title: Control Panel (CCP)
description: Multi-tenant management for provisioning and operating multiple Changemaker Lite instances.
icon: material/console
status: new
tags:
- guide
- operator
- multi-tenant
---
# Changemaker Control Panel (CCP)

View File

@ -2,6 +2,11 @@
title: Environment Variables
description: Complete reference for every .env variable in Changemaker Lite.
icon: material/file-cog
tags:
- reference
- getting-started
- operator
- configuration
---
# Environment Variables

View File

@ -2,6 +2,11 @@
title: Features at a Glance
description: A visual overview of every Changemaker Lite module.
icon: material/star-shooting
tags:
- reference
- getting-started
search:
boost: 2
---
# Features at a Glance

View File

@ -2,6 +2,12 @@
title: First Steps
description: Log in, explore the dashboard, and set up your first campaign and volunteer shift.
icon: material/shoe-print
tags:
- tutorial
- getting-started
- admin
search:
boost: 2
---
# First Steps

View File

@ -2,6 +2,12 @@
title: Getting Started
description: Install and configure Changemaker Lite from scratch.
icon: material/rocket-launch
tags:
- guide
- getting-started
- operator
search:
boost: 2
---
# Getting Started

View File

@ -2,6 +2,13 @@
title: Installation
description: System requirements, configuration wizard walkthrough, and initial service startup.
icon: material/download
tags:
- guide
- getting-started
- operator
- docker
search:
boost: 2
---
# Installation

View File

@ -2,6 +2,11 @@
title: Services Overview
description: Complete catalog of all Docker services, ports, and startup commands.
icon: material/docker
tags:
- reference
- getting-started
- operator
- docker
---
# Services Overview

View File

@ -2,6 +2,10 @@
title: Updates & Upgrades
description: Keep Changemaker Lite up to date via the admin GUI or command line.
icon: material/update
tags:
- guide
- operator
- upgrades
---
# Updates & Upgrades

View File

@ -2,6 +2,11 @@
title: Documentation
description: Changemaker Lite documentation hub — guides for users, admins, and operators.
icon: material/book-open-variant
tags:
- guide
- getting-started
search:
boost: 2
---
# Documentation
@ -82,7 +87,7 @@ Welcome to the Changemaker Lite documentation. Whether you're a campaign volunte
Prometheus metrics, Grafana dashboards, Alertmanager rules, and health checks.
[:octicons-arrow-right-24: Monitoring](../blog/index.md){ .md-button .md-button--secondary } *Coming soon*
[:octicons-arrow-right-24: Monitoring](admin/services/monitoring.md)
</div>
@ -120,7 +125,7 @@ Welcome to the Changemaker Lite documentation. Whether you're a campaign volunte
Development setup, code style, git workflow, and pull request guidelines.
[:octicons-arrow-right-24: Contributing](../blog/index.md){ .md-button .md-button--secondary } *Coming soon*
[:octicons-arrow-right-24: Contributing](https://gitea.bnkops.com/admin/changemaker.lite){ target="_blank" }
</div>

View File

@ -1,6 +1,10 @@
---
title: Philosophy
description: Why we build Changemaker Lite — and why your movement should own its infrastructure.
tags:
- concept
- philosophy
- FOSS
---
# Philosophy

View File

@ -2,6 +2,11 @@
title: Services
description: All services that make up the Changemaker Lite platform — configuration, ports, and links to upstream docs.
icon: material/server-network
tags:
- reference
- operator
- services
- docker
---
# Services

View File

@ -2,14 +2,18 @@
title: Troubleshooting
description: Solutions for common errors, CORS issues, database problems, and tunnel debugging.
icon: material/bug
tags:
- troubleshooting
- operator
search:
boost: 2
---
# Troubleshooting
Common issues and their solutions when running Changemaker Lite.
!!! warning "Under Construction"
This troubleshooting guide is being expanded. Check back soon for more solutions.
---
## CORS Errors in Production
@ -23,26 +27,202 @@ CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000
Then restart the API: `docker compose restart api`
## Pangolin Tunnel 403/302 Errors
---
**Symptom:** All API endpoints return 302 redirects to Pangolin auth page.
## Pangolin Tunnel — 403/302 Errors
**Fix:** In the Pangolin dashboard, set each resource to "Not Protected" (public access).
**Symptom:** All API endpoints return 302 redirects to Pangolin auth page, or 403 Forbidden.
**Fix:** In the Pangolin dashboard, set each resource to **Not Protected** (public access). Critical resources to fix first:
1. `api.yourdomain.org` — Main API
2. `app.yourdomain.org` — Admin GUI + public pages
3. `media.yourdomain.org` — Media API
**Verify:**
```bash
# Should return JSON, NOT a 302 redirect
curl -I https://api.yourdomain.org/api/health
```
---
## Database Connection Failures
**Symptom:** API logs show database connection errors.
**Checklist:**
1. Check PostgreSQL: `docker compose ps v2-postgres`
2. Verify `DATABASE_URL` in `.env`
2. Verify `DATABASE_URL` in `.env` matches container name and port
3. View logs: `docker compose logs v2-postgres --tail 50`
4. Test connection: `docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"`
---
## Redis Connection Failures
**Symptom:** API logs show Redis connection errors, rate limiting doesn't work.
**Checklist:**
1. Check Redis: `docker compose ps redis-changemaker`
2. Verify `REDIS_PASSWORD` and `REDIS_URL` format in `.env`
3. Test: `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping`
2. Verify `REDIS_PASSWORD` and `REDIS_URL` format (`redis://:password@host:port`)
3. View logs: `docker compose logs redis-changemaker --tail 50`
4. Test: `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping`
---
## API Not Starting
**Symptom:** API container keeps restarting or won't start.
**Checklist:**
1. Check logs: `docker compose logs api --tail 100`
2. Verify all required env vars are set (see `.env.example`)
3. Run migrations: `docker compose exec api npx prisma migrate deploy`
2. Verify all required env vars are set (compare with `.env.example`)
3. Check database is ready: `docker compose ps v2-postgres` (should show "healthy")
4. Run migrations manually: `docker compose exec api npx prisma migrate deploy`
5. Check for port conflicts: `ss -tlnp | grep 4000`
---
## Containers in Restart Loops
**Symptom:** `docker compose ps` shows containers with "restarting" status.
**Diagnosis:**
```bash
# Find restarting containers
docker compose ps | grep -i restarting
# Check recent logs for the problem container
docker compose logs <service-name> --tail 50
# Check container exit code
docker inspect <container-name> --format='{{.State.ExitCode}}'
```
**Common causes:**
- Missing environment variables (check `.env`)
- Database not ready (healthcheck dependencies)
- Port already in use by another process
- Insufficient memory (check with `free -h`)
---
## Newt Tunnel Won't Connect
**Checklist (in order):**
1. **Credentials:** Verify `PANGOLIN_NEWT_ID` and `PANGOLIN_NEWT_SECRET` in `.env`
2. **Endpoint:** Confirm `PANGOLIN_ENDPOINT` matches your Pangolin server URL
3. **Logs:** `docker compose logs newt --tail 50`
4. **Nginx running:** Newt depends on nginx — `docker compose ps nginx`
5. **Network:** Ensure outbound HTTPS is not blocked by your firewall
---
## Migration Errors
**Symptom:** `prisma migrate deploy` fails.
**Common fixes:**
```bash
# Check migration status
docker compose exec api npx prisma migrate status
# If migrations are out of sync, reset (DESTRUCTIVE — dev only)
docker compose exec api npx prisma migrate reset
# If shadow database errors, create one
docker compose exec -T v2-postgres createdb -U changemaker prisma_shadow_diff
```
!!! danger "Never use `prisma db push` in production"
Always use `prisma migrate dev` (development) or `prisma migrate deploy` (production) to keep migration history in sync.
---
## Media API Upload Failures
**Symptom:** Video uploads fail with permission errors or 500 status.
**Checklist:**
1. Verify inbox volume is writable: check `media/local/inbox` has `:rw` mount
2. Check disk space: `df -h`
3. Verify FFmpeg is installed in container: `docker compose exec media-api ffprobe -version`
4. Check upload size limit: default is 10 GB in Fastify multipart config
---
## Email Not Sending
**Symptom:** Advocacy emails or notifications aren't delivered.
**Checklist:**
1. Check `EMAIL_TEST_MODE` — if `true`, all emails go to MailHog (`http://localhost:8025`)
2. Verify SMTP credentials in `.env` (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`)
3. Check BullMQ queue: visit **Admin > Email Queue** or check logs
4. Test SMTP from Settings: **Admin > Settings > Email > Test Connection**
---
## Services Unreachable via Tunnel
**Checklist:**
1. Verify nginx is running: `docker compose ps nginx`
2. Test locally first: `curl http://localhost:4000/api/health`
3. Check nginx logs: `docker compose logs nginx --tail 50`
4. Verify DNS: `dig app.yourdomain.org` should point to your Pangolin server
5. Check Pangolin resources are all set to "Not Protected"
---
## Slow Map Performance
**Symptom:** Map page is slow or returns 500 errors with many locations.
**Causes and fixes:**
- **Too many locations loaded at once** — the API limits by address count with debounced bounds queries
- **Missing indexes** — verify database has the 5 performance indexes on Location/Address tables
- **Browser memory** — marker clustering activates above zoom level 18; below that, addresses are grouped
---
## Docker Disk Space
**Symptom:** Builds fail, containers can't start, or images won't pull.
```bash
# Check disk usage
df -h
# Clean unused Docker resources
docker system prune -f
# Clean old images (keep only last 2 days)
docker image prune -a --filter "until=48h"
# Check what's using space
docker system df
```
---
## Getting Help
If your issue isn't listed here:
1. Check the API logs: `docker compose logs api --tail 200`
2. Search the [Gitea issues](https://gitea.bnkops.com/admin/changemaker.lite/issues)
3. Review the [Deployment guide](../deployment/index.md) for production-specific issues
4. File a new issue with your logs and `.env` (redact passwords)

View File

@ -2,6 +2,11 @@
title: Campaigns
description: Find advocacy campaigns, look up your representatives by postal code, and send emails.
icon: material/email-fast
tags:
- guide
- user
- influence
- campaigns
---
# Campaigns

View File

@ -2,6 +2,11 @@
title: Donations
description: Support the cause with one-time donations on branded pages with goals and suggested amounts.
icon: material/hand-heart
status: new
tags:
- guide
- user
- payments
---
# Donations

View File

@ -2,6 +2,10 @@
title: Events (Gancio)
description: Self-hosted event management with automatic shift-to-event sync powered by Gancio.
icon: material/calendar-star
tags:
- guide
- user
- events
---
# Events (Gancio)

View File

@ -2,6 +2,11 @@
title: Gallery
description: Watch campaign videos, browse photos, explore playlists, and view shorts.
icon: material/play-box-multiple
tags:
- guide
- user
- media
- gallery
---
# Gallery

View File

@ -2,6 +2,9 @@
title: User Guide
description: How to use Changemaker Lite as a public visitor or registered supporter.
icon: material/account
tags:
- guide
- user
---
# User Guide

View File

@ -2,6 +2,10 @@
title: Map
description: Explore the interactive community map showing locations across your area.
icon: material/map
tags:
- guide
- user
- map
---
# Map

View File

@ -2,6 +2,11 @@
title: Self-Service Contact Profile
description: Token-based CRM contact profiles where supporters can view their engagement history, edit their details, manage communication preferences, and opt out.
icon: material/card-account-details
status: new
tags:
- guide
- user
- CRM
---
# Self-Service Contact Profile

View File

@ -2,6 +2,10 @@
title: Shifts
description: Sign up for volunteer shifts, join canvassing teams, and get started quickly with QR codes.
icon: material/calendar-clock
tags:
- guide
- user
- shifts
---
# Shifts

View File

@ -2,6 +2,11 @@
title: Shop & Pricing
description: Browse campaign merchandise and membership subscription plans.
icon: material/shopping
status: new
tags:
- guide
- user
- payments
---
# Shop & Pricing

View File

@ -2,6 +2,11 @@
title: Achievements & Leaderboard
description: Badge-based gamification and volunteer leaderboards to recognize and encourage platform participation.
icon: material/trophy
status: new
tags:
- guide
- volunteer
- social
---
# Achievements & Leaderboard

View File

@ -2,6 +2,11 @@
title: Canvassing
description: GPS-guided door-to-door canvassing with the volunteer map, visit recording, and walking routes.
icon: material/map-marker-path
tags:
- guide
- volunteer
- map
- canvassing
---
# Canvassing

View File

@ -2,6 +2,9 @@
title: Volunteer Guide
description: Getting started as a volunteer — shifts, canvassing, social connections, and achievements.
icon: material/walk
tags:
- guide
- volunteer
---
# Volunteer Guide

View File

@ -2,6 +2,10 @@
title: Your Shifts
description: View your assigned volunteer shifts, activity history, and canvassing stats.
icon: material/calendar-check
tags:
- guide
- volunteer
- shifts
---
# Your Shifts

View File

@ -2,6 +2,11 @@
title: Social Connections
description: Connect with fellow volunteers through friend requests, activity feeds, groups, and real-time notifications.
icon: material/account-heart
status: new
tags:
- guide
- volunteer
- social
---
# Social Connections

View File

@ -0,0 +1,50 @@
*[API]: Application Programming Interface
*[JWT]: JSON Web Token
*[RBAC]: Role-Based Access Control
*[CORS]: Cross-Origin Resource Sharing
*[SMTP]: Simple Mail Transfer Protocol
*[CSV]: Comma-Separated Values
*[ORM]: Object-Relational Mapping
*[SSL]: Secure Sockets Layer
*[TLS]: Transport Layer Security
*[DNS]: Domain Name System
*[CRUD]: Create, Read, Update, Delete
*[SSO]: Single Sign-On
*[SPA]: Single Page Application
*[CLI]: Command Line Interface
*[GUI]: Graphical User Interface
*[QR]: Quick Response
*[GPS]: Global Positioning System
*[GDPR]: General Data Protection Regulation
*[CDN]: Content Delivery Network
*[VPS]: Virtual Private Server
*[CGNAT]: Carrier-Grade Network Address Translation
*[NAR]: National Address Register
*[CRM]: Customer Relationship Management
*[OG]: Open Graph
*[DDoS]: Distributed Denial of Service
*[SSE]: Server-Sent Events
*[UUID]: Universally Unique Identifier
*[FOSS]: Free and Open Source Software
*[HSTS]: HTTP Strict Transport Security
*[CSP]: Content Security Policy
*[BullMQ]: Bull Message Queue
*[FFprobe]: FFmpeg Probe — media metadata extraction tool
*[FFmpeg]: Fast Forward MPEG — multimedia processing framework
*[XMPP]: Extensible Messaging and Presence Protocol
*[ACME]: Automatic Certificate Management Environment
*[SSH]: Secure Shell
*[YAML]: YAML Ain't Markup Language
*[JSON]: JavaScript Object Notation
*[HTML]: HyperText Markup Language
*[CSS]: Cascading Style Sheets
*[UI]: User Interface
*[UX]: User Experience
*[CI/CD]: Continuous Integration / Continuous Deployment
*[UID]: User Identifier
*[GID]: Group Identifier
*[RSVP]: Respondez S'il Vous Plait
*[CTR]: Click-Through Rate
*[KPI]: Key Performance Indicator
*[AES]: Advanced Encryption Standard
*[PIN]: Personal Identification Number

View File

@ -1,7 +0,0 @@
---
template: lander.html
hide:
- navigation
- toc
title: "lander"
---

View File

@ -1,7 +0,0 @@
---
template: main.html
hide:
- navigation
- toc
title: "main"
---

View File

@ -1,7 +0,0 @@
{% extends "main.html" %}
{% block content %}
<style>
* { box-sizing: border-box; } body {margin: 0;}#i25w{padding:10px;}
</style>
<body id="i7af"><div id="i25w">Insert your text here</div></body>
{% endblock %}

View File

@ -1,7 +0,0 @@
---
template: test-page.html
hide:
- navigation
- toc
title: "Test Page"
---

View File

@ -1,461 +0,0 @@
# Test
Testing page.
[[test-page]]
TEst
test
test
<div class="photo-block" data-photo-id="1" data-size="large" data-caption="" data-link-to-gallery="true" data-alignment="center">Loading...</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 12px; margin: 16px 0;">
<h2 style="color: #fff; margin: 12px 0;">Choose Your Plan</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Get access to exclusive content and features.</p>
<a href="http://app.org/pricing" style="display: inline-block; padding: 14px 36px; background: #722ed1; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">View Plans</a>
</div>
<div class="photo-card-block" data-photo-id="1" data-photo-title="vlcsnap-2026-01-09-15h39m52s898.png" data-photo-format="png" data-photo-width="1920" data-photo-height="1040" data-photo-views="0" style="max-width: 480px; margin: 0 auto;">
<a href="http://app.org/gallery?expanded=photo-1" style="display: block; text-decoration: none; color: inherit; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="position: relative; padding-bottom: 66.67%; background: #0d1b2a; overflow: hidden;">
<img src="http://app.orgdata:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22320%22%20viewBox%3D%220%200%20480%20320%22%3E%3Crect%20fill%3D%22%230d1b2a%22%20width%3D%22480%22%20height%3D%22320%22%2F%3E%3Ccircle%20cx%3D%22240%22%20cy%3D%22160%22%20r%3D%2232%22%20fill%3D%22rgba(46%2C125%2C50%2C0.6)%22%2F%3E%3Crect%20x%3D%22224%22%20y%3D%22144%22%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%224%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%22154%22%20r%3D%223%22%20fill%3D%22%23fff%22%2F%3E%3Cpath%20d%3D%22M256%20176l-10-10L224%20176%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3C%2Fsvg%3E" alt="vlcsnap-2026-01-09-15h39m52s898.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;" />
<span style="position: absolute; top: 8px; left: 8px; background: #2e7d32; color: #fff; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px;">PNG</span>
<span style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: #fff; font-size: 12px; font-weight: 500; padding: 2px 6px; border-radius: 4px;">1920×1040</span>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 56px; height: 56px; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">vlcsnap-2026-01-09-15h39m52s898.png</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 6px;">
<span style="color: #8899aa; font-size: 13px;">0 views</span>
<span style="color: #43cea2; font-size: 13px; font-weight: 500;">View &rarr;</span>
</div>
</div>
</a>
</div>
<div id="cm-product-mlrk53ro" style="text-align:center;padding:32px 20px;background:linear-gradient(135deg,#1a1a2e,#16213e);border-radius:12px;margin:16px 0;max-width:420px;margin-left:auto;margin-right:auto;">
<div style="width:80px;height:80px;border-radius:12px;background:linear-gradient(135deg,#9d4edd,#722ed1);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;"><span style="font-size:36px;color:#fff;">&#x1F6D2;</span></div>
<div style="display:inline-block;padding:2px 10px;border-radius:4px;background:#1890ff;color:#fff;font-size:11px;font-weight:600;margin-bottom:8px;">DIGITAL</div>
<h3 style="color:#fff;margin:8px 0 4px;">Test Product 1</h3>
<p style="color:rgba(255,255,255,0.7);font-size:0.9rem;margin-bottom:12px;">A test product</p>
<p style="color:#fff;font-size:1.4rem;font-weight:700;margin-bottom:20px;">$90.00</p>
<div id="cm-product-mlrk53ro-form" style="max-width:320px;margin:0 auto;text-align:left;">
<input type="email" id="cm-product-mlrk53ro-email" placeholder="your@email.com *" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" required />
<input type="text" id="cm-product-mlrk53ro-name" placeholder="Name (optional)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
<div id="cm-product-mlrk53ro-error" style="color:#ff4d4f;font-size:0.9rem;margin-bottom:8px;display:none;"></div>
<button type="button" id="cm-product-mlrk53ro-submit" style="width:100%;padding:14px 24px;background:#722ed1;color:#fff;border:none;border-radius:8px;font-weight:600;font-size:1.05rem;cursor:pointer;">Buy Now &mdash; $90.00</button>
<p style="margin-top:12px;font-size:0.75rem;color:rgba(255,255,255,0.4);text-align:center;">
Secure payment via Stripe. <a href="http://app.org/shop" style="color:rgba(255,255,255,0.5);">Browse all products</a>
</p>
</div>
<noscript><a href="http://app.org/shop" style="display:inline-block;padding:14px 36px;background:#722ed1;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;">View in Shop</a></noscript>
</div>
<script>
(function(){
var root=document.getElementById('cm-product-mlrk53ro');
if(!root)return;
var apiUrl='http://api.org';
var productId='cmlritfdm0000mkbgf9eelg4t';
var submitBtn=document.getElementById('cm-product-mlrk53ro-submit');
var emailInput=document.getElementById('cm-product-mlrk53ro-email');
var nameInput=document.getElementById('cm-product-mlrk53ro-name');
var errDiv=document.getElementById('cm-product-mlrk53ro-error');
function showErr(msg){errDiv.textContent=msg;errDiv.style.display='block';}
function hideErr(){errDiv.style.display='none';}
submitBtn.addEventListener('click',function(){
hideErr();
var email=(emailInput.value||'').trim();
if(!email||email.indexOf('@')<1){showErr('Please enter a valid email address.');return;}
submitBtn.disabled=true;submitBtn.textContent='Processing...';
fetch(apiUrl+'/api/payments/purchase',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({productId:productId,buyerEmail:email,buyerName:(nameInput.value||'').trim()||undefined})
}).then(function(r){return r.json();}).then(function(data){
if(data.url){window.location.href=data.url;}
else{showErr(data.error&&data.error.message||'Something went wrong.');submitBtn.disabled=false;submitBtn.textContent='Buy Now';}
}).catch(function(){showErr('Connection error. Please try again.');submitBtn.disabled=false;submitBtn.textContent='Buy Now';});
});
})();
</script>
<div id="cm-donate-mlrjofs1" style="text-align:center;padding:40px 20px;background:linear-gradient(135deg,#2d1b69,#1a1a2e);border-radius:12px;margin:16px 0;max-width:560px;margin-left:auto;margin-right:auto;">
<p style="font-size:48px;margin:0;">&#x2764;&#xFE0F;</p>
<h2 style="color:#fff;margin:12px 0;">Support Our Work</h2>
<p style="color:rgba(255,255,255,0.8);margin-bottom:24px;">Every contribution makes a difference. Choose an amount below.</p>
<div id="cm-donate-mlrjofs1-amounts" style="margin-bottom: 16px;">
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="1000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$10</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="2500" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$25</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="5000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$50</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="10000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$100</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="custom" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">Custom</button>
</div>
<div id="cm-donate-mlrjofs1-custom" style="display:none;margin-bottom:12px;">
<input type="number" id="cm-donate-mlrjofs1-custom-input" min="1" step="1" placeholder="Enter amount ($)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
</div>
<div id="cm-donate-mlrjofs1-form" style="display:none;max-width:360px;margin:0 auto;text-align:left;">
<input type="email" id="cm-donate-mlrjofs1-email" placeholder="your@email.com *" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" required />
<input type="text" id="cm-donate-mlrjofs1-name" placeholder="Name (optional)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
<div style="margin-bottom:12px;color:rgba(255,255,255,0.7);font-size:0.85rem;">
<label><input type="checkbox" id="cm-donate-mlrjofs1-anon" style="margin-right:6px;" />Make my donation anonymous</label>
</div>
<div id="cm-donate-mlrjofs1-error" style="color:#ff4d4f;font-size:0.9rem;margin-bottom:8px;display:none;"></div>
<button type="button" id="cm-donate-mlrjofs1-submit" style="width:100%;padding:14px 24px;background:#eb2f96;color:#fff;border:none;border-radius:8px;font-weight:600;font-size:1.05rem;cursor:pointer;">
Donate
</button>
<p style="margin-top:12px;font-size:0.75rem;color:rgba(255,255,255,0.4);text-align:center;">
Secure payment via Stripe. <a href="http://app.org/donate" style="color:rgba(255,255,255,0.5);">Open full donate page</a>
</p>
</div>
<noscript><a href="http://app.org/donate" style="display:inline-block;padding:14px 36px;background:#eb2f96;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;">Donate Now</a></noscript>
</div>
<script>
(function(){
var root=document.getElementById('cm-donate-mlrjofs1');
if(!root)return;
var apiUrl='http://api.org';
var selectedCents=0;
var form=document.getElementById('cm-donate-mlrjofs1-form');
var submitBtn=document.getElementById('cm-donate-mlrjofs1-submit');
var emailInput=document.getElementById('cm-donate-mlrjofs1-email');
var nameInput=document.getElementById('cm-donate-mlrjofs1-name');
var anonBox=document.getElementById('cm-donate-mlrjofs1-anon');
var errDiv=document.getElementById('cm-donate-mlrjofs1-error');
var amtBtns=root.querySelectorAll('.cm-donate-mlrjofs1-amt');
var customWrap=document.getElementById('cm-donate-mlrjofs1-custom');
var customInput=document.getElementById('cm-donate-mlrjofs1-custom-input');
var activeStyle='background:#eb2f96;border-color:#eb2f96;';
var baseStyle='background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.25);';
function selectAmt(btn,cents){
amtBtns.forEach(function(b){b.style.cssText='display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);'+baseStyle;});
btn.style.cssText='display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);'+activeStyle;
if(cents==='custom'){
selectedCents=0;customWrap.style.display='block';
customInput.focus();
submitBtn.textContent='Donate';
}else{
selectedCents=parseInt(cents,10);customWrap.style.display='none';
submitBtn.textContent='Donate $'+(selectedCents/100).toFixed(0);
}
form.style.display='block';
}
amtBtns.forEach(function(b){
b.addEventListener('click',function(){selectAmt(b,b.getAttribute('data-cents'));});
});
if(customInput){customInput.addEventListener('input',function(){
var v=parseFloat(customInput.value);
if(v>0){selectedCents=Math.round(v*100);submitBtn.textContent='Donate $'+v.toFixed(2);}
else{selectedCents=0;submitBtn.textContent='Donate';}
});}
function showErr(msg){errDiv.textContent=msg;errDiv.style.display='block';}
function hideErr(){errDiv.style.display='none';}
submitBtn.addEventListener('click',function(){
hideErr();
var email=(emailInput.value||'').trim();
if(!email||email.indexOf('@')<1){showErr('Please enter a valid email address.');return;}
if(!selectedCents||selectedCents<100){showErr('Please select a donation amount.');return;}
submitBtn.disabled=true;submitBtn.textContent='Processing...';
fetch(apiUrl+'/api/payments/donate',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({amountCents:selectedCents,email:email,name:(nameInput.value||'').trim()||undefined,isAnonymous:!!(anonBox&&anonBox.checked)})
}).then(function(r){return r.json();}).then(function(data){
if(data.url){window.location.href=data.url;}
else{showErr(data.error&&data.error.message||'Something went wrong.');submitBtn.disabled=false;submitBtn.textContent='Donate';}
}).catch(function(){showErr('Connection error. Please try again.');submitBtn.disabled=false;submitBtn.textContent='Donate';});
});
})();
</script>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #2d1b69, #1a1a2e); border-radius: 12px; margin: 16px 0; max-width: 560px; margin-left: auto; margin-right: auto;">
<p style="font-size: 48px; margin: 0;">&#x2764;&#xFE0F;</p>
<h2 style="color: #fff; margin: 12px 0;">Support Our Work</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Every contribution makes a difference. Choose an amount below.</p>
<div style="margin-bottom: 16px;">
<a href="http://app.org/donate?amount=1000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$10</a>
<a href="http://app.org/donate?amount=2500" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$25</a>
<a href="http://app.org/donate?amount=5000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$50</a>
<a href="http://app.org/donate?amount=10000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$100</a>
</div>
<a href="http://app.org/donate" style="display: inline-block; padding: 10px 22px; margin: 4px; background: #eb2f96; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem;">Custom Amount</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #9d4edd, #722ed1); border-radius: 12px; margin: 16px 0;">
<p style="font-size: 48px; margin: 0;">&#x1F6D2;</p>
<h2 style="color: #fff; margin: 12px 0;">Browse Our Products</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Reports, toolkits, event tickets, and more.</p>
<a href="http://app.org/shop" style="display: inline-block; padding: 14px 36px; background: #fff; color: #722ed1; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">Shop Now</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 12px; margin: 16px 0;">
<h2 style="color: #fff; margin: 12px 0;">Choose Your Plan</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Get access to exclusive content and features.</p>
<a href="http://app.org/pricing" style="display: inline-block; padding: 14px 36px; background: #722ed1; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">View Plans</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #2d1b69, #1a1a2e); border-radius: 12px; margin: 16px 0;">
<p style="font-size: 48px; margin: 0;">&#x2764;&#xFE0F;</p>
<h2 style="color: #fff; margin: 12px 0;">Support Our Cause</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Your contribution helps us create lasting change in our community.</p>
<a href="http://app.org/donate" style="display: inline-block; padding: 14px 36px; background: #eb2f96; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">Donate Now</a>
</div>
<div class="video-card-block" data-video-id="2" data-video-title="Testing This Sucker" data-video-duration="594" data-video-quality="" data-video-views="0" style="max-width: 480px; margin: 0 auto;">
<a href="http://app.org/gallery/watch/2" style="display: block; text-decoration: none; color: inherit; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="position: relative; padding-bottom: 56.25%; background: #0d1b2a; overflow: hidden;">
<img src="http://app.orgdata:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22270%22%20viewBox%3D%220%200%20480%20270%22%3E%3Crect%20fill%3D%22%230d1b2a%22%20width%3D%22480%22%20height%3D%22270%22%2F%3E%3Ccircle%20cx%3D%22240%22%20cy%3D%22135%22%20r%3D%2232%22%20fill%3D%22rgba(157%2C78%2C221%2C0.6)%22%2F%3E%3Cpolygon%20points%3D%22230%2C118%20258%2C135%20230%2C152%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E" alt="Testing This Sucker" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;" />
<span style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: #fff; font-size: 12px; font-weight: 500; padding: 2px 6px; border-radius: 4px;">9:54</span>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 56px; height: 56px; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 20 20" fill="#fff"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"/></svg>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Testing This Sucker</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 6px;">
<span style="color: #8899aa; font-size: 13px;">0 views</span>
<span style="color: #9d4edd; font-size: 13px; font-weight: 500;">Watch &rarr;</span>
</div>
</div>
</a>
</div>
# Test
Testing page.
<div class="photo-block" data-photo-id="1" data-size="large" data-caption="" data-link-to-gallery="true" data-alignment="center">Loading...</div>
<div class="photo-card-block" data-photo-id="1" data-photo-title="vlcsnap-2026-01-09-15h39m52s898.png" data-photo-format="png" data-photo-width="1920" data-photo-height="1040" data-photo-views="0" style="max-width: 480px; margin: 0 auto;">
<a href="http://app.org/gallery?expanded=photo-1" style="display: block; text-decoration: none; color: inherit; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="position: relative; padding-bottom: 66.67%; background: #0d1b2a; overflow: hidden;">
<img src="http://app.orgdata:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22320%22%20viewBox%3D%220%200%20480%20320%22%3E%3Crect%20fill%3D%22%230d1b2a%22%20width%3D%22480%22%20height%3D%22320%22%2F%3E%3Ccircle%20cx%3D%22240%22%20cy%3D%22160%22%20r%3D%2232%22%20fill%3D%22rgba(46%2C125%2C50%2C0.6)%22%2F%3E%3Crect%20x%3D%22224%22%20y%3D%22144%22%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%224%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%22154%22%20r%3D%223%22%20fill%3D%22%23fff%22%2F%3E%3Cpath%20d%3D%22M256%20176l-10-10L224%20176%22%20fill%3D%22none%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3C%2Fsvg%3E" alt="vlcsnap-2026-01-09-15h39m52s898.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;" />
<span style="position: absolute; top: 8px; left: 8px; background: #2e7d32; color: #fff; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px;">PNG</span>
<span style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: #fff; font-size: 12px; font-weight: 500; padding: 2px 6px; border-radius: 4px;">1920×1040</span>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 56px; height: 56px; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">vlcsnap-2026-01-09-15h39m52s898.png</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 6px;">
<span style="color: #8899aa; font-size: 13px;">0 views</span>
<span style="color: #43cea2; font-size: 13px; font-weight: 500;">View &rarr;</span>
</div>
</div>
</a>
</div>
<div id="cm-product-mlrk53ro" style="text-align:center;padding:32px 20px;background:linear-gradient(135deg,#1a1a2e,#16213e);border-radius:12px;margin:16px 0;max-width:420px;margin-left:auto;margin-right:auto;">
<div style="width:80px;height:80px;border-radius:12px;background:linear-gradient(135deg,#9d4edd,#722ed1);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;"><span style="font-size:36px;color:#fff;">&#x1F6D2;</span></div>
<div style="display:inline-block;padding:2px 10px;border-radius:4px;background:#1890ff;color:#fff;font-size:11px;font-weight:600;margin-bottom:8px;">DIGITAL</div>
<h3 style="color:#fff;margin:8px 0 4px;">Test Product 1</h3>
<p style="color:rgba(255,255,255,0.7);font-size:0.9rem;margin-bottom:12px;">A test product</p>
<p style="color:#fff;font-size:1.4rem;font-weight:700;margin-bottom:20px;">$90.00</p>
<div id="cm-product-mlrk53ro-form" style="max-width:320px;margin:0 auto;text-align:left;">
<input type="email" id="cm-product-mlrk53ro-email" placeholder="your@email.com *" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" required />
<input type="text" id="cm-product-mlrk53ro-name" placeholder="Name (optional)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
<div id="cm-product-mlrk53ro-error" style="color:#ff4d4f;font-size:0.9rem;margin-bottom:8px;display:none;"></div>
<button type="button" id="cm-product-mlrk53ro-submit" style="width:100%;padding:14px 24px;background:#722ed1;color:#fff;border:none;border-radius:8px;font-weight:600;font-size:1.05rem;cursor:pointer;">Buy Now &mdash; $90.00</button>
<p style="margin-top:12px;font-size:0.75rem;color:rgba(255,255,255,0.4);text-align:center;">
Secure payment via Stripe. <a href="http://app.org/shop" style="color:rgba(255,255,255,0.5);">Browse all products</a>
</p>
</div>
<noscript><a href="http://app.org/shop" style="display:inline-block;padding:14px 36px;background:#722ed1;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;">View in Shop</a></noscript>
</div>
<script>
(function(){
var root=document.getElementById('cm-product-mlrk53ro');
if(!root)return;
var apiUrl='http://api.org';
var productId='cmlritfdm0000mkbgf9eelg4t';
var submitBtn=document.getElementById('cm-product-mlrk53ro-submit');
var emailInput=document.getElementById('cm-product-mlrk53ro-email');
var nameInput=document.getElementById('cm-product-mlrk53ro-name');
var errDiv=document.getElementById('cm-product-mlrk53ro-error');
function showErr(msg){errDiv.textContent=msg;errDiv.style.display='block';}
function hideErr(){errDiv.style.display='none';}
submitBtn.addEventListener('click',function(){
hideErr();
var email=(emailInput.value||'').trim();
if(!email||email.indexOf('@')<1){showErr('Please enter a valid email address.');return;}
submitBtn.disabled=true;submitBtn.textContent='Processing...';
fetch(apiUrl+'/api/payments/purchase',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({productId:productId,buyerEmail:email,buyerName:(nameInput.value||'').trim()||undefined})
}).then(function(r){return r.json();}).then(function(data){
if(data.url){window.location.href=data.url;}
else{showErr(data.error&&data.error.message||'Something went wrong.');submitBtn.disabled=false;submitBtn.textContent='Buy Now';}
}).catch(function(){showErr('Connection error. Please try again.');submitBtn.disabled=false;submitBtn.textContent='Buy Now';});
});
})();
</script>
<div id="cm-donate-mlrjofs1" style="text-align:center;padding:40px 20px;background:linear-gradient(135deg,#2d1b69,#1a1a2e);border-radius:12px;margin:16px 0;max-width:560px;margin-left:auto;margin-right:auto;">
<p style="font-size:48px;margin:0;">&#x2764;&#xFE0F;</p>
<h2 style="color:#fff;margin:12px 0;">Support Our Work</h2>
<p style="color:rgba(255,255,255,0.8);margin-bottom:24px;">Every contribution makes a difference. Choose an amount below.</p>
<div id="cm-donate-mlrjofs1-amounts" style="margin-bottom: 16px;">
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="1000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$10</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="2500" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$25</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="5000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$50</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="10000" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">$100</button>
<button type="button" class="cm-donate-mlrjofs1-amt" data-cents="custom" style="display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);">Custom</button>
</div>
<div id="cm-donate-mlrjofs1-custom" style="display:none;margin-bottom:12px;">
<input type="number" id="cm-donate-mlrjofs1-custom-input" min="1" step="1" placeholder="Enter amount ($)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
</div>
<div id="cm-donate-mlrjofs1-form" style="display:none;max-width:360px;margin:0 auto;text-align:left;">
<input type="email" id="cm-donate-mlrjofs1-email" placeholder="your@email.com *" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" required />
<input type="text" id="cm-donate-mlrjofs1-name" placeholder="Name (optional)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.25);border-radius:8px;color:#fff;font-size:0.95rem;box-sizing:border-box;margin-bottom:10px;outline:none;" />
<div style="margin-bottom:12px;color:rgba(255,255,255,0.7);font-size:0.85rem;">
<label><input type="checkbox" id="cm-donate-mlrjofs1-anon" style="margin-right:6px;" />Make my donation anonymous</label>
</div>
<div id="cm-donate-mlrjofs1-error" style="color:#ff4d4f;font-size:0.9rem;margin-bottom:8px;display:none;"></div>
<button type="button" id="cm-donate-mlrjofs1-submit" style="width:100%;padding:14px 24px;background:#eb2f96;color:#fff;border:none;border-radius:8px;font-weight:600;font-size:1.05rem;cursor:pointer;">
Donate
</button>
<p style="margin-top:12px;font-size:0.75rem;color:rgba(255,255,255,0.4);text-align:center;">
Secure payment via Stripe. <a href="http://app.org/donate" style="color:rgba(255,255,255,0.5);">Open full donate page</a>
</p>
</div>
<noscript><a href="http://app.org/donate" style="display:inline-block;padding:14px 36px;background:#eb2f96;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;">Donate Now</a></noscript>
</div>
<script>
(function(){
var root=document.getElementById('cm-donate-mlrjofs1');
if(!root)return;
var apiUrl='http://api.org';
var selectedCents=0;
var form=document.getElementById('cm-donate-mlrjofs1-form');
var submitBtn=document.getElementById('cm-donate-mlrjofs1-submit');
var emailInput=document.getElementById('cm-donate-mlrjofs1-email');
var nameInput=document.getElementById('cm-donate-mlrjofs1-name');
var anonBox=document.getElementById('cm-donate-mlrjofs1-anon');
var errDiv=document.getElementById('cm-donate-mlrjofs1-error');
var amtBtns=root.querySelectorAll('.cm-donate-mlrjofs1-amt');
var customWrap=document.getElementById('cm-donate-mlrjofs1-custom');
var customInput=document.getElementById('cm-donate-mlrjofs1-custom-input');
var activeStyle='background:#eb2f96;border-color:#eb2f96;';
var baseStyle='background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.25);';
function selectAmt(btn,cents){
amtBtns.forEach(function(b){b.style.cssText='display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);'+baseStyle;});
btn.style.cssText='display:inline-block;padding:10px 22px;margin:4px;color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer;border:2px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);'+activeStyle;
if(cents==='custom'){
selectedCents=0;customWrap.style.display='block';
customInput.focus();
submitBtn.textContent='Donate';
}else{
selectedCents=parseInt(cents,10);customWrap.style.display='none';
submitBtn.textContent='Donate $'+(selectedCents/100).toFixed(0);
}
form.style.display='block';
}
amtBtns.forEach(function(b){
b.addEventListener('click',function(){selectAmt(b,b.getAttribute('data-cents'));});
});
if(customInput){customInput.addEventListener('input',function(){
var v=parseFloat(customInput.value);
if(v>0){selectedCents=Math.round(v*100);submitBtn.textContent='Donate $'+v.toFixed(2);}
else{selectedCents=0;submitBtn.textContent='Donate';}
});}
function showErr(msg){errDiv.textContent=msg;errDiv.style.display='block';}
function hideErr(){errDiv.style.display='none';}
submitBtn.addEventListener('click',function(){
hideErr();
var email=(emailInput.value||'').trim();
if(!email||email.indexOf('@')<1){showErr('Please enter a valid email address.');return;}
if(!selectedCents||selectedCents<100){showErr('Please select a donation amount.');return;}
submitBtn.disabled=true;submitBtn.textContent='Processing...';
fetch(apiUrl+'/api/payments/donate',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({amountCents:selectedCents,email:email,name:(nameInput.value||'').trim()||undefined,isAnonymous:!!(anonBox&&anonBox.checked)})
}).then(function(r){return r.json();}).then(function(data){
if(data.url){window.location.href=data.url;}
else{showErr(data.error&&data.error.message||'Something went wrong.');submitBtn.disabled=false;submitBtn.textContent='Donate';}
}).catch(function(){showErr('Connection error. Please try again.');submitBtn.disabled=false;submitBtn.textContent='Donate';});
});
})();
</script>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #2d1b69, #1a1a2e); border-radius: 12px; margin: 16px 0; max-width: 560px; margin-left: auto; margin-right: auto;">
<p style="font-size: 48px; margin: 0;">&#x2764;&#xFE0F;</p>
<h2 style="color: #fff; margin: 12px 0;">Support Our Work</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Every contribution makes a difference. Choose an amount below.</p>
<div style="margin-bottom: 16px;">
<a href="http://app.org/donate?amount=1000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$10</a>
<a href="http://app.org/donate?amount=2500" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$25</a>
<a href="http://app.org/donate?amount=5000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$50</a>
<a href="http://app.org/donate?amount=10000" style="display: inline-block; padding: 10px 22px; margin: 4px; background: rgba(255,255,255,0.12); color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem; border: 2px solid rgba(255,255,255,0.2);">$100</a>
</div>
<a href="http://app.org/donate" style="display: inline-block; padding: 10px 22px; margin: 4px; background: #eb2f96; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1rem;">Custom Amount</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #9d4edd, #722ed1); border-radius: 12px; margin: 16px 0;">
<p style="font-size: 48px; margin: 0;">&#x1F6D2;</p>
<h2 style="color: #fff; margin: 12px 0;">Browse Our Products</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Reports, toolkits, event tickets, and more.</p>
<a href="http://app.org/shop" style="display: inline-block; padding: 14px 36px; background: #fff; color: #722ed1; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">Shop Now</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #1a1a2e, #16213e); border-radius: 12px; margin: 16px 0;">
<h2 style="color: #fff; margin: 12px 0;">Choose Your Plan</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Get access to exclusive content and features.</p>
<a href="http://app.org/pricing" style="display: inline-block; padding: 14px 36px; background: #722ed1; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">View Plans</a>
</div>
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #2d1b69, #1a1a2e); border-radius: 12px; margin: 16px 0;">
<p style="font-size: 48px; margin: 0;">&#x2764;&#xFE0F;</p>
<h2 style="color: #fff; margin: 12px 0;">Support Our Cause</h2>
<p style="color: rgba(255,255,255,0.8); margin-bottom: 24px;">Your contribution helps us create lasting change in our community.</p>
<a href="http://app.org/donate" style="display: inline-block; padding: 14px 36px; background: #eb2f96; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">Donate Now</a>
</div>
<div class="video-card-block" data-video-id="2" data-video-title="Testing This Sucker" data-video-duration="594" data-video-quality="" data-video-views="0" style="max-width: 480px; margin: 0 auto;">
<a href="http://app.org/gallery/watch/2" style="display: block; text-decoration: none; color: inherit; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="position: relative; padding-bottom: 56.25%; background: #0d1b2a; overflow: hidden;">
<img src="http://app.orgdata:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22270%22%20viewBox%3D%220%200%20480%20270%22%3E%3Crect%20fill%3D%22%230d1b2a%22%20width%3D%22480%22%20height%3D%22270%22%2F%3E%3Ccircle%20cx%3D%22240%22%20cy%3D%22135%22%20r%3D%2232%22%20fill%3D%22rgba(157%2C78%2C221%2C0.6)%22%2F%3E%3Cpolygon%20points%3D%22230%2C118%20258%2C135%20230%2C152%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E" alt="Testing This Sucker" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;" />
<span style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: #fff; font-size: 12px; font-weight: 500; padding: 2px 6px; border-radius: 4px;">9:54</span>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 56px; height: 56px; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 20 20" fill="#fff"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"/></svg>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Testing This Sucker</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 6px;">
<span style="color: #8899aa; font-size: 13px;">0 views</span>
<span style="color: #9d4edd; font-size: 13px; font-weight: 500;">Watch &rarr;</span>
</div>
</div>
</a>
</div>

View File

@ -1,19 +0,0 @@
# testing
# testing
## Wiki-Link Tests
- Doc link: [[installation]]
- Doc link with display text: [[installation|Install Guide]]
- Doc link with anchor: [[installation#prerequisites]]
- Image embed: ![[logo.png]]
- Image with alt: ![[logo.png|Site Logo]]
- Code block (should NOT be converted):
```
[[this-should-stay-as-is]]
```
- Inline code (should NOT be converted): `[[not-a-link]]`
- Unresolved link (should stay as-is): [[nonexistent-page]]

View File

@ -9,7 +9,7 @@ use_directory_urls: true
# Repository
repo_url: https://gitea.bnkops.com/admin/changemaker.lite
repo_name: changemaker.lite
edit_uri: src/branch/main/mkdocs/docs
edit_uri: src/branch/v2/mkdocs/docs
# Theme
theme:
@ -39,9 +39,14 @@ theme:
- content.action.view
- content.code.annotate
- content.code.copy
- content.code.select
- content.tabs.link
- content.tooltips
- navigation.footer
- navigation.indexes
- navigation.instant
- navigation.instant.prefetch
- navigation.instant.progress
- navigation.path
- navigation.prune
- navigation.tabs
@ -66,6 +71,8 @@ plugins:
post_date_format: medium
archive_name: Archive
categories_name: Categories
authors: true
authors_file: blog/.authors.yml
- tags
# Extra CSS and JS
@ -125,7 +132,9 @@ markdown_extensions:
- pymdownx.keys
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.snippets
- pymdownx.snippets:
auto_append:
- includes/abbreviations.md
- pymdownx.superfences:
custom_fences:
- name: mermaid
@ -142,9 +151,22 @@ markdown_extensions:
extra:
analytics:
provider: custom
consent:
title: Cookie consent
description: >
We use cookies to recognize your repeated visits and preferences,
as well as to measure the effectiveness of our documentation.
With your consent, you help us improve.
actions:
- accept
- reject
- manage
generator: false
status:
new: Recently added
deprecated: Legacy
social:
- icon: fontawesome/brands/github
- icon: fontawesome/solid/code-branch
link: https://gitea.bnkops.com/admin
name: Gitea Repository
- icon: fontawesome/solid/paper-plane
@ -153,7 +175,7 @@ extra:
# Copyright
copyright: >
Copyright &copy; 2024 The Bunker Operations
Copyright &copy; 20242026 The Bunker Operations
<a href="#__consent">Change cookie settings</a>
# Navigation