6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Dreamwidth is a Perl-based journaling/blogging platform forked from LiveJournal. It runs on Apache mod_perl with MySQL, Memcached, and TheSchwartz job queue. All code must be GPL-licensed.
Development Environment
Code is edited on the host, but all commands must be run inside the devcontainer (.devcontainer/). The devcontainer runs Ubuntu 22.04 with MySQL, Memcached, and Apache mod_perl. The workspace is mounted at /workspaces/dreamwidth ($LJHOME). Perl modules are pre-installed at /opt/dreamwidth-extlib/lib/perl5 ($PERL5LIB).
Container Management
Build and start the devcontainer from the repo root:
npx devcontainer up --workspace-folder .
Find the running container ID:
docker ps --format "{{.ID}} {{.Names}}"
Run commands inside the container (omit -it when not in a TTY, e.g. from Claude Code):
# Interactive shell
docker exec -it <container-id> bash
# Non-interactive command execution (use this from Claude Code)
docker exec <container-id> <command>
Commands (run inside devcontainer)
# Run a single test
perl t/sometest.t
# Check code formatting (must pass before PR)
perl t/02-tidy.t
# Apply code formatting
perl extlib/bin/tidyall
# Check all modules compile (1472 subtests)
perl t/00-compile.t
# Compile static assets (CSS/JS)
bin/build-static.sh
# Restart Apache after code changes
apache2ctl restart
Code Formatting
Enforced via Perl::Tidy (.tidyallrc): Unix line endings, 4-space continuation indent, 100-char line limit. Applies to bin/, cgi-bin/, t/, and worker scripts. Run bin/tidyall to auto-format; t/02-tidy.t validates in CI.
Architecture
Module Namespaces
DW::*— Modern Dreamwidth code (controllers, auth, blob storage, templates)LJ::*— Legacy LiveJournal modules (still heavily used for core entities: users, entries, comments)Apache::*— mod_perl request handlersS2::*— S2 style/theming language compiler
Key Directories
| Directory | Purpose |
|---|---|
cgi-bin/ |
Core Perl modules and CGI scripts (DW::, LJ::, handlers) |
views/ |
Template Toolkit (.tt) templates for page rendering |
htdocs/ |
Static assets (CSS, JS, images) and legacy BML pages |
styles/ |
S2 style layer definitions (theming system) |
bin/ |
CLI utilities, maintenance scripts, worker processes |
t/ |
Test suite (139 test files) |
etc/ |
Config templates and Docker configs |
api/ |
REST API OpenAPI spec (YAML fragments built via Node.js) |
ext/ |
Optional modules (dw-nonfree) |
Request Flow
- Apache mod_perl receives request →
Apache::*handlers DW::Routingdispatches toDW::Controller::*modules- Controllers use
DW::Controllerhelpers (controller(),needlogin(),error_ml(),success_ml()) - Views rendered via
DW::Templateusing Template Toolkit (.ttfiles inviews/) - Legacy pages use BML (Block Markup Language) templates in
htdocs/
Core Entities
- Users:
LJ::User(main class),DW::User(extensions) - Entries:
LJ::Entry, withDW::Entry::*extensions - Comments:
LJ::Comment - Communities:
LJ::Community
Database
Multi-database MySQL architecture with cluster sharding (dw_global, dw_cluster01+). Uses DBI directly and Data::ObjectDriver as a lightweight ORM. Tests can use SQLite via t/bin/initialize-db.
Storage Backends
Media/blob storage via DW::BlobStore with pluggable backends: S3, MogileFS, or local disk.
Job Queue
Background processing via DW::TaskQueue with pluggable backends: SQS (DW::TaskQueue::SQS) or local disk (DW::TaskQueue::LocalDisk). Tasks are defined in DW::Task::*. Legacy jobs use TheSchwartz. Worker scripts in bin/worker/.
Plack Server (development)
The codebase runs under both Apache/mod_perl and Plack/Starman. See doc/PLACK.md for full architecture details (middleware stack, routing, security notes, testing).
# Inside the devcontainer
perl bin/starman --port 8080
This runs a single-worker Starman instance. The Plack entry point is app.psgi. Plack-specific middleware lives in cgi-bin/Plack/Middleware/DW/. The DW::Request abstraction layer (DW::Request::Plack, DW::Request::Apache2) allows most code to work under both servers.
Key differences from Apache:
$r->urimust return the path only (not full URL) —DW::Request::Plackhandles this- In the dev container,
$LJ::DOMAIN,$LJ::SITEROOT, etc. are empty — URLs are built from the request Host header viaLJ::create_url() - BML pages render via
DW::BML(Plack) instead ofApache::BML(mod_perl); both share the same BML engine internals
Dev Container Config
The dev container ($IS_DEV_SERVER && $IS_DEV_CONTAINER) intentionally sets $LJ::DOMAIN = "", $LJ::SITEROOT = "", etc. in LJ::Global::Defaults. This means domain/redirect logic is skipped and URLs are constructed dynamically from request headers. Do not use local to override these globals in middleware — it leaks into downstream code.
Git Workflow
ABSOLUTE RULE — NEVER run git commit unless the user has explicitly asked you to commit in that moment. Not after making changes. Not as part of a workflow. Not proactively. Not because the changes look ready. Not for any reason whatsoever. The ONLY acceptable trigger is the user saying words like "commit this" or "go ahead and commit". If in doubt, ASK. Violating this rule destroys trust. Follow the repository's existing commit message style. NEVER amend commits unless explicitly instructed — assume commits have already been pushed.
Always use --no-gpg-sign when committing, as GPG signing requires interactive passphrase entry which hangs in this environment.
Before Pushing
Before pushing any branch, run these checks inside the devcontainer and fix any failures — CI runs them and the build fails if they don't pass, even for files you didn't touch:
perl extlib/bin/tidyall -a— auto-format all filesperl t/02-tidy.t— verify formatting passesperl t/00-compile.t— verify all modules compile
Pull Requests
PRs target dreamwidth/dreamwidth. If the working repo is a fork, use --head <fork-owner>:<branch-name> (check the origin remote to determine the fork owner).
PR body format:
<Technical description of the change — what code was modified and why, referencing specific
functions/files/mechanics as appropriate. This is for developers reviewing the PR.>
CODE TOUR: <Non-technical description for the Dreamwidth community. Explain what changed from a
user's perspective, skipping implementation details. Keep it conversational.>
Fixes #<issue-number>
Troubleshooting
If the container startup fails (postCreateCommand), the container still exists. Check the error output, fix the issue, remove the container (docker rm <id>), and rebuild.