Secure, multilingual CMS
built without compromise
UV Web CMS is a full-featured, Flask-based content management system with granular RBAC, dynamic routing, per-page CSS/JS injection, SEO automation, and a plugin architecture - built from scratch for full ownership and control.
Get in touch Admin panelWhy build a CMS instead of using one?
Third-party CMS platforms impose trade-offs: plugin ecosystems that age badly, security models you cannot audit, content APIs locked behind vendor contracts, and deployment constraints tied to their infrastructure.
UV Web CMS was built to give organisations complete ownership. Pages, routes, permissions, content, CSS, JS, SEO settings, and analytics configuration all live in your database, served by code you control, deployed wherever you need it.
The RBAC model goes beyond simple admin/editor splits - 68 individual permissions cover every content operation, making it possible to give precisely the right access to each user or role without overreach.
What the CMS manages
Dynamic pages and routing
New pages activate immediately from the database - no restart, no redeploy. Routes, navbar visibility, and page templates are all database-driven.
RBAC with 68 permissions
Users, roles, and permissions modelled separately with full many-to-many resolution. Page-level access grants per user and per role. Admin hierarchy bypasses all permission checks.
Multilingual content
Content items stored per language with automatic fallback chain (fr-CA to fr to en). Language switcher and cookie persistence. Currently EN and ET.
Per-page CSS and JS
Stylesheets and scripts stored in the database, assigned to specific pages with load order. Base template injects them dynamically - no build step required.
SEO automation
Keyword extraction, auto meta generation, per-page scoring, bulk optimisation, and audit history. Complete Open Graph and Twitter Card support built in.
Google Analytics 4
GDPR-compliant GA4 integration with gtag.js event tracking. Analytics dashboard in admin panel. Optional GA Data API integration for in-platform reporting.
Plugin and module system
Hook-based extension architecture with a module registry. Add new capabilities - integrations, custom processors, rendering plugins - without touching core code.
Content export API
REST API for content export and import with API key authentication. Enables content synchronisation between environments and external integrations.
Enterprise security
CSRF protection via Flask-WTF, XSS prevention, SQL injection protection via ORM, PBKDF2 password hashing, and session security baked in throughout.
68 permissions, not just admin/editor
Each permission covers a specific resource and action combination. Assign them individually to roles, or use the admin hierarchy to bypass all checks for trusted operators.
Page-level grants can be applied per-user or per-role independently of global permissions, enabling scenarios like giving a single editor access to one page without touching any other configuration.
Technology stack
| Layer | Technology |
|---|---|
| Web Framework | Flask 2.x |
| ORM / Migrations | SQLAlchemy + Flask-Migrate (Alembic) |
| Authentication | Flask-Login, PBKDF2 hashing |
| Forms / CSRF | Flask-WTF |
| Rich Text Editor | CKEditor 5 (open source) |
| Frontend | Bootstrap 5, Vanilla JS |
| SEO Parsing | BeautifulSoup4 |
| Analytics | Google Analytics 4 (gtag.js) |
| Testing | pytest, pytest-flask, selenium |
| Database (dev) | SQLite |
| Database (prod) | PostgreSQL (planned) |
| App Server (prod) | Gunicorn + Nginx (planned) |
Feature status
What changes without a release. What does not.
Every page rendered by UV Web CMS is composed of two independent layers with different change characteristics. Understanding this boundary is what distinguishes a CMS from a templated website.
Infrastructure layer - 100% DB-driven
Every page's structural metadata is resolved from the database on every request. Changes take effect immediately:
- Route and URL -
Page.route: URL changes instantly, no deploy - Active / inactive -
Page.is_active: take a page offline in one toggle - Navbar label and position -
Page.name,Page.nav_order,Page.show_in_navbar - CSS stylesheets -
CSSStylerecords assigned to a page; injected into<head>, full visual theming without touching a file - JavaScript behaviors -
JSScriptrecords, delivered to header or footer per-page; analytics, interactivity, A/B testing - SEO metadata -
PageSEO: meta title, meta description, keywords, Open Graph title/description, canonical URL, schema.org type - Google Analytics tracking -
SEOSettings.google_analytics_id: GA4 ID toggled from admin - Page title (browser tab) -
Page.title(available; product pages currently override with{% block title %})
Content layer - 0% DB-driven on product pages (currently)
The visible body sections of all four product pages (wormwood.html, deltaprism.html, nexus.html, uvwebcms.html) are hardcoded in template files. Changing any of the following requires editing the .html file and redeploying:
- Hero headline, subheadline, and CTA button text
- Metric numbers and labels (36K ops/sec, 294 rules, etc.)
- Section body copy (problem, solution, use-case paragraphs)
- Feature card content and capability descriptions
- Technology stack table rows
- Status badge labels (Running, Roadmap, Planned)
- Progress bar percentages
- Navbar dropdown product links (hardcoded in
base.html) - Section layout and grid structure
The gap and the path
The PageContent model already exists - keyed HTML blocks, per-language, with a many-to-many relationship to pages. The admin panel can create and edit these blocks. The content export/import API already serves them. The only missing piece is template wiring: product templates referencing page.contents instead of hardcoded text.
With template wiring in place, editorial copy changes (headlines, descriptions, body paragraphs, CTA text) would be manageable from the admin panel without a code release. Structural changes (section layout, card count, table columns) would continue to require a release - approximately 30% of visible surface area that is purely HTML structure.
Note on flat vs. subdirectory templates: Pages using a flat template (no / in the template path) are rendered via render_page_content(page.key) in base.html, which reads directly from PageContent blocks - so content is fully DB-driven for those pages today. Product pages use subdirectory templates (products/wormwood.html) and fall through to {% block content %}, bypassing the content block system entirely.
Need a CMS you actually own?
UV Web CMS is available for deployment on your infrastructure. Get in touch to discuss licensing, customisation, or hosting.
Contact the team