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>
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
pathfield 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 DBDocumentService::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/commonmarkprocessLinks()- Convert[[wiki-links]]to HTML anchors
DocumentLink (app/Models/DocumentLink.php)
- Stores relationships between documents via
[[wiki-links]] source_document_id→target_document_idtarget_document_idcan be NULL for uncreated pages
RecentDocument (app/Models/RecentDocument.php)
- Tracks user document access history
- No timestamps, uses
accessed_atfield
Livewire Components
Full-page components:
DocumentViewer- Displays rendered Markdown with backlinksDocumentEditor- Unified create/edit interface with EasyMDEQuickSwitcher- 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):
- Place
wire:ignoreon the editor container - Use a hidden
<input type="hidden" wire:model="content">outsidewire:ignore - Update hidden input value from JS to sync with Livewire
- Keep error messages outside
wire:ignoreto prevent Alpine.js context errors
Wiki-Link Processing
- Extraction: Regex
/\[\[([^\]]+)\]\]/finds all[[Title]]patterns - Storage: Created as
DocumentLinkrecords - Rendering:
Document::processLinks()converts to HTML anchors - Uncreated pages: Links with
NULLtarget_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:ignoreplacement (should not wrap error messages)$wireaccess is inside proper Livewire context- Alpine components are registered in
alpine:initevent
Editor Not Syncing
If EasyMDE changes don't save:
- Verify hidden input exists outside
wire:ignore - Check JS is dispatching
inputevent withbubbles: 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