Content Category
Overview
Content Category is the top-level (level 1) grouping of the content system. It organises content pages and acts as the root for Content Subcategories, which can nest up to four levels deep.
The screen is aimed at administrators and content teams who need to lay out the taxonomy before authoring actual content pages, since the categories created here become selectable options across several other screens.
Each category holds the following data:
| Field | Description |
|---|---|
| Name | Required, up to 255 characters |
| Slug | Required, up to 255 characters, restricted to lowercase letters, digits, and hyphens |
| Description | Optional, up to 1,000 characters |
| Thumbnail | An illustrative image for the category |
| Sort order | A non-negative integer controlling the position in listings |
| Status | Active / inactive, defaulting to active |
Key points to be aware of:
- List responses include each category's subcategories, so the subcategory count can be displayed immediately without an extra request.
- Deleting a category affects both its subcategories and any content pages bound to it, so a warning is always shown before confirmation.
- Beyond the standard list, this module serves a dropdown endpoint that many other screens depend on.
Business Flow
Managing the category list
- Opening
/content-categoryloads a paginated list with a search term and a status filter. - The table shows the category name (clickable to open the edit form), slug, subcategory count, sort order, status, last-updated date, and an action menu.
- Categories can be searched by keyword and filtered by status; running a search always returns to the first page.
- Clicking the subcategory count, or choosing "View subcategories" from the action menu, navigates to the Content Subcategory screen pre-filtered to the selected category.
- Deletion opens a confirmation dialog warning about the impact on linked subcategories and content, and refreshes the table once completed.
Creating and editing a category
- The form at
/content-category/formdistinguishes create from edit mode based on the category ID in the query string. - Edit mode loads the existing record into the form; if loading fails, an error is reported and the user is returned to the list.
- As the name is typed, a slug is generated automatically — lowercased, stripped of characters other than letters and digits, with spaces replaced by hyphens. Auto-generation only applies when creating a new category or when no slug exists yet, so slugs of already-published categories are never changed unintentionally.
- The remaining fields — description, sort order, and status — are filled in as needed.
- Saving creates or updates the category and returns to the list on success.
Reusing categories in other features
- Existing categories are exposed to other screens through a dropdown endpoint that returns display-label and identifier pairs.
- The content page create and edit forms use this list as the first level of classification, then load the selected category's subcategories.
- The Content Subcategory list and form use the same list to indicate which category a subcategory belongs to.
- The Content Links screen uses categories as a filter criterion for link lists.
- Deactivating a category keeps the record in the system rather than removing it, but it is worth checking beforehand that no active content still depends on it.
Key Screens & Components
List page (/content-category)
- Filter bar — a search field and a status selector, using the same standard control styling as other CMS list pages.
- Category table — name, slug, subcategory count (rendered as a clickable badge that jumps to the subcategory screen), sort order, status, and last-updated date, with pagination and responsive horizontal scrolling.
- Action menu — edit, view subcategories, and delete.
- Delete confirmation dialog — spells out the impact on related records before proceeding.
Primary file: src/app/content-category/page.tsx
Form page (/content-category/form)
- Name field — required, and the source for slug auto-generation.
- Slug field — required, validated to contain only lowercase letters, digits, and hyphens.
- Description field — a multi-line description of the category.
- Sort order field — a non-negative number.
- Status selector — defaults to active.
Field rules at a glance:
| Field | Rule |
|---|---|
| Name | Required, up to 255 characters |
| Slug | Required, up to 255 characters, lowercase letters, digits, and hyphens only |
| Description | Optional, up to 1,000 characters |
| Sort order | A number of 0 or greater |
| Status | Active or inactive, defaulting to active |
Primary file: src/app/content-category/form/page.tsx
API service
All calls live in src/services/content-category.service.ts under the content-categories base path.
| Capability | Endpoint |
|---|---|
| List categories | GET /content-categories |
| Dropdown options | GET /content-categories/dropdown |
| Get a single category | GET /content-categories/{id} |
| Create a category | POST /content-categories |
| Update a category | PATCH /content-categories/{id} |
| Delete a category | DELETE /content-categories/{id} |
The service also defines endpoints for grouped dropdown options and category statistics, though no screen currently consumes them.
Dependencies
- Permissions — this is a child menu gated by the
content-categorymodule permission, and it appears only when the user can also access the parentcontent-managementmenu; both are unlocked by the backendline-oamodule. - Content Subcategory — the child tree rooted at each category, and the destination of the "view subcategories" action.
- Content Management — consumes the category dropdown so authors can classify content pages while creating or editing them.
- Content Links — uses categories as a filter criterion for content link lists.
- Category cascader component — a shared control that pulls top-level categories from this module and extends them with hierarchical subcategories.
- Shared infrastructure — the CMS HTTP client (automatic bearer token and sign-out on expiry), the breadcrumb and side-menu system, shared table and filter constants, and the common translation namespaces.
Backend Details (CMS API)
The backend lives in internal/modules/contentcategory/. It is a small module that, beyond ordinary CRUD, acts as the category option provider for several other screens.
Required permissions
- Every route requires the shared authentication check (JWT), but is not wrapped in a module gate, and the declared policy references the
line-oamodule as metadata that is not yet enforced. - Per-menu access control therefore happens mainly on the front end; any signed-in user can still call the category endpoints directly.
- Data is always scoped to the LINE OA and organisation currently in use.
Endpoints the backend offers beyond what the UI calls
Alongside the list and dropdown endpoints the UI already uses, the API exposes two more:
| Endpoint | Purpose |
|---|---|
GET /api/content-categories/dropdown-group | Returns grouped options for dropdowns that need option group headings |
GET /api/content-categories/stats | Returns the content count per category |
Categories are also surfaced on a public route for readers, so they are not exclusive to the admin screens.
What to watch out for around deletion
- The
content_categorytable uses a plain deletion-timestamp column rather than the ORM's automatic soft-delete mechanism. Every read query must spell out the "not deleted" condition itself; miss it in one place and deleted categories reappear as options on other screens. - A deliberately preserved edge case — the update operation does not filter out already-deleted rows (retaining the previous system's behaviour). Updating by the id of a deleted category therefore still succeeds, even though that category appears in no listing.
- Deletion responds with a no-content success status (204) and does not return the deleted record.
- The backend runs no reference check before deleting. The warning about the impact on subcategories and content pages shown in the confirmation dialog comes from the front end, not from a server-side guard, so bound subcategories and pages can end up referencing a category that no longer exists.
What gets stored, and the side effects
- Data lives in the
content_categorytable, referencingcontent_subcategoryandcontent_page. - This module writes no cache and publishes no queue work, so adding or editing a category is reflected in other screens' dropdowns as soon as they reload.
- The list response embeds each category's subcategories, which removes the need for a second request to count them — but it also means the response grows with the number of subcategories.
A technical note
The fixed-name routes dropdown, dropdown-group, and stats are registered before the route that takes a category id, so the router resolves them correctly; otherwise dropdown would be interpreted as a category id. This ordering must be preserved when adding new endpoints to the module.