Admin (CMS)
Claude skill atlas-admin · for Frontend, Admin user, Editor
How the Atlas of Today's World administration works and how to extend it - admin layout and menu (admin-nav.ts, adminRoutes), the permission model (roles x sections x v/c/e/d, the database decides), page structure, forms, destructive actions (confirm vs undo), drafts, revisions, previews and scheduled publishing, the Maps & charts editor, plus a short guide for editors. Use when building or changing an admin screen, menu item, permission check, admin form or workflow, or when an editor asks how to do something in the admin.
The admin at /admin is where the invited team edits everything on the site without a deploy. Every screen looks and behaves the same: header, one table or form pattern, the same confirmation and status messages. The database decides what each person may do; the UI only reflects it.
Editors: jump to For editors.
Before you start
- Related skills:
atlas-datatable(every list),atlas-frontend(form mechanics),atlas-backend(Server Actions, RLS, RPCs, cache tags),atlas-security(sessions, 2FA),atlas-accessibility,atlas-content(what to write),atlas-maps-data(what makes a good map or chart). - Look at a similar existing screen first:
src/app/admin/approvals/page.tsx(list),src/app/admin/redirects/(list + form),src/app/admin/content/[id]/page.tsx(tabs, workflow, revisions, visuals).
Rules
Layout and menu
src/app/admin/layout.tsxisforce-dynamicwith private (noindex) metadata. It signs the person in, asks for the second factor where the role needs it (MfaGate), shows a "No access" page to anyone who is not staff (readers), then renders the header, theAdminNavsidebar (15 rem) and<main id="content">across the full width. Forms and detail pages use that whole width; do not addmax-w-*caps to admin screens.- A new screen needs three things: a URL in
adminRoutes(src/config/routes.ts:records()for a page per record,editable()when records are also created there, a plain string for a single page), a menu item inADMIN_NAV(src/config/admin-nav.ts: label, section or sections, lucide icon), andexport const metadata = { title: "…" }(the layout adds "— Atlas administration"). - The menu shows an item only when the person has
vin one of its sections (navVisible).section: null= every team member. - Admin links use
next/linkwithadminRoutes.*; ESLint bans hand-written"/admin…". - Admin UI text is English, written in the component.
Permissions
- Model: roles × 11 sections (
SECTIONSinsrc/config/sections.ts: news, approvals, areas, regions, layers, appearance, specials, site, users, members, permissions) × actionsvview,ccreate,eedit,ddelete. Roles also carry whose articles they edit (news_scope) and which they approve (approval_scope); labels infeatures/roles/labels.ts. - Every admin page starts with the same check, then uses
can()only to show or hide controls:
// src/app/admin/redirects/page.tsx: any one of the sections is enough to view.
const access = await sectionAccess(["news", "site"]);
if (!access) return <NoAccess />;
const canDelete = can(access.permissions, "news", "d");- UI checks are UX only. RLS policies and RPCs decide. Every Server Action runs under the
person's session; a write that RLS refuses touches zero rows and
affected()turns that into a message (atlas-backend). - Hide controls a role can never use. When a role may see but not change something, wrap it in
ReadOnly(readOnly,reason), which disables the fieldset and says why. - Owner decisions: anyone invited into the team may edit content; do not add new approval steps,
per-item locks or extra sign-offs. Readers register as
readerand never see the admin. The second factor is required only for the admin and permission-admin roles. - A new section touches the DB CHECK,
my_permissions(),config/sections.tsandfeatures/roles/labels.tsin one PR (migration rules:atlas-backend).
Page structure
- Top:
PageHeaderwithicon={navIcon(adminRoutes.x)}, the menu label astitle, a one-sentenceleadsaying what the screen is for, and the main action ("Add redirect") inactions. - Lists:
DataTablewithRowActions(atlas-datatable). No other list or table. - Record detail: its own page (
adminRoutes.x(id)). A large record splits into tabs that are links with?tab=andaria-current(seeDossierTabsinadmin/content/[id]/page.tsx), so every tab has a URL. - Forms live in
features/<domain>/components/*Form(s).tsxand followatlas-frontend: each section is a bordered box (rounded-2xl border p-5) with its ownh2,SubmitButton, andActionStatusnext to it. - Button labels name the action and the object: "Save redirect", "Approve and publish", "Schedule publication". Pending labels end with "…" ("Saving…").
- After a save the action calls
updateTagfor the public cache andrevalidatePathfor the admin list (atlas-backend); the form shows "Saved." inActionStatus.
Destructive actions: confirm or undo
| Action | Pattern |
|---|---|
| Move up / down in a hand-ordered list | Instant, no dialog (moveActions); the opposite arrow undoes it |
| Unpublish, hide, withdraw, cancel schedule | ConfirmButton; reversible, so prefer these to delete |
| Delete, revoke, anything hard to reverse | ConfirmButton with variant="danger" (or quietDanger icon) |
- The dialog title names the record and the consequence ("Delete the redirect /news/old?"), the body explains what happens, the confirm button repeats the action ("Delete redirect"). Cancel is always there and comes first.
- Content state changes only through the workflow RPCs (
submit_entry,withdraw_entry,approve_entry,unpublish_entry, scheduling), never by writingstatusdirectly. - Do not add type-to-confirm or double dialogs for ordinary records.
Drafts, revisions, previews and scheduling
- Articles move planned → draft → pending → published (
ENTRY_STATUSES) throughEntryWorkflow. Shown withStatusBadge; who edited what withEditStamp. - Scheduled publishing:
scheduleEntry/unscheduleEntry; a database job publishes due articles. Show the time with its zone. Never raise the ISR revalidate interval; scheduled publishing relies on it. - Revisions:
RevisionList(last 30 versions, restore) andVersionDiff(lib/diff.ts) for articles;PageRevisionswith therestore_site_pageRPC for site pages. Both show who saved each version:staffNames()(lib/db/staff-names.ts, DBstaff_name, staff only) turnssaved_byinto a name. Use it for any other "by" column; never readprofilesfor names. - Unsaved article edits are backed up in the browser only (
useDraftBackup, every 5 s); saving to the server is always an explicit click. - Secret preview links:
PreviewShare(createPreviewLink), opening/preview/<token>without Share and without indexing. - Extend these mechanisms; do not build a second autosave, history or preview system.
Maps & charts editor
- One editor for every owner (region, global issue, topic, subtopic):
VisualsEditor(src/components/admin/VisualsEditor.tsx) = aDataTableof visuals (move, edit, delete) plusVisualFormbelow;?visual=<id>opens one in the form. - The page passes the Server Actions in (
VISUAL_ACTIONSfromfeatures/visuals/admin), pluscanEdit/canRemove/readOnlyReason. The database (can_edit_visual) has the final word. - The custom map builder (
MapVisualBuilder) loads lazily throughfeatures/visuals/map-builder-slot.ts(admin only) and keeps its own undo history; when the form is at least 56 rem wide (a container query, not the window) the map takes 7/10 of the width and the panel 3/10 (at least 20 rem), otherwise the panel goes under the map. Add new builder controls inside its panel, not as separate admin screens. - The view above the builder's map (
ViewStatus) says what readers will see and whether it is saved. A free view follows the map (pan, zoom, save); a countries or region framing stays until "Use this view". "Reset to saved view" brings back the framing of the stored visual. Points without a title are flagged at once and block the save with a message, never silently. - Visual kinds, embed providers, limits and what makes a good map or chart:
atlas-maps-data.
Checklist
- [ ] URL in
adminRoutes, item inADMIN_NAV, pagemetadata.title. - [ ]
sectionAccess+NoAccessat the top;can()only for showing controls; RLS covers the write. - [ ]
PageHeaderwith icon and lead; lists inDataTable; forms withActionForm+ActionStatus. - [ ] Destructive actions confirmed with a specific dialog; reversible options offered first.
- [ ] e2e: the screen renders for an allowed role and shows
NoAccessfor a forbidden one (tests/admin-permissions.spec.ts).
Anti-patterns
- Checking permissions only in the UI: anyone can call a Server Action directly.
forbidden()for "no access": useNoAccess(why:atlas-architecture).- A custom list with its own filters or a "⋯" menu instead of
DataTableandRowActions. - A confirmation that says "Are you sure?" with "OK": it names neither the item nor the result.
- A new approval gate or sign-off step: against the owner's trust model.
For editors
- Sign in at atlas-of-todays-world.vercel.app/admin. Admins and permission admins also confirm a 6-digit code from an authenticator app.
- You see only your sections. "You don't have permission for this section" means your role lacks it; ask a permission admin (Roles & permissions).
- Write a topic: New topic. Fill the Key points box on the Article tab and In short on the SEO & GEO tab first; they are what readers see before the long text. Then Subtopics, Timeline, Maps & charts, Learn more.
- Save often. Unsaved changes are kept only in this browser as a safety net.
- Publish: Submit for approval, or Approve and publish / Schedule publication if your role
approves. Share a draft with someone outside the team with a preview link. Publishing rules and
what to check before approving:
atlas-content. - Fix a mistake: Unpublish rather than delete; restore an older version from the version list.
- Site texts: UI texts, Site pages, Site menus.
- These rules (skills): read them in Admin → Skills, on the public Skills page or on GitHub. To suggest a change, use "Edit on GitHub" (or open the file and click the pencil, e.g. edit atlas-admin), then "Propose changes". GitHub opens a pull request (from a fork if you have no write access); a developer reviews it and it goes live with the next deploy.
Sources
- NN/g, data tables: https://www.nngroup.com/articles/data-tables/
- NN/g, confirmation dialogs: https://www.nngroup.com/articles/confirmation-dialog/
- NN/g, form errors: https://www.nngroup.com/articles/errors-forms-design-guidelines/
- Sanity, drafts and history: https://www.sanity.io/docs/user-guides/history-experience
- Payload, autosave: https://payloadcms.com/docs/versions/autosave
- Next.js production checklist (authorise in every action): https://nextjs.org/docs/app/guides/production-checklist