Repository navigation
docs: Split public write frontend into create and edit guides - #892
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found 12 new issues in 9 files · 12 warnings · score 76 / 100 (Needs work) · 0 fixed · vs 12 warnings
Reviewed by React Doctor for commit |
There was a problem hiding this comment.
💡 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| const permissions = useCommentPermissions( | ||
| items.flatMap(item => (item.record ? [item.id] : [])), | ||
| ); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (user && definition.publicWrite.update.enabled) { | ||
| const container = await resolveContentRowContainer( | ||
| c, | ||
| db, | ||
| definition, | ||
| row, | ||
| ); |
There was a problem hiding this comment.
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 👍 / 👎.
| ...(definition.publicWrite.update.enabled || definition.reports.enabled | ||
| ? [contentPermissionsRoute(model, { pluginId })] | ||
| : []), |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| const field = definition.search.authorField; | ||
| if (!field || authorId === null) return undefined; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| isUpload && | ||
| typeof json.attrs?.src !== "string" && | ||
| json.attrs?.deleted !== true | ||
| ) { | ||
| return null; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>
20b4fee to
05bd248
Compare
| className="shrink-0" | ||
| nativeButton={false} | ||
| render={ | ||
| <a href={src} rel="noopener noreferrer nofollow" target="_blank" /> |
There was a problem hiding this comment.
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.
| ); | ||
| }; | ||
|
|
||
| export const EditorImageNodeView = ({ |
There was a problem hiding this comment.
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.
| const before = Y.encodeStateVector(doc); | ||
| if (!mutate(doc)) continue; | ||
|
|
||
| await db |
There was a problem hiding this comment.
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.
| if (!error) return; | ||
|
|
||
| overlay.style.display = "none"; | ||
| const html = await requestErrorPage(error); |
There was a problem hiding this comment.
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.
| }).observe(document.documentElement, { childList: true, subtree: true }); | ||
| }; | ||
|
|
||
| window.addEventListener("message", event => { |
There was a problem hiding this comment.
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".
| const locale = useLocale(); | ||
| const invalidateOptions = useInvalidateContentOptions(); | ||
|
|
||
| const languages = new Intl.DisplayNames([locale], { type: "language" }); |
There was a problem hiding this comment.
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
| const invalidateOptions = useInvalidateContentOptions(); | ||
|
|
||
| const languages = new Intl.DisplayNames([locale], { type: "language" }); | ||
| const conjunction = new Intl.ListFormat(locale, { type: "conjunction" }); |
There was a problem hiding this comment.
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
| return isSharedRender ? (session.data?.user?.id ?? null) : null; | ||
| }; | ||
|
|
||
| export const BlogIndex = ({ |
There was a problem hiding this comment.
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.
| <Empty className="border"> | ||
| <EmptyHeader> | ||
| <EmptyMedia variant="icon">{icon}</EmptyMedia> | ||
| <EmptyTitle render={<h2 />}>{title}</EmptyTitle> |
There was a problem hiding this comment.
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.
|
|
||
| <div | ||
| className="flex flex-col gap-3 md:flex-row md:items-center" | ||
| role="search" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| if (typeof node.attrs.fileId === "number") { | ||
| return node.type.name === EDITOR_IMAGE_NODE || runtime.attachments; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 👍 / 👎.
Improving Documentation
What?
Restructured the public write frontend documentation by:
Split "Public write frontend" into two focused guides:
public-write-frontend.mdx) - covers building/newpages where members create recordspublic-write-edit.mdx) - covers building/:id/editpages where authors/moderators edit recordsAdded three new guides:
public-frontend.mdx) - navigation hub for all public-facing Content Engine featuresrecord-actions.mdx) - explains Edit and Report buttons on detail pages and list rowsthreads-frontend.mdx) - renders nested replies with reply/edit/report actionsRestructured "Public lists" guide:
Updated example plugin code:
discussions-page.tsx- full discussion list page with search, filters, sortingdiscussion-list.ts- reusable list query optionsdiscussion-actions.tsx- Edit/Report buttons componentlatest-discussions.tsx- embedded list for other pagesdiscussion-edit-page.tsxto use new list invalidationdiscussion-new-page.tsxto invalidate lists after creationdiscussion-page.tsxto show author and add action buttonsboards-page.tsxto embed latest discussionsUpdated documentation metadata:
implementationsections with objectives, decisions, and constraints to guidesrelatedsections linking between guidesmeta.jsonto reflect new guide structureEnhanced locale strings:
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:
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