Helpdesk Reference Public

Web Pages & Help Center

The standalone Web Pages pillar with public per-slug rendering and short-link sharing, plus the public CMS-themed Help Center landing.

Guide version: r1 Module version: 0.1.0 Updated: 2026-07-22 Estimated time: 14 min 1 views

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.

KB / Page Manager HR Admin Support Agent Developer Public / Guest Visitor

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.

Admin controller
Pages
Public controller
About (GuestController)
Model / table
Pages_modelpages
Admin route
/pages
Public route
/about/{slug}
Master toggle
module_page
Base permit
page
RBAC group
Web Pages
Access gate The pillar is reachable only when the module_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:

FieldColumnNotes
TitletitleRequired. Shown in the admin list and used as the page heading.
ContentcontentWYSIWYG HTML body. Sent through encodeAjaxPostData() on submit and decoded server-side with decode_ajax_post_data().
SlugslugRequired. The public URL segment — the page renders at /about/{slug}. Must be unique (see the duplicate guard below).
StatusstatusDropdown of active / inactive.
Internal use onlyinternal_use_onlyWhen checked, the public renderer requires a logged-in user.
Visible to team members onlyvisible_to_team_members_onlySub-flag of internal use: restricts to staff users.
Visible to clients onlyvisible_to_clients_onlySub-flag of internal use: restricts to client users.

Status values

active inactive

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.

The internal-use flags are mutually exclusive In the modal, ticking Internal use only reveals the two visibility sub-flags. Checking Visible to team members only hides the Visible to clients only option and vice-versa — a page can be scoped to staff or clients, never both at once. Leaving both unchecked (but internal-use ticked) means any logged-in user may view it.

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.

Slugs must be unique On save, 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.
SOP — Create & publish a company reference 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.

Request/about/{slug}
LookupPages_model by slug
Gateinternal-use checks
Renderpublic shell

Behaviour of the public render:

  • No slug → 404. Calling /about with an empty slug triggers show_404().
  • Public shell. The view about/index outputs the page body (via nl2br()) 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_only is set, a guest (no logged-in user) receives show_404(). For non-admin logged-in users, a team-members-only page 404s anyone whose user_type is not staff, and a clients-only page 404s anyone whose user_type is not client. Admins bypass the type restriction.
404, not 403, is intentional Restricted pages return a Not-Found rather than a Forbidden. This hides the very existence of a gated page from users who are not entitled to see it — the same "hide the resource" posture the customer portal uses.

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:

ToolKindWhat 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.

/s/{code}opaque token
Lookuptool_shortlinks (primary DB)
Decrypttenant company key
Switchtenant DB
Emit OG + renderthe 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.

Ownership boundary The 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.
SOP — Share a page publicly

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.

Tenant isolation on the public render path Because pages are served to guests without a session, the tenant must be resolved correctly and in isolation. The public slug renderer resolves within the current tenant request context; short links carry an encrypted tenant key that is decrypted and switched to before the page is loaded — the key is never exposed in the URL. Never expose a page publicly that contains data intended for another tenant, and treat any page you make public as internet-visible.

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).

Route
help-center
Controller
Help_center (GuestController)
CMS route key
core.helpcenter
Rendered by
Content module CMS theme

The 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().

Soft dependency on the CMS The Help Center landing is a soft dependency on the Content module — it loads the CMS resolver dynamically. If the Content/CMS module is off, or no override is assigned and activated for core.helpcenter, the route simply 404s gracefully. It is a target an admin opts into, not something that exists automatically.
Turning on the Help Center To make /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

URLTargetAudiencePurpose
/pagesPages/indexStaff (gated)Admin list of web pages.
pages/modal_formPages::modal_formStaff (gated)Create / edit modal (default dispatch).
pages/savePages::saveStaff (gated)Persist a page (default dispatch).
pages/deletePages::deleteStaff (gated)Soft-delete / undo (default dispatch).
pages/list_dataPages::list_dataStaff (gated)DataTable JSON source (default dispatch).
/about/{slug}About/index/$1Guest / publicPublic per-slug page render.
/s/{code}Tools_shortlink/resolve (Tools module)Guest / publicShort-link resolver — public; the URL Shortener tool ships enabled (free).
help-centerhelp_center/indexGuest / publicCMS-themed Help Center landing.
Route dispatch note Only 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 keyLabelGroupGates
pageWeb PagesWeb PagesAccess to the pillar — required to open /pages at all (checked with the master module_page toggle).
page_createCreate PageWeb PagesShows the Add page button and authorises creating a page in the modal and on save.
page_updateUpdate PageWeb PagesAuthorises editing/saving an existing page and shows the edit icon on rows.
page_deleteRemove PageWeb PagesAuthorises the soft-delete / undo action and shows the delete icon on rows.
Edit vs. save permit keys The modal-open path checks 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

Web Pages pillar standalone

The Helpdesk capability for authoring simple standalone pages — distinct from the CMS Content module. Backed by Pages, Pages_model and the pages table.

Slug

The unique URL segment that identifies a page publicly. The page renders at /about/{slug}.

Internal-use flags

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.

Public shell

The lightweight guest template About renders into — core public topbar (includes/public/topbar), no left menu.

Short link Tools-owned

A /s/{code} URL minted by the Tools module's URL Shortener that resolves to a page across tenants via an encrypted tenant key.

Help Center override

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.)

Was this guide helpful?

Report a content problem