A production-ready, fast, and modular bookmark management system built with PHP 8.2+ and MySQL/MariaDB. Designed for both Docker deployment and cPanel shared hosting with a focus on speed, security, and maintainability.
- ⚡ Fast Search - Full-text search with AJAX and client-side caching
- 📁 Nested Categories - Support for up to 10 levels of category hierarchy
- 🏷️ Flexible Tagging - Tag bookmarks for quick filtering
- 📥 Import/Export - Support for JSON, HTML (browser format), and CSV
- 📥 Smart Duplicate Detection - Skip duplicates only when URL AND category match exactly
- 🌙 Dark Mode - Automatic theme detection with manual toggle
- 🔒 Secure by Default - CSRF protection, XSS prevention, prepared statements
- 🇪🇺 GDPR Compliant - Cookie consent and data privacy features
- 📱 Responsive Design - Mobile-first approach
- ⌨️ Keyboard Shortcuts - Power user friendly
- 🐳 Docker Ready - Easy deployment with Docker Compose
- 🖼️ Image Caching - Automatic favicon and image caching (weekly refresh for bandwidth efficiency)
- 🖼️ Image URL Support - Save image URLs directly with automatic offloading/caching
- 🔄 Meta Fetching - Automatic title and description extraction from URLs
- 🧩 Browser Extension - Chrome/Edge extension for quick bookmark saving
- 🛠️ Web Installer - Easy setup wizard for shared hosting
- Docker Engine 20.10+
- Docker Compose 2.0+
- PHP 8.1 or higher
- MySQL 8.0+ or MariaDB 10.5+
- Apache with mod_rewrite (or nginx)
- Required PHP extensions: pdo, pdo_mysql, json, mbstring, curl, openssl
The easiest way to get started is using Docker:
# Clone the repository
git clone <repository-url>
cd bookmark-manager
# Start the containers
docker compose up -dThe application will be available at http://localhost:8080.
Default Docker Configuration:
- Web Server:
http://localhost:8080 - MySQL:
localhost:3306 - Database:
bookmarks_db - User:
bookmark_user - Password:
bookmark_pass
For cPanel, Plesk, or any shared hosting with PHP 8.1+ and MySQL:
Method A: Upload to public_html directly (easiest)
- Upload Files - Upload ALL files to
public_htmlvia FTP/File Manager - Run Installer - Visit
https://yourdomain.com/install.php - Follow Wizard - Configure database, create admin account
- Delete Installer - Remove
install.phpafter installation
The root .htaccess and index.php will route requests properly.
Method B: Subdirectory with custom document root (if available)
- Upload files to
/home/user/bookmark-manager/ - In cPanel, set domain document root to
/home/user/bookmark-manager/public - Visit
https://yourdomain.com/install.php
Upload all files to your web hosting. The public folder should be your document root.
/home/username/
├── bookmark-manager/ # Application root
│ ├── app/ # Application code
│ ├── cache/ # Cache storage
│ ├── cron/ # Cron scripts
│ ├── database/ # Database schema
│ └── public/ # Document root (point domain here)
Create a new MySQL database via cPanel and import the schema:
mysql -u username -p database_name < database/schema.sqlOr use phpMyAdmin to import database/schema.sql.
Copy the example config and edit with your settings:
cp app/config/config.example.json app/config/config.jsonEdit config.json:
{
"database": {
"host": "localhost",
"name": "your_database",
"user": "your_username",
"password": "your_password"
},
"app": {
"url": "https://yourdomain.com"
}
}chmod 755 -R app/
chmod 777 cache/
chmod 777 logs/
chmod 644 app/config/config.jsonIn cPanel > Cron Jobs, add:
# Refresh bookmark metadata weekly (Sunday 4 AM)
0 4 * * 0 /usr/local/bin/php /home/username/bookmark-manager/cron/refresh-meta.php
# Cache bookmark images weekly (Sunday 3 AM) - bandwidth efficient
0 3 * * 0 /usr/local/bin/php /home/username/bookmark-manager/cron/cache-images.php
# Cleanup expired cache weekly (Sunday 2 AM)
0 2 * * 0 /usr/local/bin/php /home/username/bookmark-manager/cron/cleanup.php
Visit https://yourdomain.com/register to create your first account.
{
"database": {
"host": "localhost",
"name": "bookmarks",
"user": "root",
"password": "",
"charset": "utf8mb4"
},
"app": {
"name": "Bookmark Manager",
"url": "http://localhost",
"debug": false,
"timezone": "UTC",
"per_page": 20
},
"security": {
"session_lifetime": 7200,
"password_algo": "argon2id"
},
"cache": {
"enabled": true,
"ttl": 3600,
"driver": "file"
},
"gdpr": {
"enabled": true,
"cookie_consent": true,
"data_retention_days": 365
}
}bookmark-manager/
├── app/
│ ├── api/ # API endpoints (bookmarks, export, import, meta, search)
│ ├── config/ # Configuration files
│ ├── controllers/ # Request handlers
│ ├── core/ # Core framework classes (Autoloader, Database, Router, View)
│ ├── helpers/ # Utility functions (Auth, CSRF, Sanitizer)
│ ├── models/ # Database models (Bookmark, Category, Tag, User)
│ ├── services/ # Business logic services
│ │ ├── CacheService.php
│ │ ├── EnhancedMetaFetcher.php
│ │ ├── ImageCacheService.php
│ │ ├── ImportExportService.php
│ │ ├── MetaFetcher.php
│ │ └── SearchService.php
│ └── views/
│ ├── components/ # Reusable UI components
│ ├── layout.php # Main layout template
│ └── pages/ # Page templates
├── cache/ # File-based cache storage
│ └── images/ # Cached favicon and images
├── cron/ # Scheduled task scripts
│ ├── cache-images.php
│ ├── cleanup.php
│ ├── fetch-meta.php
│ └── refresh-meta.php
├── database/ # SQL schema and migrations
│ ├── schema.sql
│ └── migrations/
├── logs/ # Application logs
├── public/ # Web-accessible files
│ ├── css/ # Stylesheets
│ ├── js/ # JavaScript files
│ ├── img/ # Images
│ ├── errors/ # Error pages (404, 500)
│ └── index.php # Application entry point
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Docker image definition
└── README.md # This file
These endpoints require session authentication (login via browser):
GET /api/search?q=query&limit=10 # Search bookmarks
GET /api/meta?url=https://example.com # Fetch URL metadata
GET /api/bookmarks # List bookmarks
POST /api/bookmarks # Create bookmark
GET /api/bookmarks/:id # Get bookmark
PUT /api/bookmarks/:id # Update bookmark
DELETE /api/bookmarks/:id # Delete bookmark
For Chrome extensions, mobile apps, or other external integrations.
Authentication: Include your API key in the request header:
Authorization: Bearer bm_xxxxxxxxxxxxxxxxxxxx
Or use the X-API-Key header:
X-API-Key: bm_xxxxxxxxxxxxxxxxxxxx
Generate API Keys: Go to Settings → API Keys in your dashboard.
POST /api/external.php
curl -X POST https://yourdomain.com/api/external.php \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"title": "Example Site",
"description": "Optional description",
"category": "Work",
"tags": ["reference", "tools"],
"is_favorite": true
}'Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | The URL to bookmark |
title |
string | No | Title (auto-fetched if empty) |
description |
string | No | Description (auto-fetched if empty) |
category |
string | No | Category name (created if doesn't exist) |
category_id |
int | No | Category ID (takes precedence over name) |
tags |
array | No | Array of tag names |
is_favorite |
bool | No | Mark as favorite |
fetch_meta |
bool | No | Auto-fetch title/description (default: true) |
Response:
{
"success": true,
"message": "Bookmark created successfully",
"data": {
"id": 123,
"url": "https://example.com",
"title": "Example Site",
...
}
}GET /api/external.php?page=1&per_page=20GET /api/external.php?id=123DELETE /api/external.php?id=123A Chrome/Edge/Brave extension is included for quickly saving bookmarks from any webpage.
- Open
chrome://extensions/(oredge://extensions/for Edge) - Enable Developer mode
- Click Load unpacked
- Select the
browser-extensionfolder - The extension icon will appear in your toolbar
- Click the extension icon → Settings
- Enter your Server URL (e.g.,
http://127.0.0.1:8080) - Enter your API Key (from Settings → API Keys in your dashboard)
- Click Test Connection then Save
- Navigate to any webpage
- Click the extension icon
- Select a category (default: Uncategorized)
- Add tags (optional)
- Click Save Bookmark
See browser-extension/README.md for detailed documentation.
| Key | Action |
|---|---|
Ctrl+K / Cmd+K |
Focus search |
N |
New bookmark |
? |
Show shortcuts help |
↑ / ↓ |
Navigate search results |
Enter |
Select result |
Esc |
Close search/modal |
- Format: Linkwarden-compatible hierarchical collections
- Fields: All metadata (title, description, meta_image, favicon, site name, type, author, keywords, locale, http_status, content_type, timestamps, tags, favorites)
- Hierarchy: Categories are exported as collections with parent/child relationships
- Favorites: Exported as pinnedLinks array
- Import: Supports Linkwarden, Raindrop, and browser JSON formats
- Format: Netscape Bookmark File (browser standard)
- Hierarchy: Full folder structure preserved (parent/child folders)
- Attributes: ADD_DATE, LAST_MODIFIED, ICON, and descriptions included
- Favorites: Exported as a special toolbar folder
- Import: Nested folders are imported as hierarchical categories
- Format: Simple flat table
- Fields: url, title, description, category, tags, is_favorite, created_at
- CSRF Protection: All forms include CSRF tokens
- XSS Prevention: All output is escaped by default
- SQL Injection Prevention: All queries use prepared statements
- Password Hashing: Argon2id or bcrypt with secure defaults
- Session Security: HTTPOnly cookies, secure flags, regeneration
- Rate Limiting Ready: API endpoints support rate limiting headers
- Full-text Search: MySQL FULLTEXT indexes for fast search
- File-based Caching: JSON cache for search results and metadata
- Lazy Loading: Images and non-critical resources load lazily
- Minimal Dependencies: No external PHP packages required
- Client-side Caching: Search results cached in browser memory
Containers won't start:
# Check container logs
docker compose logs -f
# Rebuild containers
docker compose down && docker compose up -d --buildDatabase connection issues in Docker:
- Wait for MySQL to fully initialize (check with
docker compose logs mysql) - Ensure the MySQL health check passes before the web container starts
- Check PHP error logs (in Docker:
docker compose logs web) - Verify
config.jsonsyntax is valid - Ensure cache and logs directories are writable
- Verify database credentials in
config.json - Check if MySQL server is running
- Ensure database user has proper permissions
- Check namespace declarations match folder structure
- Verify autoloader is properly included
- Ensure full-text indexes exist on bookmarks table
- Check if minimum word length is configured in MySQL
MIT License - see LICENSE file for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
# Start containers
docker compose up -d
# Stop containers
docker compose down
# View logs
docker compose logs -f
# Rebuild after changes
docker compose up -d --build
# Access MySQL CLI
docker compose exec mysql mysql -u bookmark_user -pbookmark_pass bookmarks_db
# Access PHP container shell
docker compose exec web bashFor issues and feature requests, please use the GitHub issue tracker.