6.7 KiB
bare-site
A zero-dependency PHP micro-framework for content websites. No database. No Composer. No build step. Just files on disk.
The Story
This framework wasn't built by a developer. It was built by a solutions architect who spent 25 years watching the industry overcomplicate simple things.
In the early 2000s, I was a webmaster. PHP files with inline HTML. It worked. Then the industry decided that every website needed a CMS, a database, an ORM, a build pipeline, and 847 npm dependencies. I watched startups spend $500/month on cloud infrastructure for a 5-page brochure site. I watched enterprise sales demos fail because "composer install" hung during the client meeting.
So I kept doing what worked: a single index.php that routes URLs, language files that are the content store, and an nginx config that blocks everything except the entry point. For 8 years, this pattern has powered:
- lohmar.co.uk — a multi-language company website (7 languages, served from Forgejo)
- A guesthouse booking site — with e-commerce via Beds24 iframe integration
- Enterprise IDV demos — for Jumio, Keyless Go, and Jaguar Land Rover
- Multi-brand landing pages — where the language routing system was repurposed to switch CSS per brand
When JLR's legal team discovered this little framework predated our engagement by years, they were not happy. Previous art. It wasn't theirs.
The philosophy is simple: you don't need a hammer drill to punch a hole in cardboard. Most websites are cardboard. Use the right tool.
What It Is
| Feature | Implementation |
|---|---|
| Router | 80-line array-driven URL mapper in start.php |
| Templates | Raw .phtml files — HTML with optional inline PHP |
| Languages | File-based: lng/{lang}/menu.php + .phtml per page |
| Security | nginx isolates PHP — only index.php executes |
| Bot detection | 30+ UA patterns + browser heuristic bypass |
| Rate limiting | CSV-based velocity tracking, blocks at 5 req/s |
| Logging | Structured JSON, weekly rotation (errors_2026-W26.log) |
| Sessions | httponly, SameSite=Lax, strict_mode, UUIDv4 correlation IDs |
| Optional | Geo-location (ipgeolocation.io), Telegram visitor alerts |
What It's NOT
- A WordPress alternative (no admin panel, no users, no plugins)
- A headless CMS (no content API, no editing interface)
- A Laravel competitor (no ORM, no migrations, no queues, no Artisan)
- A general-purpose framework (no database abstraction, no routing DSL, no middleware)
If you need any of those things, this is the wrong tool. If you have a 5-20 page website that needs to exist in multiple languages and never break, this is exactly the right tool.
Quick Start
⚠️ Step 1: Configure your web server (NOT optional)
bare-site's entire security model depends on the web server blocking direct access to PHP files. The framework will not work without these rules.
nginx (recommended) — copy nginx/default.conf:
# Only index.php can execute PHP
location = /index.php {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ \.php$ { return 404; } # ← blocks config.php, functions.php, etc.
# Everything routes through index.php
location / { try_files $uri $uri/ /index.php?$query_string; }
Apache (.htaccess):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
RewriteCond %{REQUEST_URI} !^/index\.php$
RewriteRule \.php$ - [R=404,L]
Caddy:
your-domain.com {
root * /var/www/html
php_fastcgi unix//run/php/php8.4-fpm.sock
file_server
@blockedPhp path_regexp /(?!index\.php$).*\.php$
respond @blockedPhp 404
}
Step 2: Drop the files
git clone https://git.lohmar.co.uk/cclohmar/bare-site.git /var/www/html/
Step 3: Edit your config
Open html/class/config.php and set:
SITE_NAME— your site titleSITE_DESCRIPTION— meta description- Page slugs (
PAGE_00_SLUGthroughPAGE_05_SLUG) - Supported languages in
$languagesarray - Optional:
GEO_KEY(ipgeolocation.io),TELEGRAM_BOT_TOKEN+TELEGRAM_CHAT_ID
Step 4: Create your pages
lng/en/
├── menu.php # navigation labels + error messages
├── home.phtml # your content — raw HTML, no template engine
├── about.phtml # another page
└── ...
Content files are plain HTML inside a <section>. Use inline PHP if needed: <?php echo date('Y'); ?>.
Step 5: Add a language
# 1. Add to config.php:
$languages = ['en' => 'English', 'de' => 'Deutsch'];
# 2. Create the directory and copy files:
mkdir lng/de/ && cp lng/en/* lng/de/
# 3. Translate menu.php + .phtml files
# Done — appears in the nav dropdown automatically.
Architecture
html/
├── index.php # Entry point: session → route
├── class/
│ ├── config.php # Site constants, env detection, API keys, security
│ ├── functions.php # Logging, bot detection, geo-location, rate limiting
│ └── site.php # Session bootstrap, language detection
├── app/
│ ├── controler/
│ │ ├── start.php # URL router (80 lines, 6 slugs, sub-page support)
│ │ └── request.php # POST/GET form handler
│ ├── model/menu.php # Dynamic nav builder from language constants
│ └── view/
│ ├── _header.phtml # HTML head, navbar, Bootstrap CDN
│ ├── _footer.phtml # Footer, JS, debug block
│ └── _error.phtml # HTTP error page renderer (400–504)
└── lng/{en,de,fr,...}/
├── menu.php # Language-specific text constants
└── {slug}.phtml # Page content templates
Optional Features
| Feature | How to enable |
|---|---|
| Geo-location | Set GEO_KEY in config.php (free at ipgeolocation.io) |
| Telegram alerts | Set TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID |
| Weekly logs | Auto-rotated: logs/errors_2026-W26.log |
| Article sub-pages | Create lng/en/{slug}/{article}.phtml — routed at /{slug}/{article} |
| Multi-language | Add to $languages array, create directory, translate |
Requirements
- PHP 8.1+ (uses
FILTER_SANITIZE_FULL_SPECIAL_CHARS, named arguments not required) - nginx, Apache, or Caddy with the isolation rules above
- No database
- No Composer
- No npm
- No build step
License
MIT — use it, fork it, ship it. If this saves you from deploying WordPress for a 5-page site, we've already won.