Tricycle – A Web Application Kernel with an Integrated CMS

Tricycle is a web application kernel with an integrated content management system, written in Perl on top of Mojolicious and first put into production in 2013. Its goal was to simplify custom web application development as much as possible, and make it as easy as riding a tricycle.

Custom web projects faced a gap between two kinds of tools. Application frameworks offered the freedom to build any domain logic but had no ready content management. Content management systems offered structure and editing but resisted deep customization. Tricycle was built to close that gap from one foundation.

The architecture separates the result into three logical layers: the reusable kernel, the CMS capabilities shipped with it, and the domain application created for a particular project. The kernel supplies the machinery for routing, page types, plugins, themes, database access, and reusable interface components. The CMS uses that same machinery to provide the site tree, users, media, administration, and live content editing. The developer then adds the page types, plugins, templates, data, and rules that make the result a particular application.

One idea runs through all of these layers: the site is a single tree of typed nodes, and the application runs against that tree. A node has an address, a place in the hierarchy, visibility and access rules, and a type that selects the controller responsible for its behavior. The same node can therefore represent a conventional content page, a product, a document, a report, a gallery, or an interactive visualization. Routing, navigation, permissions, editing, and domain behavior all resolve through the same model.

This foundation was used for applications of very different kinds: content sites edited directly on the live page, an e-commerce catalog with faceted filtering and price history, a residents’ portal with voting and financial reporting, and a self-service BI reporting tool that ran saved SQL against multiple databases. Each remained custom software: Tricycle supplied the recurring infrastructure, while the project defined its own domain.

A Site Runs on One Tree

The site tree is the application’s structural model. Each node occupies a place in the hierarchy, contributes a segment to its public URL, specifies the access level required to reach it, and has a page type that determines how it is handled. From the tree, Tricycle derives complete URLs, menus, breadcrumbs, active navigation states, and request dispatch.

The page type connects content structure to executable behavior. A type such as gallery, product, document, or report maps to a controller on disk. Both the runtime dispatcher and the tree editor discover the capability from that file: the dispatcher invokes it, while the editor exposes it as an available page type. A developer can introduce a new kind of node in one place, and any number of nodes can use it with different positions, addresses, content, and configuration.

The tree serves as a shared execution model for the framework, the CMS, and the domain application. General behavior can be implemented once against this model, while each application adds the behavior specific to its own node types.

Page Types and Plugins Define the Application

Page types define the units of domain behavior. A controller is the executable container for one kind of entity or application capability. It receives the common request context prepared by the kernel: the current node, hierarchy, URL fragments, access state, variables, title, and other structural data. The behavior specific to that kind of node remains concentrated in the controller. At its lightest, a page type needs no controller at all – just a template named after the node.

Plugins package capabilities for reuse across page types and applications. A plugin can register helpers, routes, templates, public assets, and data operations from a single self-contained directory. Copying or enabling it brings the capability and its supporting resources into another project.

Some modules deliberately combine both roles. For example, a gallery module can handle gallery pages as a controller while also registering a helper that embeds a gallery elsewhere. The module becomes the home of the entity and its complete lifecycle: request handling, integration, rendering, templates, and assets can evolve together.

This organization keeps each capability cohesive without forcing the application to scatter it across unrelated controllers, helpers, templates, and registration code.

Routing Is Derived from the Tree

Mojolicious expects routes to be declared when an application starts, while a CMS must allow an editor to add or move a page at runtime and have its new address work immediately. Tricycle bridges those two models without rebuilding the routing table.

The application registers one general route whose internal shape is /:controller/:action/:category_id. A before_routes hook receives the human-readable URL first, resolves it against the tree, loads the matching node, and rewrites the request into that internal form. A request such as /catalog/generators/model-x may therefore become /product/get/42 before Mojolicious dispatches it. The controller name is not accepted directly from the browser: it comes from the page type stored on the resolved node.

One stable routing contract can consequently serve any number of public addresses. Creating a node makes a new route available; renaming or moving it changes the address; assigning another page type changes the behavior. The editor works with the structure of the site, while the router continues to operate on a small, fixed set of contracts.

