Skip to content

Add metadata-only PUT endpoint and read-only GET endpoint for pages#888

Merged
otomatty merged 2 commits into
developfrom
claude/investigate-yjs-realtime-FKw4R
May 16, 2026
Merged

Add metadata-only PUT endpoint and read-only GET endpoint for pages#888
otomatty merged 2 commits into
developfrom
claude/investigate-yjs-realtime-FKw4R

Conversation

@otomatty
Copy link
Copy Markdown
Owner

@otomatty otomatty commented May 16, 2026

概要

local コラボレーションモード廃止に伴い、ページメタデータ(タイトル・プレビュー)の更新と読み取り専用アクセスのための新しい REST エンドポイントを追加します。

  • PUT /api/pages/:id — メタデータのみ更新(Y.Doc は Hocuspocus 経由)
  • GET /api/pages/:id/public-content — 読み取り専用の本文取得(ゲスト・viewer 用)

変更点

  • PUT /api/pages/:id エンドポイント追加

    • タイトルと content_preview のメタデータ更新に特化
    • 既存の applyPagesMetadataUpdatetryPropagateTitleRenameemitPageUpdatedIfChanged を再利用
    • タイトル伝播・SSE 通知のゲーティングは PUT /:id/content と同等
    • 同じ値の round-trip は UPDATE をスキップ(PR feat: add note-scoped SSE event channel for page list (#860 Phase 4) #867 の不変条件を維持)
  • GET /api/pages/:id/public-content エンドポイント追加

    • 読み取り専用ビュー(未認証ゲスト・viewer ロール)向けの本文取得 API
    • Y.Doc バイト列は返さず、content_text と派生情報のみ返す
    • authOptional で未ログインゲストも public/unlisted ノートにアクセス可能
    • ゲストは public, max-age=60, must-revalidate でエッジキャッシュ可能
    • ログイン済みユーザーは private, must-revalidate で個人スコープに限定
  • 型定義追加 (src/lib/api/types.ts)

    • UpdatePageMetadataBody
    • UpdatePageMetadataResponse
    • PagePublicContentResponse
  • API クライアント追加 (src/lib/api/apiClient.ts)

    • updatePageMetadata()
    • getPagePublicContent()
  • テスト追加 (server/api/src/__tests__/routes/pages.test.ts)

    • PUT /api/pages/:id の 5 つのテストケース
    • GET /api/pages/:id/public-content の 6 つのテストケース

変更の種類

  • ✨ 新機能 (New feature)

テスト方法

  1. bun test で新規テストがすべてパスすることを確認
  2. 既存の PUT /api/pages/:id/content テストが引き続きパスすることを確認
  3. bun run lintbun run format:check が通ることを確認

チェックリスト

関連 Issue

local モード廃止に伴う REST API の整備

https://claude.ai/code/session_019PjuoaMdQiFcVdR96i9ATc

Summary by CodeRabbit

  • New Features
    • Users can now update page title and preview metadata through a dedicated endpoint.
    • Added public content retrieval for pages with proper caching and access control, allowing guests to view public pages without authentication and logged-in users to access private pages with appropriate permissions.

Review Change Stack

Y.js `local` モード廃止の Phase 1 として、以下を新設する。

- `PUT /api/pages/:id`: タイトル等のメタデータだけを更新する純粋な REST
  経路。Y.Doc は Hocuspocus が一手に担うので、本ルートでバイト列は受けない。
  既存の `applyPagesMetadataUpdate` / `tryPropagateTitleRename` /
  `emitPageUpdatedIfChanged` を再利用するため、PUT /:id/content と同じ
  ゲーティング(SSE emit、タイトル伝播)が維持される。
- `GET /api/pages/:id/public-content`: ゲスト / viewer 向けの読み取り専用
  本文 API。`page_contents.content_text` だけを返し、Y.Doc バイト列は
  含めない。`getNoteRole` で role を解決して private / restricted を
  弾く。未ログインの guest は短期 `public, max-age=60` でエッジキャッシュ
  可能、ログイン済みは `private, must-revalidate`。

これらは local モードを最終的に削除する後続 Phase の前提となる代替経路。
本 Phase では既存 `GET/PUT /content` には触れず、後続 PR で削除する。

Phase 1 — adds metadata + read-only content routes ahead of retiring the
`local` collaboration mode. Editors will continue through Hocuspocus while
read-only consumers (guests on public/unlisted notes, viewer-role members,
MCP) move onto `public-content`. Existing `GET/PUT /:id/content` is
intentionally untouched here; removal happens in a later PR.

Test plan:
- `cd server/api && bun run test` — 1278 tests pass
- `bun run test:run` — full multi-package suite passes
- `bun run lint` — 0 errors (warnings unchanged)
- `bun run format:check` — clean
- `cd server/api && bun run typecheck` — clean
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@otomatty has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 47 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81995928-93b7-49f5-bdd0-407c4ad5e170

📥 Commits

Reviewing files that changed from the base of the PR and between 4b528f9 and c4f4c16.

📒 Files selected for processing (3)
  • server/api/src/__tests__/routes/pages.test.ts
  • server/api/src/routes/pages.ts
  • src/lib/api/types.ts
📝 Walkthrough

Walkthrough

This PR introduces two new pages REST endpoints: PUT /api/pages/:id for metadata-only updates (title, content_preview) with edit access control, and GET /api/pages/:id/public-content for read-only rendered text with role-based access and adaptive cache headers. Both endpoints are fully typed, client-wrapped, and tested across success, error, and permission scenarios.

Changes

Page Metadata and Public Content Endpoints

Layer / File(s) Summary
API type contracts
src/lib/api/types.ts
UpdatePageMetadataBody, UpdatePageMetadataResponse, and PagePublicContentResponse define request/response shapes for metadata updates and public content retrieval.
API client methods
src/lib/api/apiClient.ts
updatePageMetadata(pageId, body) and getPagePublicContent(pageId) wire the new endpoints into the typed client with standard and optional-auth request helpers.
Backend route handlers
server/api/src/routes/pages.ts
PUT /api/pages/:id validates metadata fields, enforces edit access, applies changes via applyPagesMetadataUpdate, triggers title rename propagation, and conditionally emits page.updated. GET /api/pages/:id/public-content resolves role-based access, fetches rendered text and metadata, sets guest vs authenticated Cache-Control headers, and returns 404 for missing pages. Route documentation updated.
Test coverage
server/api/src/__tests__/routes/pages.test.ts
PUT /api/pages/:id tests cover title updates, no-op skips, and errors (missing fields, auth, non-existent). GET /api/pages/:id/public-content tests cover owner success with caching, missing content fallback, authorization failures, and unauthenticated guest access to public pages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • otomatty/zedi#714: New PUT and GET endpoints rely on note-native authorization model (assertPageEditAccess, pages.note_id) introduced in this PR.
  • otomatty/zedi#736: Both PRs add/extend title rename detection in routes/pages.ts and trigger propagateTitleRename to rewrite wiki-link/tag references.
  • otomatty/zedi#867: New PUT /api/pages/:id metadata route wires into applyPagesMetadataUpdate and emitPageUpdatedIfChanged SSE broadcasting logic.

Poem

🐰 A pages path springs to life—one to mend, one to read,
With types and cache headers tailored to each need,
Guest and owner alike shall find their way,
Metadata whispers, content on display! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding two new endpoints (PUT metadata-only and GET read-only) for pages REST API.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/investigate-yjs-realtime-FKw4R

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces two new endpoints to the pages API: a metadata update route (PUT /api/pages/:id) and a read-only content route (GET /api/pages/:id/public-content). These changes support the transition away from the legacy local collaboration mode by separating metadata management from Y.Doc synchronization. The review feedback identifies a critical bug in the metadata update endpoint where a no-op request (sending current values) results in a response containing null values instead of the current resource state. Additionally, there is a recommendation to refine the TypeScript types to ensure updated_at is non-nullable, maintaining consistency with other API responses.

Comment on lines +931 to +936
return c.json({
id: pageId,
title: updatedRow?.title ?? null,
content_preview: updatedRow?.contentPreview ?? null,
updated_at: updatedRow?.updatedAt?.toISOString() ?? 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.

high

applyPagesMetadataUpdate は、変更が検出されない場合(クライアントが現在と同じ値を送信した場合など)に updatedRow: null を返します。その結果、この実装では titlecontent_previewupdated_atnull として返されてしまいます。PUT エンドポイントは、実際に更新が行われたかどうかにかかわらず、リソースの現在の状態を返すべきです。

共有ヘルパーである applyPagesMetadataUpdate を修正して常に現在の行を返すようにするか、このルート内で updatedRownull の場合に現在のデータを取得するように修正することを検討してください。

body: JSON.stringify({ title: "Same Title", content_preview: "Same Preview" }),
});

expect(res.status).toBe(200);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

このテストケースでは、更新がスキップされた場合でもレスポンスボディに現在のページメタデータが正しく含まれていることを確認するようにしてください。現状の実装(no-op 時に null を返すバグ)を検出するために重要です。

Suggested change
expect(res.status).toBe(200);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body).toMatchObject({
id: PAGE_ID,
title: "Same Title",
content_preview: "Same Preview",
});

Comment thread src/lib/api/types.ts Outdated
id: string;
title: string | null;
content_preview: string | null;
updated_at: string | 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.

medium

既存のページには必ず更新日時が存在するため、updated_atnull を許容すべきではありません。これは PagePublicContentResponseSyncPageItem の定義とも整合します。

Suggested change
updated_at: string | null;
updated_at: string;

Copy link
Copy Markdown

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

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: 4b528f9182

ℹ️ 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 thread server/api/src/routes/pages.ts Outdated
Comment on lines +933 to +935
title: updatedRow?.title ?? null,
content_preview: updatedRow?.contentPreview ?? null,
updated_at: updatedRow?.updatedAt?.toISOString() ?? 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.

P2 Badge Return current metadata for no-op PUT updates

When PUT /api/pages/:id receives values that already match the stored metadata, applyPagesMetadataUpdate returns updatedRow: null and this response path emits title, content_preview, and updated_at as null. In that common round-trip scenario, clients that trust the response can overwrite valid local metadata with nulls even though no data changed server-side; the endpoint should return the current persisted values (or avoid nullable success fields) for no-op updates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/api/src/__tests__/routes/pages.test.ts (1)

770-785: ⚡ Quick win

Assert response payload in the no-op metadata test, not just UPDATE count.

This test currently verifies only DB-chain behavior. Add body assertions so a null-field response regression is caught.

Proposed test tightening
   it("skips the UPDATE when title matches current value (PR `#867` invariant)", async () => {
@@
     expect(res.status).toBe(200);
+    const body = (await res.json()) as Record<string, unknown>;
+    expect(body).toMatchObject({
+      id: PAGE_ID,
+      title: "Same Title",
+      content_preview: "Same Preview",
+    });
     const updateChains = chains.filter((c) => c.startMethod === "update");
     expect(updateChains.length).toBe(0);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/src/__tests__/routes/pages.test.ts` around lines 770 - 785, The
test "skips the UPDATE when title matches current value (PR `#867` invariant)"
only asserts DB chain behavior; update it to also assert the response payload so
a null-field regression is caught: after receiving res from
app.request(`/api/pages/${PAGE_ID}`, ...) call await res.json() and assert the
returned body contains the expected metadata fields (e.g., title === "Same
Title" and content_preview === "Same Preview" or non-null metadata object) and
that status is 200; reference createPagesAppWithChains, pageAccessPrefix,
authHeaders, PAGE_ID and chains to locate the test and add these JSON body
assertions alongside the existing updateChains length check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/src/routes/pages.ts`:
- Around line 911-924: Validate that incoming metadata fields are strings before
calling applyPagesMetadataUpdate: check that if body.title is present it is of
type string (otherwise throw new HTTPException(400, { message: "title must be a
string" })) and likewise for body.content_preview; perform these checks
immediately after parsing body and before assertPageEditAccess /
applyPagesMetadataUpdate so malformed JSON like { "title": 123 } returns a 400
instead of causing body.title.trim() inside applyPagesMetadataUpdate to throw.
- Around line 924-936: The handler currently returns null
title/content_preview/updated_at when applyPagesMetadataUpdate returns
updatedRow === null, which can clobber client state; fix by when updatedRow is
null, query the current page record (using db and pageId) to read the existing
title, contentPreview and updatedAt and use those values in the JSON response
instead of null; keep the existing flow for renamed/emitPageUpdatedIfChanged
(renamed, metadataChanged) unchanged and only use the fallback read when
updatedRow is null so responses always contain the current page fields.

---

Nitpick comments:
In `@server/api/src/__tests__/routes/pages.test.ts`:
- Around line 770-785: The test "skips the UPDATE when title matches current
value (PR `#867` invariant)" only asserts DB chain behavior; update it to also
assert the response payload so a null-field regression is caught: after
receiving res from app.request(`/api/pages/${PAGE_ID}`, ...) call await
res.json() and assert the returned body contains the expected metadata fields
(e.g., title === "Same Title" and content_preview === "Same Preview" or non-null
metadata object) and that status is 200; reference createPagesAppWithChains,
pageAccessPrefix, authHeaders, PAGE_ID and chains to locate the test and add
these JSON body assertions alongside the existing updateChains length check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ebe0938c-e191-4dcc-8318-f431da7f02c1

📥 Commits

Reviewing files that changed from the base of the PR and between dcdc83d and 4b528f9.

📒 Files selected for processing (4)
  • server/api/src/__tests__/routes/pages.test.ts
  • server/api/src/routes/pages.ts
  • src/lib/api/apiClient.ts
  • src/lib/api/types.ts

Comment thread server/api/src/routes/pages.ts
Comment thread server/api/src/routes/pages.ts
PR #888 のレビューフィードバックに対応。

- gemini-code-assist / chatgpt-codex / coderabbitai 全員が指摘した
  「no-op PUT で title/content_preview/updated_at が null で返り、
  クライアントの正規キャッシュを破壊しうる」問題を修正。
  `applyPagesMetadataUpdate` が UPDATE を skip して `updatedRow=null` を
  返す場合に限り、現在の行を追加 SELECT してその値を返す。
- coderabbitai 指摘の型バリデーション欠落を修正。`body.title` /
  `body.content_preview` が非文字列の場合に、ヘルパー内の `.trim()` で
  500 になる前にルート境界で 400 を返す。
- gemini-code-assist 指摘に従い `UpdatePageMetadataResponse.updated_at`
  を non-nullable に修正 (`pages.updated_at` はスキーマ上 notNull)。
- テストを拡張: no-op 経路がレスポンスボディに現在値を含めて返すこと、
  および title/content_preview の非文字列入力で 400 を返すことを検証。

Test plan:
- `cd server/api && bun run test` — 1280 tests pass (+2 new)
- `bun run lint` / `bun run format:check` — clean
- `cd server/api && bun run typecheck` — clean
- root frontend typecheck — clean

Refs: PR #888 review threads (HIGH/Major priority)
@otomatty otomatty self-assigned this May 16, 2026
@otomatty otomatty merged commit f2388b0 into develop May 16, 2026
18 checks passed
@otomatty otomatty deleted the claude/investigate-yjs-realtime-FKw4R branch May 16, 2026 17:56
otomatty added a commit that referenced this pull request May 17, 2026
… Phase 2) (#893)

* refactor(note): introduce NotePagePublicView for read-only guests (#889 Phase 2)

Y.js `local` モード廃止の Phase 2。`PageEditorContent` を `src/components/note/`
配下に物理移動し、ゲスト / viewer 向けの読み取り専用 UI として新規
`NotePagePublicView` を追加する。

- `src/components/editor/PageEditor/` から `src/components/note/` へ
  `PageEditorContent.tsx` / `PageTitleBlock.tsx` / `PageTitleBlock.test.tsx`
  を `git mv` で移動。Phase 3 で `/pages/:id` ルートを削除する前提に向けて、
  note 配下で完結する描画コンポーネントを集約する。`PageEditorContent` の
  内部 import は絶対パスに書き換え、`WikiGeneratorStatus` は canonical な
  `@/hooks/useWikiGenerator` 由来に統一 (Phase 3 で旧 `types.ts` ごと削除)。
- 新規 `NotePagePublicView` は Phase 1 で追加した
  `GET /api/pages/:id/public-content` から `content_text` を取得し、
  `convertMarkdownToTiptapContent` で Tiptap doc に変換した上で
  `PageEditorContent` を `isReadOnly` で再利用して描画する。Y.Doc / WebSocket
  は一切張らない。403 / 404 / その他エラーに応じた文言と再試行 UI を提供。
- `NotePageView` の `!canEdit` 分岐 (guest + viewer) を `NotePagePublicView`
  への委譲に置換。`useCollaboration` API は変更せず、既存の
  `canEdit && isSignedIn` ゲートでこれまで通り WebSocket は張られない。
- 既存テスト (`NotePageView.test.tsx`) を新しい branch 構造に合わせて更新。
  `keeps note-native pages read-only` テストはタイトル編集 UI が
  `NotePagePublicView` 内に存在しないことを構造的に保証する形に書き換えた。
  新規 `NotePagePublicView.test.tsx` で loading / success (Tiptap 変換) /
  title fallback / null content / 403 / 404 / 再試行を網羅。

Phase 2 — relocates `PageEditorContent` into `src/components/note/` and
introduces `NotePagePublicView`, a Y.Doc-free read-only renderer that
covers both `guest` (anonymous) and `viewer`-role members. The
`NotePageView` `!canEdit` branch now delegates to it, paving the way for
Phase 3's `/pages/:id` route removal and Phase 4's deletion of the legacy
`GET/PUT /api/pages/:id/content` endpoints. `useCollaboration`'s API is
untouched.

Test plan:
- `bunx vitest run src/components/editor src/components/note src/pages` —
  79 files / 623 tests pass
- `bun run lint` — 0 errors
- `bun run format:check` — clean
- `bunx tsc --noEmit --ignoreDeprecations 6.0 -p tsconfig.app.json` —
  no new errors in touched files (pre-existing unrelated errors elsewhere)

Refs: Issue #889, PR #888 (Phase 1)

https://claude.ai/code/session_016u6uKXnWVPobXkGjKnmLeR

* fix(note): wire read-only Markdown export to public-content body (#893)

Codex P1 (PR #893 review) で指摘された不整合への修正。`NotePageReadOnly` の
`useMarkdownExport` は `page.content` を読んでいたが、`apiPageToPage`
(`src/hooks/useNoteQueries.ts:295-297`) はメタデータ取得時に `content: ""`
を強制セットしているため、画面に本文が見えていても export / copy で生成
される Markdown が常に空になる回帰だった。

- `usePagePublicContent` を新規切り出し (`src/hooks/usePagePublicContent.ts`)。
  `["page-public-content", pageId]` のクエリキーを `NotePagePublicView`
  (本文描画) と `NotePageReadOnly` (Markdown export / copy のソース) で共有し、
  TanStack Query 上で 1 リクエストに dedup させる。
- `NotePagePublicView` を新フック経由に切替。挙動は不変。
- `NotePageReadOnly` で `usePagePublicContent` を呼び、`content_text` を
  `useMarkdownExport` の本文引数に渡す。タイトルも response 側を優先。
- 公開コンテンツ未到着の間は `export-markdown` / `copy-markdown` メニュー
  項目を `disabled` にし、ロード前に空 Markdown を吐く動線を塞ぐ。
- 既存テスト (`NotePageView.test.tsx`) を新フックの mock に対応。
  `useMarkdownExport` モックを引数キャプチャ式に書き換え、Codex 指摘の
  回帰テスト 2 件 (export ソースが `content_text` を使うこと、ロード中は
  メニュー項目が disabled になること) を追加。

Test plan:
- bunx vitest run src/hooks src/components/note src/pages — 59 ファイル / 554
  テスト全通過
- bunx eslint src/hooks/usePagePublicContent.ts
  src/components/note/NotePagePublicView.tsx src/pages/NotePageView.{tsx,test.tsx}
  — 0 errors
- bun run format:check — clean

Refs: PR #893 review (Codex P1)

https://claude.ai/code/session_016u6uKXnWVPobXkGjKnmLeR

* docs(note): refresh NotePageReadOnly contract comment for public-content source

CodeRabbit nitpick (PR #893): `NotePageReadOnly` の docstring が古く、export /
copy のソースが `page.title` / `page.content` だと記述していたが、実際の実装は
直前のコミット (`83aee43`) で `usePagePublicContent` 経由 (`publicContent.title`
/ `content_text`) に切り替わっている。実装と仕様コメントの食い違いを解消する。

コメント本体に以下を追記:
- 本文表示と Markdown export / copy の両ソースが `usePagePublicContent` 由来
- `page.content` を読まない理由 (`apiPageToPage` が `""` に落とすため、export が
  常に空になる Codex P1 回帰になっていた)
- `NotePagePublicView` と同じフックを使うので TanStack Query が 1 リクエストに
  dedup する

挙動変更はゼロ。docstring 文言のみの更新。

Test plan:
- bun run format:check — clean
- bunx eslint src/pages/NotePageView.tsx — 0 errors (pre-existing size 警告のみ)

Refs: PR #893 review (CodeRabbit nitpick)

https://claude.ai/code/session_016u6uKXnWVPobXkGjKnmLeR

---------

Co-authored-by: Claude <noreply@anthropic.com>
otomatty added a commit that referenced this pull request May 17, 2026
#889 Phase 3) (#894)

* refactor(#892): retire local Y.js mode and /pages/:id route (#889 Phase 3)

Issue #889 段階的リファクタの Phase 3。Phase 1 (#888) でメタデータ専用
`PUT /api/pages/:id` + 読み取り専用 GET ルートを、Phase 2 (#893) で
`NotePagePublicView` を整備した上で、残った大本命のクリーンアップとして
以下を完全に廃止する。

- `CollaborationManager` の `local` モード(IndexedDB 同期と並行に
  `GET/PUT /api/pages/:id/content` を debounce で叩いて Y.Doc を REST
  保存する経路)を撤去。全ページは所属ノートを持つので、Hocuspocus
  WebSocket 同期に統一する。
- `/pages/:id` および `/page/:id` ルートを撤去。ノートネイティブ経路
  `/notes/:noteId/:pageId` に統合し、`useCreatePage` の戻り値が常に
  `noteId` を持つことを前提に 16 箇所の `navigate(...)` を書き換えた。
- `useCollaboration` API から `mode` / `flushSave` / `setPageTitle` を撤去
  し、`PageEditor/` 配下の重複コンポーネント・フック 22 ファイルを削除。
- Web Clipper / AI チャット / PromoteToWiki / WikiLink dialog などの
  作成フローは `navigate("/notes/:noteId/:pageId", { state: { initialContent } })`
  経由で `NotePageView` に seed を渡し、Hocuspocus `synced` 後に Y.Doc に
  反映する形式に切り替えた。
- サーバ側は `POST /api/pages` レスポンスに `note_id` を追加。PDF 派生
  ページ・ハイライト一覧・グローバル検索の各レスポンスに `note_id` を
  同梱して、クライアントが `/notes/:noteId/:pageId` を組み立てられるように
  した。`GET/PUT /api/pages/:id/content` 本体や `snapshotService.ts` の
  削除は Phase 4 に温存(移行期セーフネット)。
- Phase 3 で `/pages/:id` 専用の `e2e/page-editor.spec.ts` と
  `e2e/wikilink-create-dialog.spec.ts` を削除し、`e2e/auth-mock.ts` の
  `createNewPage` helper を `/notes/:noteId/:pageId` URL を待つように更新。

Issue #889 phase 3 — retires the legacy `local` Y.js REST path and the
top-level `/pages/:id` route. Every page now syncs through Hocuspocus and
navigates under its owning note. `useCollaboration` loses `mode` /
`flushSave` / `setPageTitle`; create flows pass an `initialContent` seed
via React Router state and the editor applies it after the initial sync.
The server's `POST /api/pages` response, the derive-page handler, the
highlight list, and the global search rows now all carry the derived
page's `note_id` so the client never has to ask twice. Phase 4 will
delete the now-orphaned `/api/pages/:id/content` endpoints.

Test plan:
- `bun run lint` — 0 errors (621 pre-existing warnings)
- `bun run format:check` — clean
- `bunx vitest run` (main) — 229 files / 2327 tests pass
- `cd server/api && bunx vitest run` — 95 files / 1280 tests pass

Refs: Issue #892, Phase 1 (#888), Phase 2 (#893)

https://claude.ai/code/session_01CVtupQrUS23UEerQJgPgLH

* fix(#892): finish /pages/:id sweep + guard noteId on note-scoped nav

PR #894 のレビューフィードバックを反映。Codex P1 二件と CodeRabbit Major
複数件への対応。

- 取り残されていた `/pages/:id` リンク発火を全て撤去(Codex P1):
  - `AIChatWikiLink` の `<Link to="/pages/...">` を `page.noteId` 付きに。
  - `Onboarding` 完了後の `welcome_page_id` 遷移は新規 `welcome_page_note_id`
    と組で `/notes/:noteId/:pageId` へ。サーバ側 `WelcomePageCreationResult`
    に `noteId` を追加し、`POST /api/onboarding/complete` / `GET
    /api/onboarding/status` も `welcome_page_note_id` を返す。
  - `IndexPage` の `__index__` ページリンクは `/api/activity/index` /
    `/api/activity/index/rebuild` が新たに返す `noteId` を使う。
    `PersistIndexResult` / `rebuildIndexForOwner` も `noteId` を伝搬。
- `NotePageView` の `pendingInitialContent` を location.key 変更で再水和
  できるよう同期 derived-state 化(Codex P1: NotePageView マウント中の
  intra-route navigation で seed が落ちていた)。`useEffect + setState` は
  `you-might-not-need-an-effect` ルールに引っかかるため避け、ref 経由の
  最終適用キー記録は新 ルールの "Cannot access refs during render" を
  踏むため、適用後の navigate クリアが `locationStateInitialContent` を
  undefined にする副作用に依存して二重適用を防ぐ。
- `noteId` 欠落時に `/notes/undefined/...` を組み立てるのを防ぐガードを
  AI チャット作成系・PromoteToWikiDialog に追加(CodeRabbit Major)。
- `useCreateNewPage` / `useFloatingActionButtonHandlers` の link-failed
  パスを「ページは作成済み」前提でデフォルトノート配下にフォールバック
  遷移し、`common.attachPageToNoteFailed` (新規 i18n key) で正確な失敗
  内容を伝える(CodeRabbit Minor)。
- `NotePageView` の冗長な `?? undefined` を削除(CodeRabbit nitpick)。
- Knip 失敗 (`src/hooks/useTitleValidation.ts` が unused) を解消。Phase 3
  で `PageEditor/usePageEditorState.ts` が唯一の consumer だったので
  ファイル自体も削除した。

PR #894 review responses (Codex P1 + CodeRabbit Major):

- Migrate the remaining `/pages/:id` emitters Codex caught:
  `AIChatWikiLink`, `Onboarding` welcome-page landing, and the `IndexPage`
  `__index__` link. Server-side adds `welcome_page_note_id` to the
  onboarding endpoints and `noteId` to the activity-index GET / rebuild
  responses + the `PersistIndexResult` shape.
- Rehydrate `pendingInitialContent` on intra-mount `/notes/:noteId/:pageId`
  navigations (Codex P1). Uses the synchronous "derived state from
  location.key" pattern to satisfy the new
  `you-might-not-need-an-effect` lint rule without reading refs during
  render.
- Guard `noteId` (alongside `id`) before building `/notes/:noteId/:pageId`
  in `runAIChatAction`, `useAIChatActions`, and `PromoteToWikiDialog` so
  partial backend responses never produce `/notes/undefined/...`.
- `useCreateNewPage` / `useFloatingActionButtonHandlers` now fall back to
  the page's default-note URL when `addPageToNoteMutation` fails (the
  page itself was created successfully) and surface a new
  `common.attachPageToNoteFailed` toast so the message matches the actual
  failure (CodeRabbit Minor).
- Drop the unused `useTitleValidation` hook (only consumer was the
  Phase-3-deleted `usePageEditorState.ts`) — this was the Knip CI
  failure.

Test plan:
- `bun run lint` — 0 errors
- `bun run format:check` — clean
- `DATABASE_URL=postgres://... bunx knip` — clean, exit 0
- `bunx vitest run` (main) — 229 files / 2327 tests pass
- `cd server/api && bunx vitest run` — 95 files / 1280 tests pass

Refs: PR #894 (#894)

https://claude.ai/code/session_01CVtupQrUS23UEerQJgPgLH

* fix(#892): tighten Phase 3 review nits (stale seed, conflict-read, bilingual)

PR #894 の追加レビュー(CodeRabbit)への対応。

- `NotePageView` で `location.key` が変化するたびに
  `pendingInitialContent` を現在の location.state から無条件に再水和する
  ように修正。state なしの隣ページ遷移で未消費 seed が残り、
  `<NotePageEditorEditable key={page.id}>` 再マウント先に古い
  initialContent が渡る回帰を防ぐ。
- `welcomePageService.findExistingWelcomePage` の戻り値を `{ id, noteId }`
  に拡張し、conflict-read パスでも永続化済みの note_id を返すように。
  並行 insert で勝者ノートと自前で resolve したデフォルトノートがずれた
  場合に、不正な遷移先 URL を生成しないようにする。
- `apiClient.getOnboardingStatus` の `welcome_page_note_id` と
  `IndexPage` の `IndexRebuildResponse.noteId` / `IndexViewModel.noteId`
  のコメントをバイリンガル化(プロジェクト規約 `**/*.{ts,tsx,js,md}` は
  日本語・英語の両方を要求)。

CodeRabbit follow-up review fixes:

- `NotePageView`: unconditionally re-sync `pendingInitialContent` from the
  current location on every `location.key` change. Without this, a route
  change without `location.state.initialContent` retained the prior
  pending seed and could leak it into the next `key={page.id}` remount.
- `welcomePageService`: return the persisted `note_id` from the
  conflict-read branch so a concurrent insert that landed in a different
  note than the local `ensureDefaultNote` resolution still produces a
  valid `/notes/:noteId/:pageId` URL.
- `apiClient.ts` / `IndexPage.tsx`: extend the new shorthand TSDoc to be
  bilingual per the repo coding guideline.

Test plan:
- `bun run lint` — 0 errors
- `bun run format:check` — clean
- `bunx vitest run src/pages/NotePageView` — 29 tests pass
- `cd server/api && bunx vitest run src/__tests__/routes/onboarding.test.ts`
  — 11 tests pass

Refs: PR #894 (#894)

https://claude.ai/code/session_01CVtupQrUS23UEerQJgPgLH

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants