# 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_`): ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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 ```bash 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_id` → `target_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: ```php // 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:** ```php // 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: ```sql 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 `` 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 ### Wiki-Link Processing 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`: ```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: ```bash 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`