Skip to content

docs: Split public write frontend into create and edit guides - #892

Merged
aXenDeveloper merged 1 commit into
canaryfrom
claude/content-engine-frontend-docs
Oct 21, 2026
Merged

aXenDeveloper merged 1 commit into
canaryfrom
claude/content-engine-frontend-docs

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Improving Documentation

What?

Restructured the public write frontend documentation by:

  1. Split "Public write frontend" into two focused guides:

    • "Public create form" (public-write-frontend.mdx) - covers building /new pages where members create records
    • "Public edit form" (public-write-edit.mdx) - covers building /:id/edit pages where authors/moderators edit records
  2. Added three new guides:

    • "Build a public frontend" (public-frontend.mdx) - navigation hub for all public-facing Content Engine features
    • "Record actions" (record-actions.mdx) - explains Edit and Report buttons on detail pages and list rows
    • "Threaded replies frontend" (threads-frontend.mdx) - renders nested replies with reply/edit/report actions
  3. Restructured "Public lists" guide:

    • Expanded with implementation objectives and constraints
    • Clarified the distinction between what the Content Engine provides vs. what plugins define
    • Added before-you-begin section and improved examples
  4. Updated example plugin code:

    • Added discussions-page.tsx - full discussion list page with search, filters, sorting
    • Added discussion-list.ts - reusable list query options
    • Added discussion-actions.tsx - Edit/Report buttons component
    • Added latest-discussions.tsx - embedded list for other pages
    • Updated discussion-edit-page.tsx to use new list invalidation
    • Updated discussion-new-page.tsx to invalidate lists after creation
    • Updated discussion-page.tsx to show author and add action buttons
    • Updated boards-page.tsx to embed latest discussions
  5. Updated documentation metadata:

    • Added implementation sections with objectives, decisions, and constraints to guides
    • Added related sections linking between guides
    • Updated meta.json to reflect new guide structure
  6. Enhanced locale strings:

    • Added board field labels and validation messages
    • Added latest discussions section strings
    • Removed edit-specific messages from shared form section

Why?

The original "Public write frontend" guide tried to cover both create and edit flows in one document, making it harder to follow and understand the distinct concerns. Splitting into separate guides allows each to focus on its specific use case:

  • Create form emphasizes guest sign-in, pending approval states, and cache invalidation after posting
  • Edit form emphasizes version conflicts, permission checks, and handling concurrent edits

The new guides also establish a clearer information architecture for public-facing Content Engine features, with a navigation hub and cross-references between related topics. The example plugin code now demonstrates complete, working implementations of lists, detail pages, and record actions.

https://claude.ai/code/session_0127YWdeYvCW1buERsEUUMbu

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:07:42.728792Z 05bd248 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

React Doctor found 12 new issues in 9 files · 12 warnings · score 76 / 100 (Needs work) · 0 fixed · vs canary

12 warnings

src/components/tiptap/attachment/attachment-node-view.tsx

  • ⚠️ L87 Anchor has no content anchor-has-content

src/components/tiptap/image/image-node-view.tsx

  • ⚠️ L103 React function has high control-flow complexity no-high-complexity-react-function

src/content/server/live/document-store.ts

  • ⚠️ L168 await inside a loop async-await-in-loop

src/framework/dev-error/client.ts

  • ⚠️ L143 await before an early-return guard async-defer-await
  • ⚠️ L175 Browser global read at module scope no-unguarded-browser-global-at-module-scope

src/pages/blog-index.tsx

  • ⚠️ L108 React function has high control-flow complexity no-high-complexity-react-function

src/pages/discussions-page.tsx

  • ⚠️ L101 Heading has no content heading-has-content
  • ⚠️ L154 Role used instead of HTML tag prefer-tag-over-role

src/tanstack/content/public-write.tsx

  • ⚠️ L167 Array lookup inside a loop js-set-map-lookups

src/tanstack/fetcher/fetcher.test-d.ts

  • ⚠️ L192 Sequential independent awaits server-sequential-independent-await

src/views/admin/views/content/actions/content-form.tsx

  • ⚠️ L218 Intl formatter rebuilt each call js-hoist-intl
  • ⚠️ L219 Intl formatter rebuilt each call js-hoist-intl

Reviewed by React Doctor for commit 05bd248. See inline comments for fixes.

@aXenDeveloper aXenDeveloper changed the title Docs: Split public write frontend into create and edit guides docs: Split public write frontend into create and edit guides Oct 20, 2026
@github-actions github-actions Bot added the 📖 Documentation Documentation changes label Oct 20, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9c92c7f0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
```

The record is `{ id, slug, status, publishedAt, version, values, approval }`. `values` holds exactly the fields in `update.fields`, so it fills the form as is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe the full values allowlist

When create.fields and update.fields differ, this statement is incorrect: the edit response contains the union of both allowlists. In the example, board is create-only but is still present in record.values; forwarding the object as described to PATCH /{id} can fail because the update schema is strict and rejects board. Explain that edit forms must select only update.fields rather than treating all returned values as editable.

Useful? React with 👍 / 👎.

The record is `{ id, slug, status, publishedAt, version, values, approval }`. `values` holds exactly the fields in `update.fields`, so it fills the form as is.

- `401` means the visitor is signed out. Show a sign-in link that returns to this page.
- `403` means they may read the record but not edit it: someone else's discussion, a record a moderator [hid](/docs/content-engine/content-visibility), or an owner edit after a container's [edit window](/docs/content-engine/container-policies) closed. The last one carries the `edit_window_closed` issue; the Forum plugin's `topic-edit-page.tsx` shows its own message for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify hidden edit reads as 404

When a moderator hides a record, an owner without moderator can_edit receives 404, not 403: hidden rows are non-public, so contentPublicWriteRefusal deliberately maps the denial to notFound. A moderator allowed to edit the hidden record receives 200. Listing hidden records under 403 therefore teaches consumers to handle an API state that cannot occur and contradicts the privacy behavior described in the next bullet.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5bdb562a5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +376 to +378
const permissions = useCommentPermissions(
items.flatMap(item => (item.record ? [item.id] : [])),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Batch permission lookups before exceeding the 50-ID cap

On threads containing more than 50 visible nodes—which can happen on the initial response because nested replies are capped at 500, or after loading another page—this passes every accumulated ID to one permissions request. The new endpoint rejects more than 50 IDs with 400, causing the query to have no data and permissions.of() to return false for every comment, so all Edit and Report actions disappear. Split the IDs into requests of at most 50 or make the hook perform that batching.

Useful? React with 👍 / 👎.

Comment on lines +84 to +90
if (user && definition.publicWrite.update.enabled) {
const container = await resolveContentRowContainer(
c,
db,
definition,
row,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Batch container resolution in the permissions route

For every signed-in visitor, this resolves each record's container independently. resolveContentRowContainer() loads the container chain and, for inherited containers such as forum comments, first queries the related topic, so a valid 50-comment request can generate roughly 100 database queries merely to render action buttons. Since the new endpoint runs on each thread/list view, resolve the relevant container IDs and chains in bulk rather than once per row.

Useful? React with 👍 / 👎.

Comment on lines +687 to +689
...(definition.publicWrite.update.enabled || definition.reports.enabled
? [contentPermissionsRoute(model, { pluginId })]
: []),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent /permissions from shadowing a valid slug

For any content type with updates or reports enabled, a published record whose valid slug is permissions can no longer be fetched through GET /{slug}: the newly mounted static route handles /permissions first and rejects the detail request because its required ids query is absent. Reserve this value during slug generation or place the capability endpoint under a path that cannot collide with public slugs.

Useful? React with 👍 / 👎.

"Cache-Control": "private, no-store",
// The body is the record as usual; this is how a page tells its author
// or a moderator that readers cannot see it.
[CONTENT_HIDDEN_HEADER]: "true",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the hidden-record header to cross-origin clients

In supported split web/API deployments, this custom response header is not readable by browser code because the default CORS configuration exposes only the rate-limit headers. Consequently detail.headers.get(CONTENT_HIDDEN_HEADER) returns null during client-side navigation even though the API returned a hidden record, and the page omits the warning that readers cannot see it. Add this header to the default Access-Control-Expose-Headers list.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35a0f2c0e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +42
const field = definition.search.authorField;
if (!field || authorId === null) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back to public-write ownership in hidden lists

For a visibility-enabled type that defines publicWrite.ownership but no search.authorField, this returns no author scope, so the owner cannot see their own hidden records in public lists. The bundled example.discussion type has exactly that configuration (ownership.field is author), while detail access already falls back to publicWrite.ownerField; apply the same fallback here so hidden discussions do not disappear from their authors' list.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c06d967de0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +74 to +78
isUpload &&
typeof json.attrs?.src !== "string" &&
json.attrs?.deleted !== true
) {
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block submission until editor uploads settle

When a user inserts an image or attachment and submits a JSON editor before the upload request completes, the placeholder has an uploadId but no src, so this branch silently removes it from the serialized document. The write can therefore succeed without the media the user just inserted; if the page then navigates away, the eventual upload is also left orphaned in storage. Track pending uploads and block or await form submission rather than dropping their nodes.

Useful? React with 👍 / 👎.

table: PgTable;
},
): Promise<(ContentPublicHiddenFlag & TRow)[]> => {
const ids = rows.map(row => row.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve record IDs while marking hidden list rows

For a visibility-enabled content type whose publicApi.listFields omits id—which the public API configuration explicitly permits—the projector removes the internally selected ID before calling this helper. Every value collected here is then undefined, so the hidden-row lookup cannot match and authorized hidden records are returned without the promised hidden: true flag. Perform the lookup before projection or carry the internal IDs separately from the public response.

Useful? React with 👍 / 👎.

… polish

- docs(content-engine): complete and reorganize guides for building public frontends around options
- feat(content): permissions route, public write helper and live API reload in dev
- feat(content): better error descriptions and translation management in the content form
- feat(blog): support hidden articles in blog posts and categories
- feat(tanstack/layout): enhance error overlay and document head
- feat(tiptap): styles for images and attachments; fix KIND_ICONS type
- test(public-routes): enhance OpenAPI schema validation
- fix(example): prevent commit on Enter during composition in discussions page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aXenDeveloper
aXenDeveloper force-pushed the claude/content-engine-frontend-docs branch from 20b4fee to 05bd248 Compare October 21, 2026 13:01
className="shrink-0"
nativeButton={false}
render={
<a href={src} rel="noopener noreferrer nofollow" target="_blank" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/anchor-has-content (warning)

Blind users can't follow this link because screen readers announce nothing, so add visible text, aria-label, or aria-labelledby.

Fix → Put readable text inside every link.

Docs

);
};

export const EditorImageNodeView = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

EditorImageNodeView has cyclomatic complexity 22, cognitive complexity 23, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

const before = Y.encodeStateVector(doc);
if (!mutate(doc)) continue;

await db

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

if (!error) return;

overlay.style.display = "none";
const html = await requestErrorPage(error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-defer-await (warning)

This await blocks the function before an early-return that doesn't use the awaited value, so the skip path waits for nothing. Move the await below the guard so it only runs when you need the data

Fix → Move the await below the early-return guard so the skip path stays fast and avoids unnecessary async work.

Docs

}).observe(document.documentElement, { childList: true, subtree: true });
};

window.addEventListener("message", event => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-unguarded-browser-global-at-module-scope (warning)

Reading window here crashes with "ReferenceError: window is not defined" the instant this module is imported during SSR — move the read inside a function or effect, or guard it with typeof window !== "undefined".

Fix → Reading window/navigator/localStorage at module scope throws ReferenceError: window is not defined when the module is imported during SSR. Move the read inside a function/effect, or guard it with typeof window !== "undefined".

Docs

const locale = useLocale();
const invalidateOptions = useInvalidateContentOptions();

const languages = new Intl.DisplayNames([locale], { type: "language" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-hoist-intl (warning)

This is slow because new Intl.DisplayNames() rebuilds on every call inside a function, so move it to the top of the file, or wrap it in useMemo

Fix → Move new Intl.NumberFormat(...) to the top of the file or wrap it in useMemo. Building one is slow, so don't redo it on every call

Docs

const invalidateOptions = useInvalidateContentOptions();

const languages = new Intl.DisplayNames([locale], { type: "language" });
const conjunction = new Intl.ListFormat(locale, { type: "conjunction" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-hoist-intl (warning)

This is slow because new Intl.ListFormat() rebuilds on every call inside a function, so move it to the top of the file, or wrap it in useMemo

Fix → Move new Intl.NumberFormat(...) to the top of the file or wrap it in useMemo. Building one is slow, so don't redo it on every call

Docs

return isSharedRender ? (session.data?.user?.id ?? null) : null;
};

export const BlogIndex = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

BlogIndex has cyclomatic complexity 21, cognitive complexity 20, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">{icon}</EmptyMedia>
<EmptyTitle render={<h2 />}>{title}</EmptyTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs


<div
className="flex flex-col gap-3 md:flex-row md:items-center"
role="search"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/prefer-tag-over-role (warning)

Screen reader users get more reliable semantics from <search> than role="search", so use <search> instead.

Fix → Use the matching HTML element when one exists so browsers and assistive tech get native semantics.

Docs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05bd248d37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}) =>
contentListQueryOptions({
...request,
queryKey: discussionQueryKeys.all,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Partition cached discussion lists by viewer

When an author or moderator loads a list containing hidden discussions, this identity-agnostic query key stores the personalized API response for a minute. Signing out only clears the designated identity queries, and the subsequent router invalidation can reuse this still-fresh entry, so another visitor using the same browser can see the previous user's hidden rows. Include the viewer identity in this key or explicitly remove these list queries when the session changes, as the new blog viewer queries do.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34
if (typeof node.attrs.fileId === "number") {
return node.type.name === EDITOR_IMAGE_NODE || runtime.attachments;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify pasted file IDs before bypassing remote-image policy

When remote images are disabled, any pasted or dragged HTML containing a numeric data-file-id is accepted without verifying that the ID and URL belong to this installation. For example, dragging an image rendered by another VitNode site preserves that site's remote src and fabricates a local file association, bypassing the default upload-only policy and potentially causing the image to be marked deleted when an unrelated local file with the same ID is removed. Only treat authenticated same-install metadata as a stored upload; otherwise apply the remote-image rule or re-upload the file.

Useful? React with 👍 / 👎.

runtime
.upload(file)
.then(result => {
finished.set(uploadId, result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope finished uploads to an editor lifecycle

After the first successful upload, this module-global finished map retains every result for the lifetime of the SPA and is never cleared. Consequently hasFinishedEditorUploads() remains true forever, making every subsequent document-changing transaction in every editor traverse its entire document in appendTransaction, even after the originating editor is destroyed; long-lived authoring sessions therefore accumulate memory and permanent per-keystroke work. Keep these results per editor and release them when its history/editor is discarded.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit dc46b4f into canary Oct 21, 2026
7 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/content-engine-frontend-docs branch October 21, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📖 Documentation Documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant