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>
This commit is contained in:
230
CLAUDE.md
Normal file
230
CLAUDE.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user