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>
231 lines
6.8 KiB
Markdown
231 lines
6.8 KiB
Markdown
# 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`
|