Tricycle applies the same dispatch model to entity operations. For resource-oriented operations, GET, POST, PUT, and DELETE map to corresponding controller actions, giving each page type a consistent REST-style CRUD interface. Operations that do not fit CRUD can instead be expressed as named commands: the client sends an explicit action, which the routing hook maps to the corresponding controller method, providing an RPC-style interface through the same system. The client-side layer supports both approaches directly and can also intercept an ordinary HTML form, serialize its data, and submit it with the intended HTTP method, turning forms that are natively limited to GET and POST into REST-capable CRUD components.

Shared endpoints, such as text editing and media uploads, are not tied to a particular page. Before handling a request, Tricycle resolves the referring page through the same site tree and reconstructs its node, hierarchy, and access context. A single endpoint can therefore serve every page without duplicating page-specific routes.

The Database Knows Both Sides of Every Address

URL resolution is implemented partly inside MySQL. Stored functions translate a path into a node ID and reconstruct the current path for a node ID. The application can therefore resolve an incoming address with one database expression, while ordinary queries can return the current URL as though it were another computed field.

This is useful beyond routing. A query that loads menu items, photographs, documents, or search results can ask the database for their addresses directly, rather than returning IDs and rebuilding paths in application code afterward. The same functions can participate in filtering, joins, and ordering wherever the relationship between a node and its URL matters.

The design also keeps the public address separate from the internal identity. The node ID remains stable while its path is derived from its current position in the tree. Other parts of the system can refer to the identity and allow the URL to change around it.

Deep Hierarchies Without Recursive Queries

Tricycle stores two complementary representations of the hierarchy. The first is a materialized path containing the IDs of a node’s ancestors. It allows the application to recognize an entire subtree, build breadcrumbs, determine which menu branches are active, and answer ancestor or descendant questions without repeatedly walking parent links.

The second is a hierarchical sort path assembled from the ordering values of every ancestor. Sorting by this one value produces the entire tree in display order, including all levels, with a single ORDER BY.

When a node is moved to another parent, a recursive stored procedure updates the ancestry and depth of its descendants. After structural changes, the sort paths are recalculated. This gave the application efficient operations on deep trees at a time when MySQL did not yet provide recursive common table expressions.

Links Follow Nodes, Not Paths

A normal hyperlink stored in rich text becomes stale when its target page is renamed or moved. Tricycle avoids that by treating an internal link as a reference to a node rather than as a permanent string.

An editor still inserts an ordinary, readable URL in the WYSIWYG editor. When the text is saved, Tricycle resolves that URL and replaces it internally with a marker such as %cid:=42%. When the text is rendered, the marker is expanded back into the node’s current URL.

The stored content therefore follows the identity of the destination. Reorganizing the site changes the generated address but does not require rewriting every text block that referred to it. Editors never have to work with internal IDs, and developers do not need migration scripts for links embedded throughout the content.

Templates use the same principle. Instead of hardcoding /admin/login or another structural path, they can ask for the current URL of a node by its page type or stable name. Navigation written in code survives the same changes as navigation written by an editor.

The CMS Lives on the Page It Edits

Tricycle integrates content management into the live application. Authorized users edit content in the same pages, layouts, and surrounding context in which visitors experience it.

Text blocks open as in-place WYSIWYG editors exactly where they are displayed. Titles, descriptions, and other editable regions can be changed in position, while images can be added directly to galleries through drag-and-drop upload areas. The result of each change is visible immediately in its actual presentation context.

The same URL can also support different rendering modes according to the user’s access level. A visitor may see the normal gallery, product, or document page, while an authorized editor sees the same resource with an administrative template and management controls. Presentation and editing therefore remain two views of the same page rather than separate representations that can drift apart.

A dedicated administration area handles work that belongs outside an individual page, including management of the site tree, users, and global content. It complements the in-place editing model by providing structural control over the application as a whole.

Content Slots Are Part of the Template Contract

Editable content is exposed through named slots defined where it is used. A template may request the main text, a short description, a call to action, a footer block, or another project-specific type. The CMS stores values for those slots, but the template defines the contract and the surrounding presentation.

A slot can resolve content for the current node, another node, a global record, or the first available choice from a fallback list. This makes it possible to provide a site-wide default and override it only on selected pages. Access requirements, CSS classes, placeholders, callback behavior, and additional save data can be supplied by the same helper.

Large or developer-maintained text can also be loaded from files instead of the database. That content can be reviewed and versioned in Git while ordinary editorial content remains editable through the CMS. Both sources appear through the same rendering interface.

This approach keeps content flexible without turning every application into a universal page builder. Developers decide which slots make sense for a page type; editors fill them without needing to understand the controller or template underneath.

Content Can Contain Live Components

Tricycle allows editable content to include live application components rather than only static HTML. An editor can place a gallery, banner, table, another content slot, or a project-specific component directly inside a text block, while the component’s behavior remains implemented and maintained in application code.

This works because stored text is rendered as a Mojolicious inline template and can call the same helpers used by ordinary application templates. Editors control composition and placement; developers retain control over behavior.

Navigation Is Another View of the Tree

Menus and breadcrumbs are projections of the same nodes used by routing, so navigation remains synchronized with the site tree.

A single menu helper can build the primary menu, a submenu for the current section, navigation for any selected branch, or a menu positioned relative to the current node in the hierarchy. It filters entries by visibility and access, derives active states from the materialized path, and delegates the final markup to a theme template. The same navigation logic can therefore support different layouts without changes to controllers.

Themes Replace the Entire Presentation Layer

A Tricycle theme is a complete presentation-layer overlay rather than a collection of styles. Both template and static-file paths are resolved through the selected theme before falling back to the application and kernel defaults. A theme can therefore replace the entire rendered surface – layouts, menus, forms, page templates, administrative interfaces, stylesheets, scripts, and images – while inheriting every part it does not redefine.

The application’s page types, controllers, data, routing, and content remain independent of that presentation layer. Once a theme has been implemented, selecting it in configuration can switch the entire interface – including the UI framework it is built on – without modifying the kernel or domain code. The same application can therefore support substantially different designs and interaction models while retaining the same behavior and managed content.

Reusable components can declare their own scripts and styles through the once_include helper. If the same gallery or widget appears several times on a page, each requested asset is collected and emitted by the layout only once, allowing components to remain self-contained without producing duplicate resource tags.

Multiple Roots Within One Application

In its default mode, Tricycle treats the home page as the root of the site tree. Multihead mode removes that special assumption and allows several root nodes to coexist at the same level, each defining its own URL space, subtree, navigation context, and access boundary.

This allows areas such as the main site, administration, documentation, a campaign site, or a private portal to remain structurally independent without becoming separate applications. They continue to share the same page types, plugins, users, themes, content tools, configuration, and database layer.

Multihead therefore separates the application into first-class structural areas while preserving one runtime and one reusable foundation.

One Client Layer Coordinates Browser Interaction

Tricycle includes the compact AF AJAX client, which gives forms, links, and direct JavaScript calls a common interface to the routing model. It collects and submits request data using the operation selected by the caller, then expects a standard JSON envelope describing both the outcome and the action the browser should take.

The envelope may report success or failure and instruct the client to display a notification, reload the page, or redirect to another address. AF interprets those directives consistently, allowing controllers to describe routine outcomes without requiring custom JavaScript for each operation.

Together, the request and response sides of AF provide a common interaction layer for live editing, administrative controls, and application features. Server-side code remains responsible for validation, authorization, and deciding the next step; the browser applies that decision through a predictable protocol.

Database Access Without Hiding SQL

Tricycle deliberately keeps database code as ordinary SQL rather than replacing it with an ORM or a query language. DBWrapper sits transparently in front of DBI: AUTOLOAD forwards any DBI method and caches the generated proxy after its first use, while only methods that execute SQL pass through the transformation layer. Quoting, error handling, generated IDs, and other native DBI operations remain directly available.

Queries refer to tables and stored functions through a neutral prefix_ placeholder, which DBWrapper replaces at execution time with the project’s configured prefix. The same SQL can therefore be reused across installations, while multiple Tricycle projects can coexist in one database without naming conflicts. Repeated project-specific SQL expressions can also be defined once and referenced through x: substitutions, keeping common ordering and path logic centralized without obscuring the surrounding SQL.

When a caller needs more than ordinary DBI rows, the enriched select operation also returns execution metadata (timing, row count, and errors), column names and database column types, both the original query and an inspectable version with parameters applied, and an optional callback applied to each row. Developers can use that metadata for generic rendering, diagnostics, profiling, exports, or project-specific data processing; SimpleTable is one example of a component built on that flexibility.

Debugging follows the same lightweight approach. Adding ->log to one database call prints that query with its parameters represented and then automatically disables itself. The parameter binder respects quoted SQL literals, so placeholders inside strings are not mistaken for bound values. The result is a data layer that adds portability, reuse, profiling, and targeted debugging while leaving the SQL itself readable and under the developer’s control.

