Skip to content

Comments

Article/Blog機能の実装(Google AdSense審査対応)#139

Open
sugiwe wants to merge 14 commits intomainfrom
feature/article-system
Open

Article/Blog機能の実装(Google AdSense審査対応)#139
sugiwe wants to merge 14 commits intomainfrom
feature/article-system

Conversation

@sugiwe
Copy link
Owner

@sugiwe sugiwe commented Feb 15, 2026

概要

Google AdSense審査の「低品質コンテンツ」フィードバックに対応するため、記事/ブログ機能を実装しました。

実装内容

1. Article/Blog基本機能(前回セッション)

  • ✅ Articleモデル(タイトル、スラッグ、本文、カテゴリー、公開日時)
  • ✅ 管理者CRUD(/admin/articles
  • ✅ 公開ページ(/articles/articles/:slug
  • ✅ Markdownレンダリング(Redcarpet)
  • ✅ SEO/OGP対応(動的メタタグ、Twitter Card)

2. UI/UX改善

  • ✅ Markdownリスト表示修正(箇条書き・番号付きリスト)
  • ✅ サムネイル画像のアスペクト比変更(3:2 → 2:1、デスクトップ最適化)
  • ✅ 管理者ダッシュボードに記事管理リンク追加
  • ✅ 記事詳細ページに管理者専用編集ボタン追加

3. 画像管理機能(ActiveStorage)

  • ✅ ActiveStorage導入(VPS local storage使用)
  • ✅ サムネイル画像(1枚、2:1アスペクト比)
  • ✅ 本文用画像(複数枚、クリックでMarkdownパスコピー)
  • ✅ 画像削除機能(確認ダイアログ付き)
  • ✅ 既存画像の保持ロジック(空配列時にimagesパラメータを削除)
  • ✅ 画像バリデーション(5MB制限、JPEG/PNG/GIF/WebP)

4. コンテンツ準備

  • ✅ 記事下書き作成(「分割キーボードとは?メリット・デメリット完全ガイド」)

技術的ハイライト

ActiveStorage画像管理

  • has_one_attached :thumbnail - サムネイル画像
  • has_many_attached :images - 本文用画像(複数)
  • url_for() で絶対URL生成(Markdown表示用)

フォーム問題の解決

  • フォームネスト禁止対応(button_tolink_to
  • data: { turbo_method: :delete } でDELETEリクエスト送信
  • direct_upload 無効化(JavaScriptエラー回避)

画像削除機能

  • DELETE /admin/articles/:slug/images/:signed_id
  • purge_image アクション(ActiveStorage#purge)

主要な変更ファイル

モデル

  • app/models/article.rb - ActiveStorage attachments、画像バリデーション

コントローラー

  • app/controllers/admin/articles_controller.rb - 画像削除アクション、画像パラメータ処理

ビュー

  • app/views/admin/articles/_form.html.slim - 画像アップロードUI、削除ボタン
  • app/views/articles/index.html.slim - サムネイル表示(2:1)
  • app/views/articles/show.html.slim - サムネイル表示、管理者編集ボタン

マイグレーション

  • db/migrate/20260215062859_create_active_storage_tables.active_storage.rb - ActiveStorage導入

コンテンツ

  • docs/articles/0_分割キーボードとは?メリット・デメリット完全ガイド.md - 記事下書き

セキュリティ

  • ✅ RuboCop 0違反
  • ✅ Brakeman 0警告
  • ✅ Strong Parameters実装
  • ✅ 画像バリデーション(サイズ、形式)

テスト

  • ✅ 既存のRSpecテストは全てパス
  • ✅ 手動テスト完了(画像アップロード、削除、Markdown表示)

次のステップ

  1. PRマージ後、本番環境デプロイ
  2. 記事コンテンツの作成・公開
  3. Google AdSense再審査リクエスト

🤖 Generated with Claude Code

sugiwe and others added 12 commits February 15, 2026 09:31
- CLAUDE.mdをシンプルなインデックス型に刷新(Day表記を全削除)
- ドキュメントをカテゴリ別に整理:
  - docs/design/: 設計ドキュメント(機能仕様、キーボードタイプ、セキュリティ)
  - docs/operations/: 運用ドキュメント(デプロイ、バックアップ、監視、メンテナンス)
  - docs/development/: 開発ドキュメント(テスト、コード品質、国際化)
  - docs/future/: 将来実装予定の設計(Vial連携、指配置カスタマイズ)
  - docs/archive/: 日付付きドキュメント(記録として保持)
- 古いCLAUDE_*.mdファイルを削除(FEATURES、KEYBOARD_TYPE_DESIGN、STABILITY_AND_OPERATIONS、TEST_STRATEGY)
- 既存の運用ドキュメントを適切なディレクトリに移動

oobunプロジェクトの構成を参考に、アプリの仕様が明確にわかるシンプルな構成を実現。

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
- Articleモデル作成(タイトル、スラッグ、コンテンツ、カテゴリー、公開日時)
- カテゴリーenum実装(typing_tips, keyboard_basics, keyboard_reviews, customization, typnix_guides, community)
- スラッグ自動生成機能(重複チェック、一意性保証)
- スコープ実装(published, recent, by_category, popular)
- インスタンスメソッド実装(published?, to_param, increment_view_count!, author_name)
- バリデーション実装(タイトル・コンテンツ必須、スラッグ形式チェック、一意性)
- RSpecテスト26例(全てパス)
- FactoryBot trait実装(draft, scheduled, popular, カテゴリー別)
- マイグレーション実装(NOT NULL制約、インデックス、外部キー)
- 記事アイデア45+件をdocs/future/articles.mdに文書化

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Admin::ArticlesControllerの実装(index, new, create, edit, update, destroy)
- slug-basedルーティング(/admin/articles/:slug)
- フィルター機能(全て・公開中・下書き・予約投稿)
- Kaminariによるページネーション(1ページ20件)
- フォームビュー実装(_form.html.slim)
  - タイトル・スラッグ・カテゴリー・概要・本文(Markdown)・公開日時
  - バリデーションエラー表示
  - 下書き保存・公開ボタン
- 一覧ビュー実装(index.html.slim)
  - ステータスバッジ(公開中・予約投稿・下書き)
  - 閲覧数表示
  - 編集・削除アクション
- ja.ymlに日本語翻訳追加(Article属性・カテゴリー名)
- RuboCop 0違反

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
【公開ページ実装】
- ArticlesControllerの実装(index, show)
- slug-basedルーティング(/articles, /articles/:slug)
- カテゴリーフィルター機能(6カテゴリー)
- 一覧ページ実装(index.html.slim)
  - カードグリッドレイアウト(1列・2列・3列レスポンシブ)
  - カテゴリーバッジ、著者名、公開日、閲覧数表示
  - Kaminariページネーション(1ページ12件)
- 詳細ページ実装(show.html.slim)
  - パンくずリスト
  - Markdown本文レンダリング
  - X(Twitter)シェアボタン
  - 関連記事表示(同カテゴリー3件)
  - 閲覧数カウント自動インクリメント

【Markdownレンダリング実装】
- redcarpet gem導入
- ArticlesHelperにmarkdownメソッド実装
  - リンクは新しいタブで開く(target="_blank", rel="noopener noreferrer")
  - 自動リンク、テーブル、コードブロック、取り消し線、脚注対応
- proseクラス用CSS実装(application.css)
  - 見出し・段落・リンク・コードブロック・リスト・引用・テーブルスタイル
  - ダークモード対応
  - 読みやすい行間・最大幅設定

【SEO/OGP実装】
- Article#meta_descriptionメソッド追加(excerpt優先、なければcontent切り詰め160文字)
- content_for によるページごとのメタタグカスタマイズ対応
- OGPタグ実装(title, description, type, url, image, site_name, locale)
- Twitter Card実装(summary_large_image)
- 記事固有のOGPタグ(article:published_time, article:author)
- レイアウトファイル修正(application.html.slim, _head.html.slim)

【その他】
- RuboCop 0違反
- 全てのタスク完了(Article機能実装完了)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- activerecord.attributes.article.category/* を category/ プレフィックス形式に変更
- I18n::InvalidPluralizationData エラーを解消
- Article model specs 全26例パス

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- to_s(:delimited) を number_with_delimiter() に変更
- 管理者画面・一覧ページ・詳細ページの3箇所を修正
- TypeError: no implicit conversion of Symbol into Integer を解消

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- flexboxで横並び配置に変更
- gap-3で適切な間隔を確保
- button_toによるform要素のずれを解消

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- .prose.max-w-none で最大幅制限を無効化
- 記事本文とシェアエリアの幅を統一
- 読みやすさを向上

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- .prose ul に list-style-type: disc を追加(箇条書き「•」を表示)
- .prose ol に list-style-type: decimal を追加(番号付きリスト「1.」を表示)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
デスクトップ表示時に画面を覆いすぎる問題を解消するため、
よりスマートな横長の2:1アスペクト比に変更。

- 管理者フォーム: ラベル・説明文・プレビュー画像を2:1に変更
- 記事一覧ページ: カードのサムネイルを2:1に変更 (600×300px)
- 記事詳細ページ: サムネイルを2:1に変更 (1200×600px)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Google AdSense審査対応として、記事にサムネイルと本文用画像を
アップロード・管理できる機能を実装。

## 実装内容

### ActiveStorage導入
- rails active_storage:install 実行
- image_processing gem (既存)
- VPS local storage 使用 (Kamal volumes設定済み)

### Articleモデル
- has_one_attached :thumbnail (サムネイル画像)
- has_many_attached :images (本文用画像、複数)
- thumbnail_validation (5MB制限、JPEG/PNG/GIF/WebP)

### 画像アップロード機能
- サムネイル: 1枚、2:1アスペクト比でプレビュー表示
- 本文用画像: 複数枚、クリックでMarkdownパスをコピー
- url_for() で絶対URL生成 (Markdown表示用)
- 既存画像の保持ロジック (空配列時にimagesパラメータを削除)

### 画像削除機能
- DELETE /admin/articles/:slug/images/:signed_id
- purge_imageアクション (ActiveStorage#purge)
- フォームネスト回避のためlink_to使用 (button_toから変更)

### UI改善
- 管理者ダッシュボードに「📝 記事管理」ボタン追加
- 記事詳細ページに管理者専用「編集」ボタン追加
- 編集ページの「記事を表示」リンク修正
- 画像削除ボタン (×アイコン、確認ダイアログ付き)
- 2列グリッド表示

## 技術的対応

### フォーム問題の解決
- direct_upload無効化 (JavaScriptエラー回避)
- button_to → link_to 変更 (フォームネスト禁止のため)
- data: { turbo_method: :delete } でDELETEリクエスト送信

### 画像パス問題の解決
- rails_blob_path → url_for に変更
- 絶対URL生成でMarkdown内の画像が正しく表示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Google AdSense審査対応として、分割キーボードに関する
有益なコンテンツを作成。

## 記事内容
- タイトル: 分割キーボードとは?メリット・デメリット完全ガイド
- カテゴリー: 分割キーボード入門
- 対象読者: 初心者向け

## 構成
1. 分割キーボードの基本(種類、特徴)
2. メリット(肩こり軽減、手首負担軽減、カスタマイズ性)
3. デメリット(価格、慣れるまでの期間、持ち運び)
4. 選び方のポイント
5. よくある質問(FAQ)

docs/articles/ ディレクトリに下書きとして保存。
今後、管理画面から公開予定。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello @sugiwe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

このプルリクエストは、Google AdSenseの審査基準である「低品質コンテンツ」のフィードバックに対応するため、記事/ブログ機能を追加するものです。これにより、アプリケーションはよりリッチなコンテンツを提供できるようになり、SEOの強化とユーザーエンゲージメントの向上が期待されます。記事の作成、編集、公開、画像管理、そして公開記事の閲覧といった一連の機能が実装され、管理者は記事コンテンツを柔軟に運用できるようになります。

Highlights

  • 記事/ブログ機能の基本実装: Google AdSense審査のフィードバックに対応するため、記事/ブログ機能が追加されました。Articleモデル、管理者用CRUD、公開ページ、Markdownレンダリング、SEO/OGP対応が含まれます。
  • UI/UX改善: Markdownリスト表示の修正、サムネイル画像のアスペクト比変更(3:2 → 2:1)、管理者ダッシュボードへの記事管理リンク追加、記事詳細ページへの管理者専用編集ボタン追加など、ユーザーインターフェースとエクスペリエンスが向上しました。
  • 画像管理機能の導入: ActiveStorageが導入され、サムネイル画像(1枚)と本文用画像(複数枚)のアップロード・管理が可能になりました。画像削除機能、既存画像の保持ロジック、5MB制限やJPEG/PNG/GIF/WebP形式のバリデーションも実装されています。
  • 技術的ハイライト: ActiveStorageのhas_one_attachedとhas_many_attachedを活用した画像管理、フォームネスト問題の解決(button_toからlink_toへの変更とdata: { turbo_method: :delete }の使用)、direct_uploadの無効化、およびActiveStorage#purgeを利用した画像削除アクションが実装されました。
  • ドキュメントの整理: 既存のCLAUDE.mdが大幅に整理・更新され、CLAUDE_FEATURES.mdなどの古いドキュメントが削除されました。また、機能仕様、キーボードタイプ設計、セキュリティ設計、コード品質、テスト戦略、運用・メンテナンスなど、新しい設計ドキュメントがdocsディレクトリに体系的に追加されました。
  • SEO/OGP対応の強化: 記事ごとに動的なメタタグ(タイトル、ディスクリプション)とOGP/Twitter Cardタグが設定されるようになり、SEOとSNSでの表示が最適化されました。
Changelog
  • CLAUDE.md
    • プロジェクトの概要、技術スタック、開発コマンド、URL設計、ディレクトリ構造、ドキュメント索引、開発方針、Git運用、主要機能、データモデル、Kamalコマンド、トラブルシューティング、セキュリティ、参考リンクに関する情報で大幅に更新されました。
    • 以前の冗長な開発ルール、テスト戦略、日報管理、進捗状況、将来実装予定などのセクションが削除され、より簡潔で整理された内容になりました。
  • CLAUDE_FEATURES.md
    • ファイルが削除されました。
  • CLAUDE_KEYBOARD_TYPE_DESIGN.md
    • ファイルが削除されました。
  • CLAUDE_STABILITY_AND_OPERATIONS.md
    • ファイルが削除されました。
  • CLAUDE_TEST_STRATEGY.md
    • ファイルが削除されました。
  • Gemfile
    • Markdownレンダリング用のredcarpet gemが追加されました。
  • Gemfile.lock
    • redcarpet gemの依存関係が追加されました。
  • app/assets/stylesheets/application.css
    • Markdown記事の表示を整形するための.proseクラスと関連スタイルが追加されました。
    • ダークモード対応のスタイルが.proseクラスに追加されました。
  • app/controllers/admin/articles_controller.rb
    • 記事のCRUD操作(一覧、新規作成、編集、更新、削除)を管理するコントローラーが追加されました。
    • 記事に添付された画像を個別に削除するpurge_imageアクションが追加されました。
    • 記事のフィルタリング(公開中、下書き、予約投稿)機能が実装されました。
    • 画像が選択されていない場合に既存の画像を保持するためのロジックがarticle_paramsに追加されました。
  • app/controllers/articles_controller.rb
    • 公開記事の一覧表示と詳細表示を管理するコントローラーが追加されました。
    • 記事の閲覧数を増加させるincrement_view_count!メソッドが呼び出されるようになりました。
    • カテゴリによる記事のフィルタリング機能が追加されました。
  • app/helpers/admin/articles_helper.rb
    • 管理者向け記事ヘルパーが追加されました。
  • app/helpers/articles_helper.rb
    • Markdown形式のテキストをHTMLにレンダリングするためのmarkdownヘルパーメソッドが追加されました。
  • app/models/article.rb
    • 記事のタイトル、スラッグ、コンテンツ、カテゴリ、公開日時、著者、閲覧数を管理するArticleモデルが追加されました。
    • ActiveStorageを使用してサムネイル画像と本文用画像を添付する機能が追加されました。
    • 記事の公開状態、カテゴリ、閲覧数、公開日時によるスコープが定義されました。
    • スラッグの自動生成、一意性、フォーマットに関するバリデーションが追加されました。
    • サムネイル画像のファイルサイズと形式に関するカスタムバリデーションが追加されました。
  • app/views/admin/articles/_form.html.slim
    • 管理者向けの記事作成・編集フォームのパーシャルが追加されました。
    • タイトル、スラッグ、カテゴリ、概要、本文(Markdown)、公開日時を入力するフィールドが含まれます。
    • サムネイル画像と本文用画像のアップロードUIが追加され、アップロード済みの本文用画像にはMarkdownパスをコピーする機能と削除ボタンが提供されます。
  • app/views/admin/articles/edit.html.slim
    • 管理者向けの記事編集ページが追加されました。
    • 公開中の記事へのリンクと、記事編集フォームが含まれます。
  • app/views/admin/articles/index.html.slim
    • 管理者向けの記事一覧ページが追加されました。
    • 記事のタイトル、カテゴリ、ステータス、閲覧数、公開日時を表示するテーブルが含まれます。
    • 記事のフィルタリング(全て、公開中、下書き、予約投稿)とページネーション機能が追加されました。
    • 新規記事作成、編集、削除のアクションリンク/ボタンが提供されます。
  • app/views/admin/articles/new.html.slim
    • 管理者向けの新規記事作成ページが追加されました。
    • 記事作成フォームが含まれます。
  • app/views/admin/dashboard/index.html.slim
    • 管理者ダッシュボードに「📝 記事管理」へのリンクが追加されました。
  • app/views/articles/index.html.slim
    • 公開記事の一覧ページが追加されました。
    • カテゴリによるフィルター機能と、記事をカード形式で表示するグリッドレイアウトが含まれます。
    • 記事のサムネイル、カテゴリ、タイトル、概要、著者名、公開日、閲覧数が表示されます。
  • app/views/articles/show.html.slim
    • 記事の詳細ページが追加されました。
    • パンくずリスト、カテゴリバッジ、記事タイトル、サムネイル画像、著者名、公開日、閲覧数が表示されます。
    • Markdown形式の本文がレンダリングされ、管理者には編集ボタンが表示されます。
    • X(旧Twitter)シェアボタンと関連記事の表示機能が含まれます。
    • 記事固有のSEO/OGPタグが設定されるようになりました。
  • app/views/layouts/_head.html.slim
    • メタディスクリプションがcontent_for(:meta_description)で動的に設定されるように変更されました。
  • app/views/layouts/application.html.slim
    • ページのタイトルがcontent_for(:page_title)で動的に設定されるように変更されました。
    • OGP/Twitter Cardのタイトル、ディスクリプション、タイプ、URLがcontent_forヘルパーを使用して動的に設定されるように変更されました。
    • 記事固有のOGPタグ(公開日時、著者)が条件付きで追加されるようになりました。
  • config/locales/ja.yml
    • articleモデルの属性名(タイトル、スラッグ、コンテンツなど)の翻訳が追加されました。
    • articleカテゴリのenum値(タイピングのコツ、キーボード基礎知識など)の翻訳が追加されました。
  • config/routes.rb
    • 公開用のarticlesリソース(一覧、詳細)がparam: :slugで追加されました。
    • 管理者用のadmin/articlesリソースがparam: :slugで追加され、記事に添付された画像を削除するためのimages/:signed_idルートがネストされました。
  • db/migrate/20260215010200_create_articles.rb
    • articlesテーブルを作成するマイグレーションファイルが追加されました。タイトル、スラッグ、コンテンツ、カテゴリ、公開日時、著者参照、閲覧数などのカラムが含まれます。
  • db/migrate/20260215062859_create_active_storage_tables.active_storage.rb
    • Active Storageのactive_storage_blobsactive_storage_attachmentsactive_storage_variant_recordsテーブルを作成するマイグレーションファイルが追加されました。
  • db/schema.rb
    • Active Storage関連のテーブル(active_storage_attachments, active_storage_blobs, active_storage_variant_records)のスキーマ定義が追加されました。
    • articlesテーブルのスキーマ定義が追加され、usersテーブルへの外部キー制約が追加されました。
  • docs/2026-01-09-レッスンタイトルなどの翻訳メモ.md
    • ファイルがdocs/archive/2026-01-09-レッスンタイトルなどの翻訳メモ.mdにリネームされました。
  • docs/2026-01-10-キーボードタイプ管理機能の設計.md
    • ファイルがdocs/archive/2026-01-10-キーボードタイプ管理機能の設計.mdにリネームされました。
  • docs/2026-01-11-プログラミング用語系のレッスン追加.md
    • ファイルがdocs/archive/2026-01-11-プログラミング用語系のレッスン追加.mdにリネームされました。
  • docs/2026-01-12-Vial形式キーマップ連携機能の設計.md
    • ファイルがdocs/future/vial_integration.mdにリネームされました。
  • docs/2026-01-12-finger-assignment-customization.md
    • ファイルがdocs/future/finger_assignment.mdにリネームされました。
  • docs/2026-01-19-日本語入力への対応.md
    • ファイルがdocs/archive/2026-01-19-日本語入力への対応.mdにリネームされました。
  • docs/2026-01-20-日本語レッスン案.md
    • ファイルがdocs/archive/2026-01-20-日本語レッスン案.mdにリネームされました。
  • docs/2026-01-21-日本語レッスン案(2025人気作品).md
    • ファイルがdocs/archive/2026-01-21-日本語レッスン案(2025人気作品).mdにリネームされました。
  • docs/2026-01-21-日本語レッスン案(単語).md
    • ファイルがdocs/archive/2026-01-21-日本語レッスン案(単語).mdにリネームされました。
  • docs/2026-01-21-日本語レッスン案(古典・名作小説).md
    • ファイルがdocs/archive/2026-01-21-日本語レッスン案(古典・名作小説).mdにリネームされました。
  • docs/2026-01-23-日本語レッスン案(文章).md
    • ファイルがdocs/archive/2026-01-23-日本語レッスン案(文章).mdにリネームされました。
  • docs/2026-02-05-PostgreSQL定期メンテナンス設定ガイド.md
    • ファイルがdocs/archive/2026-02-05-PostgreSQL定期メンテナンス設定ガイド.mdにリネームされました。
  • docs/2026-02-10_コード品質チェックリスト.md
    • ファイルがdocs/archive/2026-02-10_コード品質チェックリスト.mdにリネームされました。
  • docs/2026-02-10_未対応の改善項目.md
    • ファイルがdocs/archive/2026-02-10_未対応の改善項目.mdにリネームされました。
  • docs/BACKUP_SETUP.md
    • ファイルがdocs/operations/backup.mdにリネームされました。
  • docs/DEPLOY_GUIDE.md
    • ファイルがdocs/operations/deployment.mdにリネームされました。
  • docs/I18N_DB_WORK_STEPS.md
    • ファイルがdocs/archive/I18N_DB_WORK_STEPS.mdにリネームされました。
  • docs/I18N_PLAN.md
    • ファイルがdocs/development/i18n.mdにリネームされました。
  • docs/SENTRY_SETUP.md
    • ファイルがdocs/operations/monitoring.mdにリネームされました。
  • docs/articles/0_分割キーボードとは?メリット・デメリット完全ガイド.md
    • 分割キーボードに関するガイド記事が追加されました。
  • docs/design/features.md
    • Typnixの機能仕様をまとめたドキュメントが追加されました。
  • docs/design/keyboard_types.md
    • キーボードタイプ対応の設計に関するドキュメントが追加されました。
  • docs/design/security.md
    • Typnixのセキュリティ対策に関するドキュメントが追加されました。
  • docs/development/code_quality.md
    • Typnixのコード品質維持のための指針とチェックリストが追加されました。
  • docs/development/testing.md
    • Typnixのテスト戦略と実装状況をまとめたドキュメントが追加されました。
  • docs/future/articles.md
    • 記事機能の目的、URL設計、データモデル、記事アイデア、実装計画、SEO対策、将来の拡張に関するドキュメントが追加されました。
  • docs/operations/maintenance.md
    • Typnixの運用・メンテナンスに関するガイドが追加されました。
  • spec/factories/articles.rb
    • ArticleモデルのFactoryBot定義が追加されました。下書き、予約投稿、人気記事、特定のカテゴリなどのtraitが含まれます。
  • spec/helpers/admin/articles_helper_spec.rb
    • 管理者向け記事ヘルパーのスペックファイルが追加されました。
  • spec/helpers/articles_helper_spec.rb
    • 記事ヘルパーのスペックファイルが追加されました。
  • spec/models/article_spec.rb
    • Articleモデルのバリデーション、アソシエーション、enum、スコープ、インスタンスメソッド、スラッグ生成に関するテストが追加されました。
  • spec/requests/admin/articles_spec.rb
    • 管理者向け記事コントローラーのルーティングとHTTPステータスに関するリクエストスペックが追加されました。
  • spec/requests/articles_spec.rb
    • 公開記事コントローラーのルーティングとHTTPステータスに関するリクエストスペックが追加されました。
  • spec/views/admin/articles/create.html.tailwindcss_spec.rb
    • 管理者向け記事作成ビューのスペックファイルが追加されました。
  • spec/views/admin/articles/destroy.html.tailwindcss_spec.rb
    • 管理者向け記事削除ビューのスペックファイルが追加されました。
  • spec/views/admin/articles/edit.html.tailwindcss_spec.rb
    • 管理者向け記事編集ビューのスペックファイルが追加されました。
  • spec/views/admin/articles/index.html.tailwindcss_spec.rb
    • 管理者向け記事一覧ビューのスペックファイルが追加されました。
  • spec/views/admin/articles/new.html.tailwindcss_spec.rb
    • 管理者向け新規記事作成ビューのスペックファイルが追加されました。
  • spec/views/admin/articles/update.html.tailwindcss_spec.rb
    • 管理者向け記事更新ビューのスペックファイルが追加されました。
  • spec/views/articles/index.html.tailwindcss_spec.rb
    • 公開記事一覧ビューのスペックファイルが追加されました。
  • spec/views/articles/show.html.tailwindcss_spec.rb
    • 記事詳細ビューのスペックファイルが追加されました。
Activity
  • このプルリクエストは、Claude Codeによって生成されました。現在、人間のレビューやコメントはありません。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@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

このプルリクエストでは、Google AdSenseの審査対応として、包括的な記事・ブログ機能が実装されました。全体的に優れた実装ですが、Redcarpet レンダラーの不適切な設定により、Markdownレンダリングヘルパーに保存型XSSの脆弱性が確認されました。この脆弱性に対処することで、悪意のあるコンテンツが記事データベースに挿入された場合でもアプリケーションのセキュリティが確保されます。また、コントローラーの可読性向上とモデルのバリデーションロジックの簡素化について、改善提案があります。

Comment on lines 5 to 9
renderer = Redcarpet::Render::HTML.new(
filter_html: false,
hard_wrap: true,
link_attributes: { target: "_blank", rel: "noopener noreferrer" }
)

Choose a reason for hiding this comment

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

security-medium medium

The markdown helper is vulnerable to Stored Cross-Site Scripting (XSS). It initializes the Redcarpet HTML renderer with filter_html: false (line 6) and then marks the rendered output as html_safe (line 24). This allows raw HTML tags and malicious scripts embedded in the article content to be rendered and executed in the browser of any user viewing the article. While article content is currently provided by administrators, this still poses a significant risk if an admin account is compromised or if a malicious internal actor is involved. Additionally, the renderer does not use safe_links_only: true, which could allow javascript: URIs in Markdown links.

To remediate this, set filter_html: true and safe_links_only: true in the renderer options. This will ensure that any HTML tags in the input are escaped or stripped, and only safe protocols are allowed for links.

    renderer = Redcarpet::Render::HTML.new(
      filter_html: true,
      safe_links_only: true,
      hard_wrap: true,
      link_attributes: { target: "_blank", rel: "noopener noreferrer" }
    )

Comment on lines 7 to 16
@articles = case @filter
when "published"
Article.published.recent
when "draft"
Article.where(published_at: nil).recent
when "scheduled"
Article.where("published_at > ?", Time.current).recent
else
Article.recent
end

Choose a reason for hiding this comment

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

medium

フィルターロジックが各case節で完結していますが、より宣言的でDRYな書き方にリファクタリングできます。case文でスコープを決定し、その後に共通のメソッドチェーン(.recent.page)を適用することで、コードの重複を減らし、可読性を向上させることができます。

さらに、Articleモデルにdraftscheduledのスコープを定義すると、コントローラーはより責務に集中でき、コードが綺麗になります。

# app/models/article.rb に追加
scope :draft, -> { where(published_at: nil) }
scope :scheduled, -> { where("published_at > ?", Time.current) }
      scope = case @filter
              when "published"
                Article.published
              when "draft"
                Article.where(published_at: nil)
              when "scheduled"
                Article.where("published_at > ?", Time.current)
              else
                Article.all
              end
      @articles = scope.recent

Comment on lines 18 to 23
validates :slug, uniqueness: true,
format: { with: /\A[a-z0-9-]+\z/ }, length: { maximum: 100 }, allow_blank: true
validates :content, presence: true
validates :category, presence: true
validate :slug_must_exist_after_generation
validate :thumbnail_validation

Choose a reason for hiding this comment

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

medium

スラッグのバリデーションロジックが少し複雑になっているようです。before_validationコールバックでスラッグが必ず生成されることを考慮すると、allow_blank: trueオプションとカスタムバリデーションメソッドslug_must_exist_after_generationは不要になります。

validates :slug, presence: true を使用することで、Railsの標準的な方法でよりシンプルにバリデーションを記述でき、コードの意図が明確になります。この変更に合わせて、63行目から67行目のslug_must_exist_after_generationメソッドも削除してください。

  validates :slug, presence: true, uniqueness: true,
            format: { with: /\A[a-z0-9-]+\z/ }, length: { maximum: 100 }
  validates :content, presence: true
  validates :category, presence: true
  validate :thumbnail_validation

@sugiwe sugiwe self-assigned this Feb 16, 2026
1. XSS脆弱性の修正(articles_helper.rb)
   - filter_html: true に変更(HTMLタグをエスケープ)
   - safe_links_only: true を追加(安全なプロトコルのみ許可)

2. DRY原則の適用(articles_controller.rb、article.rb)
   - Article モデルに draft、scheduled スコープを追加
   - コントローラーの重複コードを削減

3. バリデーションロジックの簡略化(article.rb)
   - slug に presence: true を追加
   - 不要な allow_blank: true と custom validation を削除

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@sugiwe
Copy link
Owner Author

sugiwe commented Feb 20, 2026

/gemini review

Copy link

@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

Google AdSense審査対応のための記事機能の実装、お疲れ様です。ActiveStorageを利用した画像管理やMarkdownレンダリング、SEO/OGP対応など、必要な機能がしっかりと網羅されています。特に、複数画像アップロード時の既存画像保持ロジックや、管理者向けのUI改善など、実用的な配慮がなされている点が高評価です。一方で、画像の削除機能におけるロジックの不備(実行時エラーの可能性)や、翻訳ファイルのキー設定の誤り、日本語タイトル時のスラッグ自動生成に関する挙動など、いくつか修正・改善が必要な点が見受けられました。これらを修正することで、より堅牢で使いやすい機能になるでしょう。

Comment on lines 57 to 63
image = @article.images.find_by(signed_id: params[:signed_id])
if image
image.purge
redirect_to edit_admin_article_path(@article), notice: "画像を削除しました"
else
redirect_to edit_admin_article_path(@article), alert: "画像が見つかりませんでした"
end

Choose a reason for hiding this comment

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

high

signed_id はデータベースのカラムではないため、find_by(signed_id: ...) では検索できず、実行時に ActiveRecord::UnknownAttributeError が発生します。signed_id を使用してレコードを特定するには、ActiveStorage::Attachment.find_signed メソッドを使用する必要があります。また、セキュリティのために、取得した画像が本当に対象の記事(@article)に属しているかを確認するロジックも含めるべきです。

    def purge_image
      image = ActiveStorage::Attachment.find_signed(params[:signed_id])
      if image && image.record == @article
        image.purge
        redirect_to edit_admin_article_path(@article), notice: "画像を削除しました"
      else
        redirect_to edit_admin_article_path(@article), alert: "画像が見つかりませんでした"
      end
    end

Comment on lines 361 to 366
"category/typing_tips": "タイピングのコツ"
"category/keyboard_basics": "キーボード基礎知識"
"category/keyboard_reviews": "キーボードレビュー"
"category/customization": "カスタマイズガイド"
"category/typnix_guides": "Typnix使い方"
"category/community": "コミュニティ"

Choose a reason for hiding this comment

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

medium

Railsの human_attribute_nameenum の翻訳機能は、ドット区切りによるネスト構造を期待します。現在の "category/typing_tips" のようなスラッシュを含んだフラットなキー文字列では、Article.human_attribute_name("category.#{k}") で正しく翻訳が取得されません。category: の下に各キーをネストさせる構造に修正してください。

        category:
          typing_tips: "タイピングのコツ"
          keyboard_basics: "キーボード基礎知識"
          keyboard_reviews: "キーボードレビュー"
          customization: "カスタマイズガイド"
          typnix_guides: "Typnix使い方"
          community: "コミュニティ"

end

def generate_slug
base_slug = title.parameterize

Choose a reason for hiding this comment

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

medium

title.parameterize は日本語などの非ASCII文字をデフォルトで空文字列に変換します。そのため、タイトルが日本語のみの場合、slug が空になり、18行目および19行目のバリデーション(presence: true および format)に失敗して保存できません。日本語タイトルの記事が多いことが予想されるため、空になった場合のフォールバック(ランダムな文字列の付与など)を検討するか、ユーザーにスラッグの入力を促すUI上の配慮があるとより親切です。

1. 画像削除のセキュリティ修正(articles_controller.rb)
   - ActiveStorage::Attachment.find_signed を使用
   - image.record == @Article で所有権を確認
   - 他の記事の画像削除を防止

2. enum翻訳のi18n構造改善(ja.yml、各種ビュー)
   - activerecord.enums.article.category にネスト構造化
   - human_attribute_name から I18n.t に変更
   - Rails i18n規約に準拠

3. 日本語タイトル対応(article.rb)
   - title.parameterize が空の場合、SecureRandom.hex(8) をフォールバック
   - 日本語タイトルの記事も確実に保存可能

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@sugiwe
Copy link
Owner Author

sugiwe commented Feb 21, 2026

/gemini review

Copy link

@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

Google AdSense審査対応のための記事/ブログ機能の実装、お疲れ様です。ActiveStorageを利用した画像管理やMarkdownレンダリング、SEO対応など、必要な機能が網羅的に実装されています。特に、has_many_attachedにおける既存画像の保持ロジックや、フォームのネスト回避など、Rails特有のハマりどころが適切に処理されている点が高評価です。一方で、Markdownレンダリングの効率化や、画像バリデーションの適用範囲、ビューにおける不要なCSSクラスの整理など、いくつか改善の余地が見受けられました。

Comment on lines +2 to +25
def markdown(text)
return "" if text.blank?

renderer = Redcarpet::Render::HTML.new(
filter_html: true, # HTMLタグをエスケープ(XSS対策)
safe_links_only: true, # 安全なプロトコル(http/https)のみ許可
hard_wrap: true,
link_attributes: { target: "_blank", rel: "noopener noreferrer" }
)

markdown = Redcarpet::Markdown.new(
renderer,
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true,
space_after_headers: true,
superscript: true,
underline: true,
highlight: true,
footnotes: true
)

markdown.render(text).html_safe

Choose a reason for hiding this comment

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

medium

markdownヘルパーメソッド内で、呼び出しのたびにRedcarpetのインスタンスを生成しています。記事の表示回数やコンテンツ量が増えた際にパフォーマンスに影響を与える可能性があるため、定数として定義して再利用することをお勧めします。

  RENDERER = Redcarpet::Render::HTML.new(
    filter_html: true,
    safe_links_only: true,
    hard_wrap: true,
    link_attributes: { target: "_blank", rel: "noopener noreferrer" }
  )

  MARKDOWN = Redcarpet::Markdown.new(
    RENDERER,
    autolink: true,
    tables: true,
    fenced_code_blocks: true,
    strikethrough: true,
    space_after_headers: true,
    superscript: true,
    underline: true,
    highlight: true,
    footnotes: true
  )

  def markdown(text)
    return "" if text.blank?
    MARKDOWN.render(text).html_safe

format: { with: /\A[a-z0-9-]+\z/ }, length: { maximum: 100 }
validates :content, presence: true
validates :category, presence: true
validate :thumbnail_validation

Choose a reason for hiding this comment

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

medium

PRの説明文には「画像バリデーション(5MB制限、JPEG/PNG/GIF/WebP)」とありますが、現状ではthumbnailのみが対象となっています。本文用画像(images)に対しても同様のバリデーションを適用することをお勧めします。

  validate :thumbnail_validation
  validate :images_validation

unless thumbnail.content_type.in?(%w[image/jpeg image/png image/gif image/webp])
errors.add(:thumbnail, "はJPEG、PNG、GIF、WebP形式のみ対応しています")
end
end

Choose a reason for hiding this comment

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

medium

images用のバリデーションメソッドの実装例です。各添付ファイルに対してサイズと形式のチェックを行います。

  end

  def images_validation
    return unless images.attached?

    images.each do |image|
      if image.blob.byte_size > 5.megabytes
        errors.add(:images, "の各サイズは5MB以下にしてください")
      end

      unless image.content_type.in?(%w[image/jpeg image/png image/gif image/webp])
        errors.add(:images, "はJPEG、PNG、GIF、WebP形式のみ対応しています")
      end
    end
  end

span = number_with_delimiter(@article.view_count) + " 回閲覧"

/ 記事本文(Markdown)
article.prose.prose-lg.dark:prose-invert.max-w-none.bg-white.dark:bg-gray-800.rounded-lg.shadow-md.p-8.mb-8

Choose a reason for hiding this comment

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

medium

prose-lgおよびdark:prose-invertクラスが指定されていますが、application.cssに追加されたスタイル定義にはこれらのクラスが含まれていません。また、ダークモードのスタイルは.dark .proseセレクタで定義されているため、prose-invertは不要です。定義されていないクラスを削除し、CSSの実装と整合性を取ることをお勧めします。

article.prose.max-w-none.bg-white.dark:bg-gray-800.rounded-lg.shadow-md.p-8.mb-8

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.

1 participant