Repository navigation
docs(content): complete public frontend workflows for the Content Engine - #896
aXenDeveloper wants to merge 26 commits into
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 10 new issues in 7 files · 10 warnings · score 74 / 100 (Needs work) · 0 fixed · vs 10 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: 3f5b84d978
ℹ️ 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 nodeSelection = { | ||
| createdAt: columns.createdAt, | ||
| createdAtText: sql<string>`${columns.createdAt}::text`, |
There was a problem hiding this comment.
Serialize cursor timestamps independently of DateStyle
When PostgreSQL is configured with a non-ISO DateStyle (for example SQL, DMY), timestamp::text emits a value such as 21/10/2026 12:00:00, but decodeCursor accepts only the ISO-shaped regex. A cursor generated by the first page is therefore rejected on the next request, after_ is omitted, and pagination restarts from the first page indefinitely. Use a fixed, DateStyle-independent representation that preserves microseconds rather than the session-dependent text cast.
Useful? React with 👍 / 👎.
3f5b84d to
76124c5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76124c54e0
ℹ️ 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 TIMESTAMP_TEXT = | ||
| /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}(?::?\d{2})?)?$/; |
There was a problem hiding this comment.
Reject impossible timestamp cursor values before casting
When a client supplies a base64url cursor containing a shape-valid but impossible timestamp such as 2026-99-99 12:00:00, this regex accepts it and after_ asks PostgreSQL to cast it, which raises a datetime error and turns the public thread request into a 500. The previous Date parsing rejected such values before querying; validate the actual timestamp value or otherwise handle cast failures so malformed cursors cannot trigger server errors.
Useful? React with 👍 / 👎.
| queries: contentPermissionBatches(ids).map(batch => ({ | ||
| enabled: !session.isPending, | ||
| queryFn: async () => await limit(async () => await fetch(batch)), | ||
| queryKey: [...queryKey, "permissions", { ids: batch, userId }], |
There was a problem hiding this comment.
Cancel queued permission requests when the session changes
When more than 150 IDs leave batches waiting in this limiter and the session changes before they receive a slot, each query retains a key containing the old userId, but fetch(batch) runs later with the new session cookie. The response is therefore cached under the wrong visitor's key; because the app query client disables refetch-on-mount, returning to that identity can reuse incorrect Edit/Report flags. Make queued work cancellation-aware or ensure the request is dispatched only while its captured principal still matches the active session.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 219e1fa1c4
ℹ️ 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".
| > | ||
| <div className="flex flex-wrap items-center gap-2"> | ||
| <AutoFormSubmitButton>{t("save")}</AutoFormSubmitButton> | ||
| <Button onClick={onCancel} type="button" variant="ghost"> |
There was a problem hiding this comment.
Disable Cancel while a comment save is pending
On a slow save, Cancel remains enabled and unmounts the editor without cancelling its PATCH request. If the member reopens Edit and starts typing before that request finishes, the original submission's onSaved callback still calls setEditing(null) in CommentItem, closing the new editor and discarding its unsaved text. AutoForm disables the submit button, not this custom Cancel button. Bind Cancel to the form's submitting state so conflicting actions cannot run, as required by AGENTS.md.
AGENTS.md reference: AGENTS.md:L45-L45
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d34307ce79
ℹ️ 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".
| <Composer | ||
| articleId={articleId} | ||
| // A fresh key gives the next comment an empty editor. | ||
| key={composerKey} |
There was a problem hiding this comment.
Scope the comment composer instance to the article
When a reader types an unsent comment and navigates client-side to another article through LatestArticles, composerKey remains unchanged. The /news/:slug route renders its page without a parameter-dependent key, so the existing composer and its dirty form state survive while articleId changes. Submitting then posts the previous article's draft under the newly opened article. Include articleId in the composer key, or key the comment section by article, so form state cannot cross article boundaries.
Useful? React with 👍 / 👎.
- Add Public frontend overview, Viewer permissions guide/reference, useContentPublicWrite reference and Threaded replies on the frontend. - Rewrite the public edit guide around a complete, typechecked baseline (route, messages, 401/403/404/unexpected states, versions, conflicts, pending proposals, invalidation) with real screenshots. - Point reports, pages, lists and the write API reference at the new pages; fix pendingComponent claims that predate navigation progress. fix(content): batch useContentPermissions within the 50-id route limit The hook sent every id in one request, so a thread with more than 50 visible replies got 400 and lost all Edit/Report buttons. It now dedupes ids, sends batches of 50 as separate queries (at most three in flight), merges answers by id and exposes isError. fix(content): keep microseconds in thread cursors The cursor carried a millisecond Date while Postgres stores microseconds, so every page repeated the previous page's last top-level reply. Encode the column's own ::text and cast it back, as shared pagination does. fix(forum): edit topics with useContentPublicWrite Use the core hook so the edit page shows the documented conflict UI (Keep my version), and keep the form's defaults in step after a save so AutoForm no longer puts the old title back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Rebuild the beginner path on one minimal newsroom article (title, slug, body): First content type, Drafts & publishing, Public API, Content list and Content page, each continuing the previous page and ending with a check of the result. Member forms, permissions, reports, comments and replies continue the same example, with complete files and real request/response examples. - Reorganize the sidebar into Getting started, Public content, Member content, Interactions, Organization, Moderation, Content features, Editorial, Customization, Deployment and Reference, with short titles. - Fold Public frontend into the Introduction; split Editorial into Revisions, Previews and Scheduling; add Permissions API. - Replace screenshots with captures of the documented example and drop the unused ones. - Wrap long inline code on phones so docs pages never scroll sideways. - Give a required rich text field an empty document by default, so the AdminCP create dialog can submit it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
…CP forms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Keep First content type, Drafts & publishing, Public API, Content list and Content page together, and move Embedded list after them. Embedded list now adds a server-rendered Latest articles box that reuses articlesQueryOptions. - Rewrite Relations on a minimal newsroom.category with its model, table export, registration, references map and migration; move to-many relations, groups, repeatables and mixed id strategies to Advanced fields. - Tell drafts and pending approval apart: a plain create.status "draft" never opens a submission, so the create form and the comment composer now say "draft" and "waiting for a moderator" separately and only say "posted" for a public record. Approvals documents the states. - Focus Edit form on loading, refusals, saving with the version, form defaults, invalidation and conflicts; move proposals and rejection notices to Approval notices, built on the Forum plugin. - Split comments into Comments, Comment editing and Replies, each with its own result, including loading, error and retry states and the last-write-wins behavior without editorial. - Make Reports work from Content page alone, with its own permissions wrapper, verified on a staff-written article. - Replace screenshots that no longer matched with new captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
…m their prerequisites - Relations needs only First content type: the check now creates a category, assigns it and deletes it in the AdminCP, with the list column as a required step. Reading the category through the public API moved to an optional section that names its prerequisites, keeps the other publicApi options and says that only the related id is exposed. - Comments no longer requires member article writing: the article needs its public API and page, the comment type brings its own author and publicWrite, and TanStack Query has its install commands inline. The check uses Welcome to the newsroom, and repeated explanations point to one outcome table. - Approval notices read the server's approval only. The example shows its imports and types, drops the Forum's ?submitted=1 flag and explains why a URL must not decide a notice. - Recapture the Relations screenshots on a First content type article and the comment section on Welcome to the newsroom. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
The edit page showed "Waiting for a moderator" whenever the URL carried ?submitted=1, even on a published topic or a plain draft with nothing pending. The create page added that flag, but the edit route already returns the pending submission, so the flag added nothing. - ApprovalNotice reads only `approval`: pending create and pending edit need their exact status and kind, rejections keep the reason, and `approval: null` renders nothing whatever the status. - Drop the submitted flag from topicEditHref, the edit route's search and the create redirect. Old links with ?submitted=1 still open; the parameter is ignored. - After creating, only a published topic with a slug opens its public page; a pending or draft topic opens its edit page. - Align the Approval notices guide with the Forum code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Newsroom, the plugin the Content Engine tutorial builds, now carries everything the example plugin demonstrated: stories with files, scheduling, live editing and an AI teaser action, localized explainers and bulletins, uuid tags and bigint events, a desk container tree for letters to the editor, block pages, the settings page with widget zones and its blocks, a category moderation scope, the AdminCP overview page and the routing demo pages. The example plugin is removed from the workspace and both apps, and one migration drops its tables and creates the newsroom ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
The staff permission editor loads its label namespaces in chunks of MAX_NAMESPACES and merged them with Object.assign. Two chunks that each carried part of one plugin's root object replaced each other, so a plugin's title fell back to its id and a moderation scope label showed as the scope id. Merge the chunks deeply instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Rewrite the Content Engine, moderation, widgets, events, AI and search pages that used the removed example plugin so their code, ids, URLs and tables come from plugins/newsroom. Retake the 17 screenshots that showed example-plugin screens, now on seeded newsroom data, and correct their alt texts. Also tidy the ported zones and explainer pages and the newsroom README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Rewrites the beginner lessons (setup to AdminCP) around one plugin and corrects the API mismatches the audit found in queue, events, email templates, storage and widgets. Adds a Learn group to the sidebar, shortens titles, splits the visual editor page, and repairs broken anchors. Fixes found while validating the examples: - Bodiless DELETE requests answered 500 on TanStack Start apps: Hono's bodyLimit rebuilt the request with new Request(request, init), which throws on that runtime. The API now uses its own body limit middleware. - email.tailwindConfig in the API config never reached the default email template. - The newsroom plugin gains /newsroom/pages/:slug, which renders a page record's widgets, and labels for its widgets moderator permission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Cron is a site-notes how-to that uses the real buildCron options and logger levels, explains the production CRON_SECRET refusal and the Bearer requirement, and lists the adapters' real behaviour. - Notifications, websocket and updates pages match source; rooms move to their own websocket page. - Pagination and search are rebuilt on the site_notes table. - Debugging gains a real Debug Panel screenshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Users splits into Current user, Profile fields, Avatars and Manage users; the roles URL and labels match the AdminCP. - Redis documents the in-memory fallback without REDIS_URL and the separate REDIS_PASSWORD; auth lists every cookie option. - SSO, passkeys and captcha examples match source, including the state cookie round trip and Discord's unverified-email refusal. - Real screenshots of the roles list and the Moderator CP approval queue and submission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Configuration is a reference with an entry point and option tables for buildConfig, createVitNodeStart and vitnode(). - CLI pages explain that db:migrate runs the legacy vitnode migrate and show real plugin list and validate output. - Swagger is on in development only unless docs.enabled is set. - Dashboard widgets is a site-notes how-to with real data and settings; contribution lists the checks CI actually runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
The site-notes Notes widget from the tutorial, built as documented and placed on the AdminCP dashboard; its settings form was checked too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- AI actions splits into a first-action tutorial plus action options, streaming and errors; fields-and-editor splits into field suggestions, custom UI, Quick Ask and translations. Examples use the installed ai@7 API and the real forum permission module. - Search pages document the real reference fields and HeaderSearch; Elasticsearch gains prerequisites and a check step. - Mobile navigation gains real phone screenshots of the bottom bar and the Menu drawer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
…i18n - Routing becomes an overview with a task map, plus new pages: Add a route (a /site-notes/notes/:id tutorial, built and checked on the plugin), Route module, Layouts, URL parameters and Protected routes. - Localized URLs splits into a basic page and an advanced one (domains, proxies, language detection, redirects, hreflang). - Fetcher, server functions, cache, i18n, performance and deployment pages match source and use the site-notes plugin throughout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Content zones and editable pages split into the smallest tasks: render one zone, several zones, restrict widgets, make a page editable (a site-notes how-to, built and checked end to end with a real save screenshot), custom loader and saving layouts. Duplicated excerpts, QA checklists and design history are gone; labels match the editor. - Widget pages fix registration, layout-area values, sample data and status codes. - Built-in events passes pluginId when a listener queues work, replaces the cache example and uses the real logger levels; the custom event adapter runs the local listeners before forwarding to Redis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Regenerated by vitnode dev after the newsroom plugin gained the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Regression tests for the two fixes: a streamed or bodiless request on a non-global Request no longer answers 500, and the API config's email.tailwindConfig reaches the template props. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
- Database: address the generated Compose service and project (docker compose -p my-vitnode-vitnode-dev exec -T database) instead of a container name the generator renames, from the workspace root. - Add a route: install both @tanstack/react-query and @tanstack/react-router, matching the app's version ranges. - First widget: register only the callout it teaches, allow only core widgets and the callout, keep existing registrations, add the page type's strings, and drop an undeclared AdminNavPluginSource. - Editable pages: start the config from the Plugin page state, explain merging permissionStaff, label the plugin title, and recapture the screenshot from that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
The sidebar and the docs footer's "Next up" card already link the following page. Single-link Next step sections are removed; sections with several useful links are kept as Related. The docs skill and AGENTS.md now say not to add Next step endings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
The buttons now show only the provider's name under a Continue with heading, so the setup checks say to press the Google, Discord or Facebook button, and the Google image prompt shows the two-column page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
Show a moderator permission next to the admin one, and a public route that guards it with assertStaffPermission and type moderator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J
45aa3a5 to
72d39f0
Compare
|
|
||
| <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.
| <Empty className="border"> | ||
| <EmptyHeader> | ||
| <EmptyMedia variant="icon"> | ||
| <MessagesSquareIcon aria-hidden="true" /> | ||
| </EmptyMedia> | ||
| <EmptyTitle render={<h3 />}>{t("empty_board.title")}</EmptyTitle> | ||
| <EmptyDescription>{t("empty_board.desc")}</EmptyDescription> | ||
| <EmptyTitle render={<h3 />}>{t("empty_desk.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.
| <EmptyMedia variant="icon"> | ||
| <FolderTreeIcon aria-hidden="true" /> | ||
| </EmptyMedia> | ||
| <EmptyTitle render={<h2 />}>{t("new.no_desks.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.
useContentPublicWrite reference and Threaded replies on the frontend.
(route, messages, 401/403/404/unexpected states, versions, conflicts,
pending proposals, invalidation) with real screenshots.
pages; fix pendingComponent claims that predate navigation progress.
fix(content): batch useContentPermissions within the 50-id route limit
The hook sent every id in one request, so a thread with more than 50
visible replies got 400 and lost all Edit/Report buttons. It now dedupes
ids, sends batches of 50 as separate queries (at most three in flight),
merges answers by id and exposes isError.
fix(content): keep microseconds in thread cursors
The cursor carried a millisecond Date while Postgres stores microseconds,
so every page repeated the previous page's last top-level reply. Encode
the column's own ::text and cast it back, as shared pagination does.
fix(forum): edit topics with useContentPublicWrite
Use the core hook so the edit page shows the documented conflict UI
(Keep my version), and keep the form's defaults in step after a save so
AutoForm no longer puts the old title back.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_019yePNEqnbDm9BZVuNkWU5J