Tables Are a Reusable Application Primitive

SimpleTable provides one rendering engine for tabular data regardless of its source. It can consume rows already prepared by the application, render an enriched result returned by DBWrapper, or accept a SQL query directly, execute it, discover the result structure, and produce a formatted table in one step. Developers can therefore choose between a concise declarative call and full control over data preparation without changing the presentation component.

From the supplied data, SimpleTable can discover columns, generate headers, format numeric values, calculate sums and running totals, control wrapping and truncation, and apply custom renderers to individual columns or as a fallback for all the rest. These renderers can also produce links, buttons, status indicators, and other interactive cell content, with project code attaching the required behavior. Particular cells, columns, footers, and summaries can be customized while retaining the shared behavior of the table.

The same component was used across ordinary lists, product comparisons, financial reports, voting results, and administrative diagnostics. Very different domain views could therefore share one table engine without being forced into the same presentation.

Media Is Processed Once and Then Served as Static

Thumbnail dimensions are declared in configuration and can be assigned names such as small, page, or product. Those names become helpers that templates and plugins can use without repeating dimensions throughout the application.

The first request for a particular image rendition creates the resized file on disk. Subsequent requests are served as ordinary static files, bypassing both Perl and image processing. The application therefore pays the transformation cost only when a rendition is actually needed.

The resizing routes accept only dimensions declared in configuration. A request cannot force the server to generate an arbitrary or excessively large image, so the same configuration defines the supported renditions and bounds the resources required to produce them.

Uploaded files receive a short random prefix while retaining their original filename. This prevents accidental overwrites when different uploads have the same name without making stored files unrecognizable.

The Tree Editor Is the Structural Control Room

The tree editor provides a visual interface for managing the same structure that drives routing, navigation, access control, and page behavior. Nodes can be created, renamed, hidden, disabled, reordered, and moved between branches from the same interface. Branch moves use drag and drop; collapsed destinations expand automatically, and the editor prevents a node from being placed inside its own subtree.

When an administrator edits a node, the interface previews its resulting URL while server-side validation checks the permitted format and ensures that the segment remains unique among siblings. Page type and access level are configured there as well. Nodes marked as system-level are protected from modification or deletion by ordinary administrators.

A single sort field supports three placement groups: positive values pin entries to the beginning, null values leave them in the normal middle group, and negative values pin them to the end. One shared ordering expression interprets all three cases without requiring separate flags or ordering mechanisms.

Structural changes preserve the derived state of the tree automatically. When a node changes parent, Tricycle updates the ancestry and depth of every descendant; other structural changes recalculate hierarchical sort paths and regenerate sitemap.xml as a static file. Administrators can reorganize the application without manually repairing internal links, descendant paths, navigation order, or published site structure.

Visibility and availability remain separate controls. A node can be hidden from menus while staying reachable by URL, or disabled entirely so that public requests receive a 404 while the node remains accessible in the editor. This provides a practical draft state without introducing a separate publication workflow.

Access Control and Page-Specific Settings

Access control is resolved together with the page itself. Each project can define its own ordered set of roles, and each node stores the role required to reach it. During request resolution, Tricycle compares the visitor’s role with the node’s requirement, making the access result available to the page, its templates, and navigation helpers.

Administration screens use the same role model. Role selectors adapt to the project configuration and do not offer levels above the current administrator’s own access. The same core can therefore support either a simple guest/admin model or a deeper sequence of project-specific roles.

A node can also carry project-specific key/value variables. They enter the template context, can be exposed to client-side scripts, and can populate HTML data attributes. This lets individual pages change labels, defaults, visual options, or small behavior switches without cloning controllers or templates.

Other page settings use the same approach. A node can define custom HTML for the document head, act as a transparent alias to another node, or control login return behavior. Protected pages can send anonymous visitors through authentication and return them to the original address, while direct login can send users to a role-appropriate landing page.

Security and Audit Are Built Into the Shared Paths

Tricycle gives applications a common security and audit baseline because critical operations pass through shared kernel mechanisms. Core actions write to a centralized audit log that records who acted, where the action happened, and what changed. Tree updates go further by storing a field-level before-and-after diff.

