Web Pages & Help Center
Two related public-facing surfaces of the Helpdesk module: the standalone Web Pages pillar — a lightweight capability for company reference and documentation pages, each rendered publicly at its own slug — and the branded Help Center landing that ties the public knowledge experience together through the CMS theme.
What the Web Pages pillar is
The Web Pages pillar is a self-contained way to author simple, standalone HTML pages — think an "About us", a returns policy, a vendor onboarding sheet, an internal reference note, or a public documentation page. It is deliberately distinct from the CMS Content module (modules/Content/): Web Pages carries its own controller, model, table, permissions and public renderer, and is intended for quick reference/documentation content rather than a full themed website builder.
Each page is a row in the per-tenant pages table with a WYSIWYG body, a URL slug, a status, and three visibility flags. Staff manage pages from an admin list; a separate guest-facing controller renders any published page at a public URL derived from its slug.
PagesAbout (GuestController)Pages_model → pages/pages/about/{slug}module_pagepagemodule_page sub-toggle is enabled and the signed-in user's permission grants the page permit. The Pages constructor enforces both with with_module("page", "redirect") then with_permission("page", "redirect") — a user missing either is redirected away rather than shown the list.Page fields, statuses & visibility
A page is created and edited through one modal form (pages/modal_form). The saved payload maps to these columns on the pages table:
| Field | Column | Notes |
|---|---|---|
| Title | title | Required. Shown in the admin list and used as the page heading. |
| Content | content | WYSIWYG HTML body. Sent through encodeAjaxPostData() on submit and decoded server-side with decode_ajax_post_data(). |
| Slug | slug | Required. The public URL segment — the page renders at /about/{slug}. Must be unique (see the duplicate guard below). |
| Status | status | Dropdown of active / inactive. |
| Internal use only | internal_use_only | When checked, the public renderer requires a logged-in user. |
| Visible to team members only | visible_to_team_members_only | Sub-flag of internal use: restricts to staff users. |
| Visible to clients only | visible_to_clients_only | Sub-flag of internal use: restricts to client users. |
Status values
Status is stored verbatim as active or inactive and displayed in the admin list via lang($data->status). The status flag is your editorial control over whether a page is considered live.
Managing pages (admin)
The admin list lives at /pages and is powered by the appTable plugin, loading rows from pages/list_data. Each row shows the Title, the clickable public URL (/about/{slug}, opens in a new tab), the Status, and per-row action icons (edit / delete) that appear only when your permissions allow.
Open the list
Navigate to /pages. If page_create is granted, an Add page button appears in the panel header.
Add or edit
Click Add page (or the pencil icon on a row) to load pages/modal_form. Creating requires page_create; editing an existing row requires page_edit.
Fill the form
Enter Title, author the body in the WYSIWYG editor, set a Slug, choose Status, and optionally set the internal-use / visibility flags.
Save
Submitting posts to pages/save. On success the row is inserted or updated in the table without a page reload; a duplicate slug is rejected.
Delete (soft)
The delete icon posts to pages/delete (requires page_delete). This is a soft delete — an undo path restores the row.
Pages_model::is_slug_exists() checks the slug against every non-deleted page. If the slug is already taken by a different record, the save is rejected with the page_url_cant_duplicate message and nothing is written. Pick a distinct, URL-safe slug per page.1. Confirm access
Ensure module_page is enabled in Manage Modules and your role has page + page_create.
2. Draft the page
At /pages, click Add page. Give it a clear Title and author the body.
3. Choose a slug
Use a short, lowercase, hyphenated slug (e.g. vendor-onboarding). It becomes the public URL segment.
4. Set visibility
Leave internal-use unchecked for a fully public page; or tick it (and a sub-flag) to gate the page to logged-in staff or clients.
5. Set status active
Choose active so the page is considered live, then Save.
6. Verify publicly
Click the URL in the row (or open /about/{slug} in a private window) to confirm the public render.
Public rendering — /about/{slug}
Every page has a public face served by the guest-facing About controller (which extends App\Core\GuestController, so it does not require a login by default). The route about/(:any) maps the trailing segment to About/index/$1, and About::index($slug) loads the matching page by slug.
Behaviour of the public render:
- No slug → 404. Calling
/aboutwith an empty slug triggersshow_404(). - Public shell. The view
about/indexoutputs the page body (vianl2br()) inside a lightweight public template — the controller sets the core public topbar (includes/public/topbar; CMS theme chrome injected when the Content module is active) and disables the left menu, so the page renders as a standalone public surface rather than inside the staff app chrome. - Internal-use enforcement. If
internal_use_onlyis set, a guest (no logged-in user) receivesshow_404(). For non-admin logged-in users, a team-members-only page 404s anyone whoseuser_typeis notstaff, and a clients-only page 404s anyone whoseuser_typeis notclient. Admins bypass the type restriction.
Public short-link sharing
A page can be shared as a public short link that a logged-out visitor can open. This capability is owned by the Tools Hub module (modules/Tools/), not by Helpdesk — the Web Pages pillar simply consumes it. There are two tools, delivered together in Phase 1 of the module plan:
| Tool | Kind | What it does |
|---|---|---|
| Sharable Link Generator | enabled | The standard sharer. Reuses the existing Tools Hub shareable-result generator (the tool_results store) to produce a shareable link with an Open Graph preview. This is the default sharer for distributing a page link with a preview. |
| URL Shortener | enabled · free | Mints random 7-character base-56 /s/{code} short links (an unambiguous alphabet that omits 0/O/1/l/I) backed by the primary-DB tool_shortlinks table and resolved by the Tools-owned Tools_shortlink controller. In the current source it ships active and public (free); an operator can disable the tool, and the /s/{code} resolver is always public regardless. |
Anatomy of a short link
The public form is https://erpat.app/s/{code}, where {code} is an opaque 7-character base-56 token (a CSPRNG-random code over an unambiguous alphabet that omits 0/O/1/l/I). The URL carries only that code — no tenant slug, no page id, nothing decryptable from the path. Behind the code, the tool_shortlinks row stores the tenant's company key encrypted at rest, so the resolver can locate the right tenant database before any tenant context exists, then load the target page.
On resolve, the short link emits the stored Open Graph tags (title / description / image) so social and chat unfurls show the intended preview, increments its hits counter, and renders the target page in a public shell. A missing, disabled, or expired code returns 404 to hide its existence.
tool_shortlinks table, its migration (tracked in migrations_tools), the Tool_shortlink_model, the SSRF-guarded OG fetcher, and the /s/{code} resolver all live in the Tools module. Helpdesk's Web Pages pillar consumes that service — it does not implement or own short links.1. Publish the page
Create the page and set it active. For a truly public share, leave internal_use_only unchecked so guests can open it.
2. Generate the link
Mint a shareable link with the Tools Hub Sharable Link Generator (the OG preview is drawn from the page's own meta). A one-click Share button in the Pages list is planned; today the link is minted through the Tools Hub tool.
3. (Optional) Shorten it
Use the Tools Hub URL Shortener to mint a target_type=page short link at /s/{code} with auto-captured, editable OG metadata.
4. Distribute
Send the link. Recipients open it with no login; the resolver locates the tenant, emits the preview, and renders the page.
The Help Center landing
The Help Center is the public front door for the knowledge experience — a branded landing at the route help-center. It is deliberately separate from the staff-facing, permission-gated Help / Knowledge Base management screens: this route is a public, guest-accessible page whose content is owned by the CMS (the Content module).
help-centerHelp_center (GuestController)core.helpcenterThe controller has a single job: if the Content module is loadable and a CMS override is active for core.helpcenter, it renders that assignment through the active CMS theme and returns. It even opts out of IP restriction (skipIpRestriction = true), the way other public marketing/info pages do, so the public landing is never IP-gated.
Guest hits /help-center
The Help_center controller runs as a guest surface.
Try the CMS override
It attempts to load the Content module's CmsRouteResolver, checks it can override core.helpcenter, and renders the assignment.
Render through the theme
If the override is active, the branded CMS landing is delivered through the active theme — header, footer, and chrome included.
Otherwise 404
Until an admin creates and activates the override (Settings → Sync Public Pages → Route Assignments), there is no legacy public Help page to fall back to, so the route returns show_404().
core.helpcenter, the route simply 404s gracefully. It is a target an admin opts into, not something that exists automatically./help-center live, an admin runs Settings → Sync Public Pages (the CmsStaticPageSyncService), which creates and assigns a CMS landing page for core.helpcenter, then activates the override from the Route Assignments tab. From then on the route renders that page through the CMS theme.Routes & URLs
| URL | Target | Audience | Purpose |
|---|---|---|---|
/pages | Pages/index | Staff (gated) | Admin list of web pages. |
pages/modal_form | Pages::modal_form | Staff (gated) | Create / edit modal (default dispatch). |
pages/save | Pages::save | Staff (gated) | Persist a page (default dispatch). |
pages/delete | Pages::delete | Staff (gated) | Soft-delete / undo (default dispatch). |
pages/list_data | Pages::list_data | Staff (gated) | DataTable JSON source (default dispatch). |
/about/{slug} | About/index/$1 | Guest / public | Public per-slug page render. |
/s/{code} | Tools_shortlink/resolve (Tools module) | Guest / public | Short-link resolver — public; the URL Shortener tool ships enabled (free). |
help-center | help_center/index | Guest / public | CMS-themed Help Center landing. |
pages, about/(:any) and help-center are pinned explicitly in the module's config/routes.php. The other Pages methods (modal_form, save, delete, list_data) resolve through App_Router default segment routing — no explicit route needed.Permissions (RBAC)
Web Pages permissions live in the module's own permission surface and appear in the Roles editor under the Web Pages group when module_helpdesk is enabled. The page base permit uses an Enabled toggle; the CRUD children gate individual actions.
| Permission key | Label | Group | Gates |
|---|---|---|---|
page | Web Pages | Web Pages | Access to the pillar — required to open /pages at all (checked with the master module_page toggle). |
page_create | Create Page | Web Pages | Shows the Add page button and authorises creating a page in the modal and on save. |
page_update | Update Page | Web Pages | Authorises editing/saving an existing page and shows the edit icon on rows. |
page_delete | Remove Page | Web Pages | Authorises the soft-delete / undo action and shows the delete icon on rows. |
page_edit when an id is present, while the actual save path checks page_update. In practice a role that manages pages should carry both page and the CRUD permits it needs (page_create / page_update / page_delete). See the full Permissions Reference for how these keys sit alongside the other Helpdesk pillars.The public surfaces are intentionally not permission-gated in the usual sense: /about/{slug} is a guest controller (with the internal-use flags providing per-page gating), and /help-center is a guest landing whose visibility is controlled by the CMS override, not a page* permit.
Use cases
Public "About" / policy page
A returns policy, privacy notice, or company overview at a clean /about/{slug} URL, shareable with anyone.
Staff-only reference
An internal SOP tick Internal use only + Team members only so only logged-in staff can read it.
Client-only notice
Onboarding or terms scoped to clients with Visible to clients only, hidden from staff and guests.
Shareable link
Mint a short/shareable link (Tools module) to distribute a page with a proper OG preview in chat and social.
Public Help Center front door
Activate the CMS override for core.helpcenter to give customers a branded landing that ties the public KB together.
Terminology
The Helpdesk capability for authoring simple standalone pages — distinct from the CMS Content module. Backed by Pages, Pages_model and the pages table.
The unique URL segment that identifies a page publicly. The page renders at /about/{slug}.
The trio internal_use_only / visible_to_team_members_only / visible_to_clients_only that gate the public renderer to logged-in staff or clients.
The lightweight guest template About renders into — core public topbar (includes/public/topbar), no left menu.
A /s/{code} URL minted by the Tools module's URL Shortener that resolves to a page across tenants via an encrypted tenant key.
The CMS route assignment for core.helpcenter that, once activated, makes /help-center render the branded landing through the theme.
Troubleshooting & FAQ
The Pages constructor gates on both module_page (the toggle) and the page permit. If either is missing you are redirected. Enable module_page in Manage Modules and grant page to your role.
Another non-deleted page already uses that slug. Change the slug to something unique. The check is enforced by Pages_model::is_slug_exists() and blocks the save entirely.
Most likely the page is marked Internal use only, which requires a logged-in user. It will also 404 if a guest hits /about with no slug, or if the visibility sub-flag excludes the visitor's user type. Uncheck internal-use for a fully public page.
Non-admin users are filtered by user_type: a team-members-only page 404s non-staff users, a clients-only page 404s non-client users. Admins bypass the type restriction. Check the page's visibility flags against the user's type.
The landing only exists once a CMS override for core.helpcenter is created and activated (Settings → Sync Public Pages → Route Assignments), and the Content/CMS module is enabled. Until then there is no fallback and the route 404s by design.
Yes. The standard Sharable Link Generator produces a shareable link with an OG preview, and the URL Shortener that mints /s/{code} short links also ships active and public (free) in the current source — so you can mint a page short link now. (An operator can disable the tool, and a disabled, missing, or expired code returns 404.)