REVELUP
shwld
shwld
作ることに関わるひとを幸福にしたい スキ: 個人開発 設計 アジャイル 関数型 DDD

Notes

コードブロック内の@メンションは変換しない

コメント本文中の @userId を @displayName に変換するとき、コードブロック内のトークンまで置換してしまうと表示が崩れる。マークダウンをコードブロック境界で分割してから、非コード部分だけ変換するとうまくいった。

具体的には、コードブロックのパターンで本文を split し、セグメントがコードかどうかで処理を切り替える。

const CODE_SEGMENT_PATTERN = /(```[\s\S]*?```|`[^`\n]*`)/g;

function renderWithDisplayMentions(body: string, members: Member[]): string {
  const idToName = new Map(members.map(m => [m.id, m.displayName]));

  return body
    .split(CODE_SEGMENT_PATTERN)
    .map(segment =>
      isCodeSegment(segment)
        ? segment                          // コードはそのまま
        : replaceMentions(segment, idToName) // テキストだけ変換
    )
    .join("");
}

function isCodeSegment(s: string) {
  return (s.startsWith("```") && s.endsWith("```")) ||
         (s.startsWith("`") && s.endsWith("`"));
}

split に capture group を使うことで、コードブロック自体も配列の要素として残るのがポイント。capture group がないと区切り文字が消えてしまう。

メンションの置換時は displayName にマークダウン特殊文字が含まれうるのでエスケープも忘れずに。

5 months ago
Friend only
This note is available to friends only.
5 months ago
Sign in to read

自サイトへのリンクにnoreferrerを付けるとアクセス元がわからなくなる

target="_blank" のリンクに rel="noopener noreferrer" を機械的に付けがちだが、noreferrer は Referer ヘッダーを送らなくする。自サイト内のリンク(例: サービスサイトからサポートサイトへ)に付けると、遷移先のアクセス解析で「どこから来たか」が取れなくなり、direct traffic として計上されてしまう。

noopener と noreferrer は別の役割:

  • noopener — 遷移先ページが window.opener 経由で元ページを操作できないようにする(セキュリティ対策)。Referer ヘッダーには影響しない
  • noreferrer — Referer ヘッダーを送らない。遷移先がアクセス元を知れなくなる。noopener の効果も暗黙的に含む

現代のブラウザは target="_blank" に対して noopener 相当の挙動をデフォルトで適用するので、noopener すら省略できる場面が多い。ただし古いブラウザの互換性を考慮して rel="noopener" だけ付けるのが安全な落とし所。

使い分け:

  1. 外部の信頼できないリンク — rel="noopener noreferrer" でよい。Referer を渡す必要がない
  2. 自社・自サービスのリンク — rel="noopener" のみ。Referer を残してアクセス解析を活かす
  3. 内部リンク(同一ドメイン) — 通常は target="_blank" 自体が不要。使うなら rel="noopener" で十分
5 months ago
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read

リッチエディタのテストダブルでも aria 属性を透過する

フォーム入力を textarea からリッチテキストエディタへ差し替えたとき、コンポーネントテストの失敗原因が「機能差」ではなく「アクセシビリティ属性の欠落」になりやすい。今回もモック実装が aria-label を受け渡しておらず、getByRole('textbox', { name: ... }) 系の検証が不安定になった。

テストダブル側で ariaLabel / ariaDescribedBy / ariaInvalid をそのまま textarea へ橋渡しすると、実装を差し替えてもテストの意味を維持しやすい。

vi.mock("../components/rich-text-editor", () => ({
  RichTextEditor: ({ value, onChange, ariaLabel, ariaDescribedBy, ariaInvalid, id }) => (
    <textarea
      id={id}
      role="textbox"
      value={value}
      onChange={(event) => onChange(event.target.value)}
      aria-label={ariaLabel}
      aria-describedby={ariaDescribedBy}
      aria-invalid={ariaInvalid ? "true" : "false"}
    />
  ),
}));

今回やったこと(実施済み)

  • RichTextEditor のテストモックに ariaLabel を追加した
  • 画面テスト側のモックにも同じ属性透過を追加して挙動を統一した
  • 既存フォームテストを再実行し、ロール+名前ベースの探索が安定して通ることを確認した

次に試す(任意)

  • 同種のモックに共通ヘルパーを用意して、属性漏れを防ぐ
5 months ago

一覧UIは summary と detail の取得を二段階に分離する

一覧画面で必要な最小情報だけを先に取り、行の展開や詳細遷移時だけ full detail を別クエリで取得する構成に変えた。これで初回描画時の overfetch を抑えつつ、詳細表示の責務も分離できた。

構成は次の2段階に分ける:

  1. 一覧クエリは常に detail=summary — リスト描画に必要なフィールドだけを取得する。
  2. 詳細クエリは id 単位で遅延取得 — 行展開や詳細表示のタイミングで別取得する。
// list side
query.set("detail", "summary");

// detail side
const query = useQuery({
  queryKey: itemQueryKeys.itemDetail(projectId, itemId),
  queryFn: () => fetchItemDetail(projectId, itemId),
  staleTime: 60_000,
});

この分離で効いたポイントは3つ:

  1. キャッシュ粒度が明確になる — 一覧と詳細で query key を分けると、どちらを invalidate すべきか判断しやすい。
  2. UIの責務と通信の責務が揃う — 「パネルは概要」「展開時に詳細」という画面の意図が、そのままデータ取得設計になる。
  3. 将来のフィールド追加に強い — 詳細にだけ重い関連を足しても、一覧の初期体験に影響しにくい。

今回やったこと(実施済み)

  • 一覧クエリ生成ロジックに detail=summary を追加した
  • API 入力に detailLevel を導入して usecase から repository まで伝搬した
  • 詳細取得フックを追加し、展開時に詳細を個別取得するようにした
  • 関連テストを更新して新しい取得経路を検証した

次に試す(任意)

  • summary/detail で実レスポンスサイズと描画時間を計測し、分離効果を定量化する
5 months ago
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read

matchMediaがない実行環境を先に弾かないとテーマフックが落ちる

テーマを "system" で解決するフックを実装するとき、typeof window !== "undefined" だけのガードだと不十分だった。CI の jsdom 実行時に window はあるのに window.matchMedia が未実装で、TypeError でテストが落ちた。

// NG: window はあるので通るが、matchMedia がない環境で落ちる
if (mode === "system" && typeof window !== "undefined") {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}
// OK: API の存在まで確認してからシステムテーマ判定する
const canUseMatchMedia =
  typeof window !== "undefined" && typeof window.matchMedia === "function";

if (mode === "system" && canUseMatchMedia) {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

同じ条件は useEffect 側(matchMedia(...).addEventListener(...))にも必要だった。初期計算だけ守っても、購読処理で同じ例外が起きる。

ポイントは「SSRかどうか」ではなく「使うブラウザAPIがあるか」を直接判定すること。window の有無を条件にすると、テストランナーや擬似ブラウザ実行環境で落とし穴になる。

5 months ago
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Friend only
This note is available to friends only.
5 months ago
Sign in to read
Next Page