Files

231 lines
6.8 KiB
Markdown
Raw Permalink Normal View History

# 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 `<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
### 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`