A modern, secure personal blog application written in Kotlin using the Javalin web framework. This is a complete rewrite of the original Perl/Mojolicious blog with significant improvements in security, maintainability, and developer experience.
- Blog Posts: Create, edit, and manage blog posts with a rich Markdown editor (EasyMDE)
- Draft & Archive Workflow: Save posts as drafts, publish, or archive them
- AI Post Generation: Generate posts using Claude (Anthropic API) with a customisable prompt, editable from the admin panel
- Authentication: Secure user authentication with BCrypt password hashing
- Sketchbook Gallery: Image gallery feature for showcasing artwork
- Holocene Calendar: Dates displayed in the Holocene calendar system (+10,000 years)
- Security: CSRF protection, HTML sanitization, and input validation
- Responsive Design: Mobile-friendly UI using Skeleton CSS framework
- SEO: Meta descriptions, Open Graph tags, canonical URLs, sitemap.xml
- Framework: Javalin 6.x - Lightweight web framework
- Language: Kotlin 1.9.22
- Database ORM: Exposed - JetBrains Kotlin SQL library
- Template Engine: Jte - Fast, type-safe templates
- Database: MySQL 8.0+ (with H2 for testing)
- Connection Pool: HikariCP
- Security: BCrypt for password hashing, OWASP HTML Sanitizer
- Testing: JUnit 5, MockK
- Logging: Logback with kotlin-logging
- JDK 17 or higher
- MySQL 8.0 or higher
- Gradle 8.x (or use the included wrapper)
Create a MySQL database:
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;The application will automatically create the necessary tables on first run via SchemaUtils.createMissingTablesAndColumns().
Create a .env file in the project root or set environment variables:
export DB_HOST=localhost
export DB_PORT=3306
export DB_NAME=blog
export DB_USER=your_username
export DB_PASSWORD=your_password
export SESSION_SECRET=your-secret-key-here
# Optional: enable AI post generation
export ANTHROPIC_API_KEY=sk-ant-...The application reads configuration in this order: .env file → environment variables → defaults.
After starting the application once (so tables are created), insert a user directly into MySQL. The password must be BCrypt-hashed:
# Generate a BCrypt hash for your password (online tools or use a script)
# Then insert:
mysql -u your_username -p blogINSERT INTO User (name, password, updated) VALUES ('yourname', '$2a$10$...bcrypt_hash...', NOW());If you want to use AI post generation, insert the Claude user and model rows:
INSERT INTO User (name, password, is_artificial, updated)
VALUES ('Claude', UUID(), true, NOW());
SET @claude_id = LAST_INSERT_ID();
INSERT INTO AiModel (model_id, name, user_id, created, updated) VALUES
('claude-sonnet-4-6', 'Claude Sonnet 4', @claude_id, NOW(), NOW()),
('claude-opus-4-6', 'Claude Opus 4', @claude_id, NOW(), NOW()),
('claude-haiku-4-5-20251001', 'Claude Haiku 4.5', @claude_id, NOW(), NOW());Then set your AI generation prompt via the admin panel at /admin/prompt.
# Development mode (template hot-reloading)
ENV=development ./gradlew run
# Production mode
./gradlew shadowJar
java -jar build/libs/blog-kotlin-1.0.0-all.jarThe application will start on http://localhost:7070.
./gradlew testblog-kotlin/
├── src/
│ ├── main/
│ │ ├── kotlin/com/keremgokhan/blog/
│ │ │ ├── Application.kt # Main application entry point & routes
│ │ │ ├── config/
│ │ │ │ ├── AppConfig.kt
│ │ │ │ └── DatabaseConfig.kt
│ │ │ ├── controllers/
│ │ │ │ ├── AdminController.kt
│ │ │ │ ├── AdminPromptController.kt # AI prompt editor
│ │ │ │ ├── AiPostController.kt # AI generation endpoint
│ │ │ │ ├── IndexController.kt
│ │ │ │ ├── PostController.kt
│ │ │ │ └── SketchbookController.kt
│ │ │ ├── models/
│ │ │ │ ├── AiModels.kt # AI model registry table
│ │ │ │ ├── Posts.kt
│ │ │ │ ├── Settings.kt # Key-value settings table
│ │ │ │ └── Users.kt
│ │ │ ├── services/
│ │ │ │ ├── AiPostService.kt # Claude API integration
│ │ │ │ ├── AuthService.kt
│ │ │ │ ├── PostService.kt
│ │ │ │ ├── SettingsService.kt # Admin-editable settings
│ │ │ │ └── UserService.kt
│ │ │ └── utils/
│ │ │ ├── CsrfUtil.kt
│ │ │ ├── DateUtil.kt
│ │ │ └── HtmlSanitizer.kt
│ │ └── resources/
│ │ ├── templates/
│ │ │ ├── layouts/
│ │ │ ├── posts/
│ │ │ ├── admin/
│ │ │ ├── sketchbook/
│ │ │ └── errors/
│ │ ├── prompts/
│ │ │ └── ai-post-generation.txt # Default AI prompt template
│ │ └── public/
│ │ ├── css/
│ │ └── images/
│ └── test/
└── build.gradle.kts
| Method | Path | Description | Auth Required |
|---|---|---|---|
| GET | / |
List all published posts | No |
| GET | /sitemap.xml |
XML sitemap | No |
| GET | /post/:id |
View single post | No |
| GET | /admin |
Admin dashboard or login | No |
| POST | /admin/login |
Authenticate | No |
| GET | /admin/logout |
Logout | Yes |
| GET | /admin/create |
Post creation form | Yes |
| GET | /admin/prompt |
AI prompt editor | Yes |
| POST | /admin/prompt |
Save AI prompt | Yes |
| POST | /admin/ai-generate |
Generate post via Claude API (returns JSON) | Yes |
| POST | /post |
Create new post | Yes |
| GET | /post/:id/edit |
Edit post form | Yes |
| POST | /post/:id/edit |
Update post | Yes |
| POST | /post/:id/archive |
Archive post | Yes |
| POST | /post/:id/restore |
Restore archived post | Yes |
| GET | /sketchbook |
Image gallery | No |
- Password Security: BCrypt hashing with automatic salt generation
- CSRF Protection: Token-based protection on all state-changing operations
- HTML Sanitization: All user-generated content sanitized using OWASP HTML Sanitizer
- Session Management: Secure session handling with HttpOnly cookies, secure flag in production
- SQL Injection Protection: Parameterized queries via Exposed ORM
- XSS Prevention: Automatic HTML escaping in Jte templates
ENV=development ./gradlew run- Template hot reloading (no restart needed on template changes)
- More verbose logging
- Non-secure session cookies (for HTTP testing)
- Add controller method in the appropriate controller
- Register the route in
Application.kt - Create the corresponding Jte template
- Add service method if business logic is needed
- Write tests for the service layer
- Set
ENV=production - Use a strong
SESSION_SECRET(32+ random characters) - Configure database credentials via environment variables
- Set
ANTHROPIC_API_KEYif using AI generation - Enable HTTPS (configure reverse proxy like Nginx)
- Set up automated backups for MySQL database
- Consider using a process manager (systemd)
Add environment variables in /etc/systemd/system/blog.service:
[Service]
Environment=ENV=production
Environment=DB_HOST=localhost
Environment=DB_NAME=blog
Environment=DB_USER=blog
Environment=DB_PASSWORD=secret
Environment=SESSION_SECRET=your-secret
Environment=ANTHROPIC_API_KEY=sk-ant-...
ExecStart=/usr/bin/java -Xmx256m -jar /opt/blog/blog-kotlin-1.0.0-all.jar# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests UserServiceTest
# View test report
open build/reports/tests/test/index.html- Export your data from the Perl MySQL database
- Import to new database (schema is compatible)
- Re-hash passwords: Passwords need to be re-hashed with BCrypt — users must reset them
- Copy images: Copy image files to
src/main/resources/public/images/ - Update configuration: Set environment variables for database connection
- Check database connection settings
- Verify MySQL is running and accessible
- Verify
ANTHROPIC_API_KEYis set in the environment - Ensure a prompt is saved via
/admin/prompt - Check that Claude user and AiModel rows exist in the database
- Make sure you're running in development mode (
ENV=development)
Built with ❤️ using Kotlin and Javalin