Files
knowledge_base/CLAUDE.md
Yutaka Kurosaki 6e7f8566ef Implement ID-based routing and folder auto-generation from titles
Major features:
- Switch from slug-based to ID-based routing (/documents/123)
- Enable title editing with automatic slug/path regeneration
- Auto-generate folder structure from title slashes (e.g., Laravel/Livewire/Components)
- Persist sidebar folder open/close state using localStorage
- Remove slug unique constraint (ID routing makes it unnecessary)
- Implement recursive tree view with multi-level folder support

Architecture changes:
- DocumentService: Add generatePathAndSlug() for title-based path generation
- Routes: Change from {document:slug} to {document} for ID binding
- SidebarTree: Extract recursive rendering to partials/tree-item.blade.php
- Database: Remove unique constraint from documents.slug column

UI improvements:
- Display only last path component in sidebar (Components vs Laravel/Livewire/Components)
- Folder state persists across page navigation via localStorage
- Title field accepts slashes for folder organization

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 09:41:38 +09:00

6.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is an Obsidian-like knowledge base system built with Laravel 12, Livewire v3, and Alpine.js. Documents are stored in MySQL database as Markdown content with full-text search support. The system supports wiki-style [[links]] between documents, backlinks, and a quick switcher (Ctrl+K).

Docker Environment

All commands must be run inside Docker containers. The project uses a custom Docker setup with service name prefixes (kb_):

# Docker services
docker compose up -d              # Start all services
docker compose exec php [cmd]     # Run commands in PHP container
docker compose exec php bash      # Access PHP container shell

Port mappings:

  • Nginx: localhost:9700 (main application)
  • phpMyAdmin: localhost:9701
  • MySQL: localhost:9702
  • MailHog: localhost:9725

Common Development Commands

Running the Application

# Build and start Docker
docker compose up -d

# Inside src/, build frontend assets
npm run build                     # Production build
npm run dev                       # Development mode with hot reload

# Run full dev environment (inside container)
composer dev                      # Starts server, queue, logs, and Vite concurrently

Database Operations

# Run migrations
docker compose exec php php artisan migrate

# Seed initial user
docker compose exec php php artisan db:seed --class=UserSeeder

# Initialize sample documents
docker compose exec php php artisan docs:init

Asset Management

# Publish Livewire assets (required after Livewire updates)
docker compose exec php php artisan livewire:publish --assets

# Build Tailwind CSS
cd src && npm run build

Testing

composer test                     # Run PHPUnit tests
docker compose exec php php artisan test

Architecture

Database-First Design

The knowledge base uses a database-first architecture:

  • All document content, metadata, and rendered HTML are stored in MySQL
  • The path field is a virtual path for organizing documents in directories
  • Full-text search uses MySQL FULLTEXT index with ngram tokenizer for multilingual support

Data flow:

  • DocumentService::createDocument() - Creates document directly in DB
  • DocumentService::updateDocument() - Updates document in DB
  • Document content is rendered to HTML on save using league/commonmark

Core Models

Document (app/Models/Document.php)

  • Represents a Markdown document
  • Key methods:
    • syncLinks() - Extract and save [[wiki-links]]
    • renderMarkdown($content) - Convert MD to HTML using league/commonmark
    • processLinks() - Convert [[wiki-links]] to HTML anchors

DocumentLink (app/Models/DocumentLink.php)

  • Stores relationships between documents via [[wiki-links]]
  • source_document_idtarget_document_id
  • target_document_id can be NULL for uncreated pages

RecentDocument (app/Models/RecentDocument.php)

  • Tracks user document access history
  • No timestamps, uses accessed_at field

Livewire Components

Full-page components:

  • DocumentViewer - Displays rendered Markdown with backlinks
  • DocumentEditor - Unified create/edit interface with EasyMDE
  • QuickSwitcher - Modal search (Ctrl+K)

Nested components:

  • SidebarTree - File tree navigation

Route Organization

Routes use ID-based routing instead of slug-based:

// ID-based routing (current implementation)
Route::get('/create', ...)        // Specific route first
Route::get('/{document}', ...)    // ID-based route (uses auto route model binding)
Route::get('/{document}/edit', ...)

// URLs look like: /documents/123 instead of /documents/my-document-slug

Benefits of ID-based routing:

  • Guaranteed uniqueness (no slug conflicts)
  • Title changes don't break URLs
  • Simpler route generation: route('documents.show', $document)

Document Path Auto-Generation

Document paths and slugs are automatically generated from titles:

Title with slashes creates folder hierarchy:

// Title: "Laravel/Livewire/Components"
// → path: "Laravel/Livewire/Components.md"
// → slug: "components" (from last path component)

// Title: "Getting Started"
// → path: "Getting Started.md"
// → slug: "getting-started"

Implementation:

  • DocumentService::generatePathAndSlug($title) - Converts title to path and slug
  • Titles are editable - path and slug automatically update on title change
  • No manual directory field needed - just use / in the title
  • Sidebar automatically organizes documents into folders based on path

Search Implementation

Uses MySQL FULLTEXT index with ngram tokenizer for multilingual support:

FULLTEXT INDEX documents_search_index (title, content) WITH PARSER ngram

Query via: Document::search($query)->limit(10)->get()

Key Technical Patterns

Livewire v3 with wire:ignore

When using wire:ignore with third-party JS libraries (like EasyMDE):

  1. Place wire:ignore on the editor container
  2. Use a hidden <input type="hidden" wire:model="content"> outside wire:ignore
  3. Update hidden input value from JS to sync with Livewire
  4. Keep error messages outside wire:ignore to prevent Alpine.js context errors
  1. Extraction: Regex /\[\[([^\]]+)\]\]/ finds all [[Title]] patterns
  2. Storage: Created as DocumentLink records
  3. Rendering: Document::processLinks() converts to HTML anchors
  4. Uncreated pages: Links with NULL target_document_id show as red

Important Configuration

Tailwind Typography

The @tailwindcss/typography plugin is required for Markdown rendering. Global styles in resources/css/app.css:

@layer components {
    .prose .wiki-link {
        @apply text-indigo-600 hover:text-indigo-800 underline;
    }
    .prose .wiki-link-new {
        @apply text-red-600 hover:text-red-800;
    }
}

Default User

Initial user created via UserSeeder:

  • Email: admin@example.com
  • Password: password

Debugging Tips

Livewire Issues

If Livewire assets return 404:

docker compose exec php php artisan livewire:publish --assets

If Alpine.js errors occur with Livewire components, check:

  • wire:ignore placement (should not wrap error messages)
  • $wire access is inside proper Livewire context
  • Alpine components are registered in alpine:init event

Editor Not Syncing

If EasyMDE changes don't save:

  • Verify hidden input exists outside wire:ignore
  • Check JS is dispatching input event with bubbles: true
  • Ensure wire:model="content" is on hidden input

Route Conflicts

If /create returns 404:

  • Verify specific routes are defined before dynamic /{slug} routes
  • Check route list: docker compose exec php php artisan route:list