Administrative mutations are protected at the request level, not only in the interface. The tree editor uses a session token and a hidden honeypot field for write operations, while access checks are repeated on the server before changes are accepted. Hiding a button or disabling a control in the browser is never treated as authorization.

The same principle appears in other shared mechanisms. Thumbnail routes accept only configured dimensions, uploads receive a short prefix to avoid accidental collisions while preserving the original filename, and role and system-node controls limit what ordinary administrators can assign or modify.

What matters is that these protections live where the application actually changes state: page resolution, content editing, file processing, and structural administration. Applications built on Tricycle therefore inherit auditability and guardrails from the platform instead of rebuilding them separately in each feature.

Domain Applications Built on the Same Foundation

Tricycle separated what stayed common from what made each product specific. The kernel provided routing, dispatch, data access, shared helpers, media handling, and reusable components. The CMS layer provided the editable tree, content, navigation, permissions, and administration. A domain application then added its own page types, database model, workflows, and presentation rules on top of that foundation.

This made the same structure usable beyond ordinary content sites. In an e-commerce system, domain code added catalog specifications entered as plain text and parsed into structured data, generated filters, price history, promotions, comparison matrices, and exports. In a residents’ portal, it added projects, documents, apartment-linked reactions, voting, contributions, financial reports, and visual building views. In a BI/reporting tool, it added stored SQL reports and execution against configured database connections.

The important point is not the specific list of domains. The same foundation was not tied to any one product shape. It could become a catalog, an internal operations system, a knowledge base, a reporting workbench, or another application type. Product-specific behavior stayed in the domain layer rather than being mixed into the kernel or CMS. Tricycle supplied the reusable application machinery; the domain application supplied the business model that made each system different.

Overall

Tricycle closed the gap between framework freedom and CMS structure by making one model the shared foundation: a reusable application kernel with an integrated CMS, ready for each product to add its own domain model, workflows, and interface on top.

Tricycle was not the very first to bridge framework and CMS; Drupal was already there in 2013. But Tricycle bridged it from the opposite end. Drupal made you assemble a page from separate systems for content, menus, URLs, and routing, then bolt custom behavior on through scattered hooks – and its next major version pulled those systems even further apart. Tricycle did the reverse: it made one tree an executable application model. A node was not a menu item or a content page sitting in a separate system; it was the single unit that routing, navigation, permissions, and behavior all ran through. That turned the CMS from an administrative back office into the control surface for the structure the application actually ran on – while domain page types and plugins added business behavior without breaking out of the model. Where Drupal grew into a platform you configure, Tricycle stayed a kernel you build on – in a stack, Perl on Mojolicious, where nothing like it existed.

Much of what Tricycle did in 2013 later became familiar web-development vocabulary. Catch-all routing that selects behavior from a dynamic URL is now ordinary in modern application frameworks; Tricycle already resolved content-tree URLs into controllers in 2013. Live in-place editing became mainstream through block editors years later; Tricycle editors were already changing content directly on the rendered page. Live components embedded inside editorial text arrived with MDX in 2018; Tricycle had them in 2013. Internal links that survive renaming and reorganization still need add-ons in platforms such as WordPress; Tricycle made it automatic in 2013 by referencing the node rather than its path. Server-directed interaction later became a recognizable pattern in tools such as HTMX; Tricycle already used a shared response envelope where the server told the browser whether to reload, redirect, show a message, or report an error.

The database side followed the same pattern. Tricycle made URLs queryable inside SQL through stored functions before generated columns became familiar in mainstream MySQL and PostgreSQL, and made deep trees practical in MySQL before recursive common table expressions arrived there. Its saved SQL reports showed the same idea at application level: even a lightweight reporting workbench could live inside the CMS and access model instead of becoming a separate system.

These were not borrowed trends. They were production answers to production problems: pages had to route themselves, links had to survive tree reorganization, editors had to work on the live page, and domain applications needed reusable infrastructure instead of another custom CMS rebuild. The impressive part is not that Tricycle used the same tools later frameworks used. It did not. The impressive part is that it reached many of the same architectural answers earlier, with a 2013 Perl, Mojolicious, jQuery, and MySQL stack.

That was the real achievement of Tricycle: it turned the CMS from an editing tool beside the application into part of the application’s execution model. Once the kernel, CMS, and domain layer shared one tree, new products could reuse the same structural machinery and spend their effort on the business behavior that made them different.