Content Management
Content Management lets admins and content curators create, organize, schedule and publish learning resources (articles, videos, PDFs) and in‑app surfaces (banners, FAQs). This page documents the Web/Admin UX and the GraphQL contracts used by the app.
Roles & Permissions
- Admin: full access (CRUD, publish, archive)
- Content Curator: create/edit own content, request publish
- Viewer: read‑only
Enforced via server auth and UI feature flags. The list endpoints support created_by filtering.
Data Model
- Content:
id,title,type(ARTICLE | VIDEO | PDF | BANNER | FAQ),body,url,tags[],status(DRAFT | PUBLISHED | ARCHIVED),visibility(role scoping),publish_at,created_at,updated_at.
UI Flows
List, Search, Filter
const filtered = rows
.filter((r) => !type || r.type === type)
.filter((r) => !status || r.status === status)
.filter((r) => !query || r.title.toLowerCase().includes(query.toLowerCase()));
Create / Edit
- Author enters title, type, body/url, tags
- Optional schedule (publish_at) and visibility
- Save as DRAFT, then Publish
Media Upload (presigned)
- Request presigned URL
- Upload file to object store
- Save returned
publicUrlon the content record
GraphQL Contracts
import { gql } from '@apollo/client';
export const UPSERT_CONTENT = gql`
mutation UpsertContent($input: UpsertContentInput!) {
upsertContent(input: $input) {
success
message
data {
id
title
type
status
publish_at
url
}
}
}
`;
export const SET_PUBLISH = gql`
mutation SetPublish($input: SetPublishInput!) {
setPublish(input: $input) {
success
message
}
}
`;
export const PRESIGN_UPLOAD = gql`
mutation PresignUpload($input: PresignUploadInput!) {
presignUpload(input: $input) {
success
url
headers
publicUrl
}
}
`;
export const LIST_CONTENTS = gql`
query Contents($filter: ContentFilter!) {
contents(filter: $filter) {
id
title
type
status
updated_at
}
}
`;
Helper:
async function uploadWithPresigned(client: any, file: File) {
const { data } = await client.mutate({
mutation: PRESIGN_UPLOAD,
variables: { input: { filename: file.name, type: file.type } },
});
const info = data?.presignUpload;
if (!info?.success) throw new Error('Failed to presign');
await fetch(info.url, { method: 'PUT', headers: info.headers, body: file });
return info.publicUrl as string;
}
Validation & Rules
- Title: required, 3–120 chars
- ARTICLE body or external URL required depending on type
- VIDEO/PDF must have
url(uploaded or external) publish_atcannot be in the past when scheduling
Troubleshooting
- "Upload succeeded but file not visible": ensure you save the returned
publicUrlon the record - "Video does not play": check MIME type and CORS on the object store
- "Scheduled item didn’t publish": verify server cron/worker and
publish_attimezone
Exports
const csv = toCsv(rows);
downloadCsv('content.csv', csv);
Mapping Table
| Action | Operation |
|---|---|
| List/Filter | contents(filter) |
| Upsert | upsertContent(input) |
| Publish toggle | setPublish(input) |
| Presign upload | presignUpload(input) |