bare-site v1.0 — zero-dependency PHP micro-framework
7 languages, 3 pages (What/How/Start), 80-line router, 98KB total.
Features:
- No database, no Composer, no build step — just files on disk
- nginx PHP isolation: only index.php executes
- Bot detection with browser heuristic bypass
- Rate limiting, structured weekly logs, geo-location, Telegram alerts
- Multi-language: Accept-Language auto-detect, nav dropdown
- Article sub-page routing via /start/{slug}
- Apache + Caddy config alternatives documented in Start page
- Sample config — no real keys or secrets
This commit is contained in:
commit
b93de83519
44 changed files with 2142 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Logs
|
||||
logs/*.log
|
||||
logs/*.csv
|
||||
|
||||
# Session files
|
||||
.sessions/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Backups
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
16
html/app/controler/request.php
Normal file
16
html/app/controler/request.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
// POST/GET form handler
|
||||
// Process incoming form data — no database, no SQL injection surface
|
||||
|
||||
if(!empty($HTML_REQUEST['do'])){
|
||||
switch ($HTML_REQUEST['do']) {
|
||||
default:
|
||||
$redirect = logging_errors("400",$HTML_REQUEST);
|
||||
header('Location: '.__URL__ . $redirect);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$redirect = logging_errors("405",$HTML_REQUEST);
|
||||
header('Location: '.__URL__ . $redirect);
|
||||
exit;
|
||||
}
|
||||
63
html/app/controler/start.php
Normal file
63
html/app/controler/start.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
loadLanguage();
|
||||
|
||||
$slugs = [PAGE_00_SLUG, PAGE_01_SLUG, PAGE_02_SLUG, PAGE_03_SLUG, PAGE_04_SLUG, PAGE_05_SLUG];
|
||||
|
||||
if (!empty($PARAMS['1'])) {
|
||||
$slug = $PARAMS['1'];
|
||||
|
||||
if (in_array($slug, $slugs, true)) {
|
||||
$_SESSION['SITE']['slug'] = $slug;
|
||||
include(__ROOT__.'/app/view/_header.phtml');
|
||||
if ($slug === PAGE_02_SLUG && !empty($PARAMS['2'])) {
|
||||
$article = basename($PARAMS['2']);
|
||||
include(__ROOT__.'/lng/'.$_SESSION['SITE']['language'].'/'.$_SESSION['SITE']['slug'].'/'.$article.'.phtml');
|
||||
} else {
|
||||
include(__ROOT__.'/lng/'.$_SESSION['SITE']['language'].'/'.$_SESSION['SITE']['slug'].'.phtml');
|
||||
}
|
||||
include(__ROOT__.'/app/view/_footer.phtml');
|
||||
|
||||
} elseif ($slug === 'error') {
|
||||
if (!empty($PARAMS['2'])) {
|
||||
$errorCode = $PARAMS['2'];
|
||||
$_SESSION = array();
|
||||
include(__ROOT__.'/app/view/_header.phtml');
|
||||
include(__ROOT__.'/app/view/_error.phtml');
|
||||
include(__ROOT__.'/app/view/_footer.phtml');
|
||||
} else {
|
||||
header('Location: '.__URL__.'error/404');
|
||||
exit;
|
||||
}
|
||||
|
||||
} elseif ($slug === 'dev') {
|
||||
if (!DEBUG) { header('Location: '.__URL__); exit; }
|
||||
echo '<center><h1>Development</h1></center>';
|
||||
include(__ROOT__.'/app/view/_dev.php');
|
||||
|
||||
} elseif ($slug === 'clear') {
|
||||
$_SESSION = array();
|
||||
header('Location: '.__URL__);
|
||||
exit;
|
||||
|
||||
} elseif ($slug === 'set') {
|
||||
$supported = array_keys(LANGUAGES);
|
||||
if (!empty($PARAMS['2']) && in_array($PARAMS['2'], $supported, true)) {
|
||||
$_SESSION['SITE']['language'] = $PARAMS['2'];
|
||||
header('Location: '.__URL__.$_SESSION['SITE']['slug']);
|
||||
} else {
|
||||
$_SESSION = array();
|
||||
header('Location: '.__URL__);
|
||||
}
|
||||
exit;
|
||||
|
||||
} else {
|
||||
header('Location: '.__URL__.'error/404');
|
||||
exit;
|
||||
}
|
||||
|
||||
} else {
|
||||
$_SESSION['SITE']['slug'] = PAGE_00_SLUG;
|
||||
include(__ROOT__.'/app/view/_header.phtml');
|
||||
include(__ROOT__.'/lng/'.$_SESSION['SITE']['language'].'/'.$_SESSION['SITE']['slug'].'.phtml');
|
||||
include(__ROOT__.'/app/view/_footer.phtml');
|
||||
}
|
||||
19
html/app/model/menu.php
Normal file
19
html/app/model/menu.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
// Build the menu from language constants
|
||||
$menue = '';
|
||||
if (!empty(PAGE_00_TXT)) {$menue .= '<a href="'.__URL__.PAGE_00_SLUG.'">'.PAGE_00_TXT.'</a>';}
|
||||
if (!empty(PAGE_01_TXT)) {$menue .= '<a href="'.__URL__.PAGE_01_SLUG.'">'.PAGE_01_TXT.'</a>';}
|
||||
if (!empty(PAGE_02_TXT)) {$menue .= '<a href="'.__URL__.PAGE_02_SLUG.'">'.PAGE_02_TXT.'</a>';}
|
||||
if (!empty(PAGE_03_TXT)) {$menue .= '<a href="'.__URL__.PAGE_03_SLUG.'">'.PAGE_03_TXT.'</a>';}
|
||||
if (!empty(PAGE_04_TXT)) {$menue .= '<a href="'.__URL__.PAGE_04_SLUG.'">'.PAGE_04_TXT.'</a>';}
|
||||
if (!empty(PAGE_05_TXT)) {$menue .= '<a href="'.__URL__.PAGE_05_SLUG.'">'.PAGE_05_TXT.'</a>';}
|
||||
|
||||
$menue .= '<div class="dropdown">';
|
||||
$menue .= '<button class="dropbtn">' . LANGUAGE_TITLE . '</button>';
|
||||
$menue .= '<div class="dropdown-content">';
|
||||
foreach (LANGUAGES as $code => $name) {
|
||||
$menue .= '<a href="'.__URL__.'set/'.$code.'">'.$name.'</a>';
|
||||
}
|
||||
$menue .= '</div>';
|
||||
$menue .= '</div>';
|
||||
11
html/app/view/_dev.php
Normal file
11
html/app/view/_dev.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
echo "bare-site — development info";
|
||||
echo '<hr>';
|
||||
print_r($_SESSION['VISITOR']);
|
||||
echo '<hr>';
|
||||
echo "<h1>All Headers</h1>";
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
echo "<strong>$key:</strong> " . htmlspecialchars($value) . "<br>";
|
||||
}
|
||||
echo '<hr>';
|
||||
echo "workbench";
|
||||
20
html/app/view/_error.phtml
Normal file
20
html/app/view/_error.phtml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
switch ($errorCode) {
|
||||
case '400': $errorMessage = ERROR_400; break;
|
||||
case '401': $errorMessage = ERROR_401; break;
|
||||
case '403': $errorMessage = ERROR_403; break;
|
||||
case '404': $errorMessage = ERROR_404; break;
|
||||
case '405': $errorMessage = ERROR_405; break;
|
||||
case '408': $errorMessage = ERROR_408; break;
|
||||
case '409': $errorMessage = ERROR_409; break;
|
||||
case '410': $errorMessage = ERROR_410; break;
|
||||
case '415': $errorMessage = ERROR_415; break;
|
||||
case '429': $errorMessage = ERROR_429; break;
|
||||
case '500': $errorMessage = ERROR_500; break;
|
||||
case '501': $errorMessage = ERROR_501; break;
|
||||
case '502': $errorMessage = ERROR_502; break;
|
||||
case '503': $errorMessage = ERROR_503; break;
|
||||
case '504': $errorMessage = ERROR_504; break;
|
||||
default: $errorMessage = ERROR_default; break;
|
||||
}
|
||||
echo $errorMessage;
|
||||
46
html/app/view/_footer.phtml
Normal file
46
html/app/view/_footer.phtml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
# _footer.phtml
|
||||
?>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-info"><p>Powered by <strong>bare-site</strong></p></div>
|
||||
<div class="footer-copyright">
|
||||
<p>A zero-dependency PHP micro-framework. No database required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php if (DEBUG): ?>
|
||||
<div class="Debug"><!-- <?php echo json_encode($_SESSION, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); ?> --></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hamburger = document.querySelector(".hamburger");
|
||||
const navMenu = document.querySelector(".nav-menu");
|
||||
if (hamburger && navMenu) {
|
||||
hamburger.addEventListener("click", () => {
|
||||
hamburger.classList.toggle("active");
|
||||
navMenu.classList.toggle("active");
|
||||
document.body.style.overflow = navMenu.classList.contains('active') ? 'hidden' : '';
|
||||
});
|
||||
document.querySelectorAll(".nav-menu a").forEach(n => n.addEventListener("click", () => {
|
||||
hamburger.classList.remove("active");
|
||||
navMenu.classList.remove("active");
|
||||
document.body.style.overflow = '';
|
||||
}));
|
||||
}
|
||||
document.querySelectorAll('.dropdown').forEach(function(dropdown) {
|
||||
const button = dropdown.querySelector('.dropbtn');
|
||||
const content = dropdown.querySelector('.dropdown-content');
|
||||
content.style.display = 'none';
|
||||
button.addEventListener('click', function() {
|
||||
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
html/app/view/_header.phtml
Normal file
27
html/app/view/_header.phtml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
include(__ROOT__.'/app/model/menu.php');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $_SESSION['SITE']['language'] ?? 'en'; ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="<?php echo SITE_DESCRIPTION; ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="index, nofollow" />
|
||||
<title><?php echo SITE_NAME; ?></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo __URL__; ?>cdn/css/default.css" >
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo __URL__; ?>cdn/css/mobile.css" >
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<div class="nav-logo">
|
||||
<span class="nav-logo-text">◆</span>
|
||||
</div>
|
||||
<div class="nav-title"><?php echo SITE_NAME ?></div>
|
||||
<div class="nav-menu"><?php echo $menue ?></div>
|
||||
<div class="hamburger"><span class="bar"></span><span class="bar"></span><span class="bar"></span></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="content-wrapper">
|
||||
867
html/cdn/css/default.css
Normal file
867
html/cdn/css/default.css
Normal file
|
|
@ -0,0 +1,867 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
||||
|
||||
/* ===== BASE STYLES ===== */
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Lato', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #333;
|
||||
background-color: #F5F5F5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
/* Add min-height to ensure it spans the content */
|
||||
min-height: calc(100vh - 70px - 150px); /* Adjust 150px to match your footer height */
|
||||
}
|
||||
|
||||
/* ===== NAVIGATION ===== */
|
||||
.navbar {
|
||||
background-color: #08755B;
|
||||
border-bottom: none;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-logo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-logo img {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-logo img:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-menu a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.nav-menu a.active {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 25px;
|
||||
height: 3px;
|
||||
background-color: #FFF;
|
||||
margin: 3px 0;
|
||||
transition: 0.3s;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Menu container */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropbtn {
|
||||
background-color: #08755B;
|
||||
color: white;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1f1f1
|
||||
}
|
||||
|
||||
/* ===== HERO SECTION ===== */
|
||||
.hero-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(rgba(15, 45, 82, 0.8), rgba(8, 117, 91, 0.7));
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
text-align: center;
|
||||
color: #f8da78;
|
||||
font-size: 2.1rem;
|
||||
padding-top: 2rem;
|
||||
max-width: 70%;
|
||||
margin: 0 auto;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
text-align: center;
|
||||
color: #f8da78;
|
||||
font-size: 1.4rem;
|
||||
padding-top: 1.5rem;
|
||||
max-width: 70%;
|
||||
margin: 0 auto;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
background: linear-gradient(135deg, #0F2D52 0%, #08755B 100%);
|
||||
margin: 3rem auto;
|
||||
max-width: 60%;
|
||||
border-radius: 12px;
|
||||
padding: 2.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== CONTENT SECTIONS ===== */
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
padding: 4rem 1rem;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
color: #0a775b;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
text-align: center;
|
||||
color: #7f948f;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* SECTION CONTENT STYLES */
|
||||
.section-content {
|
||||
text-align: left;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.section-content p {
|
||||
text-align: left;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.section-content ul {
|
||||
text-align: left;
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.section-content li {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-left: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.section-content strong {
|
||||
color: #0F2D52;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== CONTACT PAGE SPECIFIC STYLES ===== */
|
||||
.contact-section {
|
||||
padding: 4rem 1rem;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #2c5282;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 2.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Form input styles */
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: #2b6cb0;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
transition: background-color 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background-color: #2c5282;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.qr-code h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.qr-code p {
|
||||
color: #718096;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.qr-code img {
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Form labels */
|
||||
.form-label {
|
||||
display: block;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== CTA BUTTONS ===== */
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
margin: 0 1rem;
|
||||
background-color: #08755B;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #08755B;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cta-button.secondary {
|
||||
background-color: transparent;
|
||||
color: #08755B;
|
||||
border: 2px solid #08755B;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: #065a46;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cta-button.secondary:hover {
|
||||
background-color: #08755B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* CTA Section Styling */
|
||||
.cta-content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
color: white;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cta-subtitle {
|
||||
color: #f8da78;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.google-calendar-container {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Force Google button styling */
|
||||
.gc-button {
|
||||
display: inline-block !important;
|
||||
padding: 1rem 2rem !important;
|
||||
background-color: #f8da78 !important;
|
||||
color: #0F2D52 !important;
|
||||
text-decoration: none !important;
|
||||
border-radius: 5px !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
border: 2px solid #f8da78 !important;
|
||||
font-size: 1.1rem !important;
|
||||
font-family: 'Lato', 'Helvetica Neue', Arial, sans-serif !important;
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
||||
.gc-button:hover {
|
||||
background-color: #e6c966 !important;
|
||||
transform: translateY(-2px) !important;
|
||||
border-color: #e6c966 !important;
|
||||
}
|
||||
|
||||
/* ===== FOOTER STYLES ===== */
|
||||
.footer {
|
||||
background-color: #08755B;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #f8da78;
|
||||
text-decoration: none;
|
||||
margin: 0 1rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-legal {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.footer-legal p { margin: 0; }
|
||||
|
||||
.footer-copyright {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ===== HERO BUTTONS ===== */
|
||||
.hero-actions {
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hero-banner .cta-button.secondary {
|
||||
color: white;
|
||||
border-color: white;
|
||||
}
|
||||
.hero-banner .cta-button.secondary:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* ===== WIDE CONTENT SECTIONS (for grids) ===== */
|
||||
.content-wide {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
/* ===== GRID ===== */
|
||||
.grid { display: grid; gap: 2rem; }
|
||||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
|
||||
/* ===== SECTION LEAD ===== */
|
||||
.section-lead {
|
||||
text-align: center;
|
||||
max-width: 720px;
|
||||
margin: 0 auto 2.5rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* ===== CARDS ===== */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
border-top: 4px solid #08755B;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
.card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.1); }
|
||||
.card-title { color: #0F2D52; font-size: 1.2rem; font-weight: 700; margin-bottom: 0.75rem; }
|
||||
.card-text { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
|
||||
/* ===== FEATURE BLOCKS ===== */
|
||||
.feature-block {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
.feature-title { color: #08755B; font-size: 1.15rem; font-weight: 700; margin-bottom: 0.75rem; }
|
||||
.feature-text { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
|
||||
/* ===== TIER CARDS ===== */
|
||||
.tier-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.tier-card:hover { transform: translateY(-4px); }
|
||||
.tier-card.highlight { border: 2px solid #08755B; box-shadow: 0 6px 20px rgba(8,117,91,0.15); }
|
||||
.tier-badge {
|
||||
display: inline-block;
|
||||
background: #e8f5f0;
|
||||
color: #08755B;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tier-card.highlight .tier-badge { background: #08755B; color: white; }
|
||||
.tier-title { color: #0F2D52; font-size: 1.1rem; font-weight: 700; margin-bottom: 0.75rem; }
|
||||
.tier-text { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
|
||||
/* ===== ALT SECTION BACKGROUND ===== */
|
||||
.section-alt { background: #f0f4f8; }
|
||||
|
||||
/* ===== MOBILE GRID OVERRIDES ===== */
|
||||
@media screen and (max-width: 768px) {
|
||||
.grid-2,
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.hero-actions { flex-direction: column; align-items: center; }
|
||||
.hero-actions .cta-button { width: 100%; max-width: 320px; text-align: center; }
|
||||
.content-wide { padding: 2.5rem 1rem; }
|
||||
}
|
||||
|
||||
/* ===== MODEL CARDS (Services page) ===== */
|
||||
.model-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.model-card:hover { transform: translateY(-4px); }
|
||||
.model-card.highlight { border: 2px solid #08755B; box-shadow: 0 6px 20px rgba(8,117,91,0.15); }
|
||||
.model-icon {
|
||||
color: #08755B;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.model-title { color: #0F2D52; font-size: 1.15rem; font-weight: 700; margin-bottom: 0.25rem; }
|
||||
.model-subtitle { color: #7f948f; font-size: 0.95rem; font-weight: 400; display: block; margin-top: 0.25rem; }
|
||||
.model-text { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 1rem 0; }
|
||||
.model-list { padding-left: 1.25rem; margin: 0; }
|
||||
.model-list li { color: #4a5568; font-size: 0.9rem; line-height: 1.6; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== PAGE HERO (no background image) ===== */
|
||||
.page-hero {
|
||||
background: linear-gradient(135deg, #0F2D52 0%, #08755B 100%);
|
||||
padding: 4rem 2rem 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.page-hero .hero-title { padding-top: 0; font-size: 2.2rem; }
|
||||
.page-hero .hero-subtitle { padding-top: 1rem; font-size: 1.2rem; }
|
||||
|
||||
/* ===== INSIGHTS FILTER BAR ===== */
|
||||
.insights-filter {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 1.5rem 1rem 2rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.filter-item {
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
color: #4a5568;
|
||||
cursor: default;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.filter-item.active {
|
||||
background: #08755B;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== INSIGHT CARDS ===== */
|
||||
.insight-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.insight-card:hover { transform: translateY(-4px); }
|
||||
.insight-card.featured { border: 2px solid #08755B; }
|
||||
.card-meta {
|
||||
color: #08755B;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.card-title { color: #0F2D52; font-size: 1.2rem; font-weight: 700; margin-bottom: 0.75rem; line-height: 1.35; }
|
||||
.card-title a { color: inherit; text-decoration: none; }
|
||||
.card-title a:hover { color: #08755B; }
|
||||
.card-excerpt { color: #4a5568; font-size: 0.95rem; line-height: 1.65; margin-bottom: 1rem; }
|
||||
.read-more {
|
||||
color: #08755B;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.read-more:hover { text-decoration: underline; }
|
||||
|
||||
/* ===== SUBSCRIBE SECTION ===== */
|
||||
.insights-subscribe {
|
||||
background: #f0f4f8;
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
.insights-subscribe h3 { color: #0F2D52; font-size: 1.5rem; margin-bottom: 0.75rem; }
|
||||
.insights-subscribe p { color: #4a5568; font-size: 1rem; max-width: 500px; margin: 0 auto 1.5rem; }
|
||||
.subscribe-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
max-width: 440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.subscribe-form input {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
padding: 0.65rem 1rem;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 6px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.subscribe-form button {
|
||||
padding: 0.65rem 1.5rem;
|
||||
background: #08755B;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.subscribe-form button:hover { background: #065a46; }
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.subscribe-form { flex-direction: column; }
|
||||
}
|
||||
|
||||
/* ===== ARTICLE PAGE ===== */
|
||||
.article-header { max-width: 800px; margin: 3rem auto 1.5rem; padding: 0 1rem; }
|
||||
.back-link { color: #08755B; font-weight: 600; font-size: 0.9rem; text-decoration: none; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
.article-header h1 {
|
||||
color: #0F2D52;
|
||||
font-size: 2rem;
|
||||
line-height: 1.3;
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.article-meta { color: #7f948f; font-size: 0.85rem; }
|
||||
.article-body { max-width: 800px; margin: 0 auto 3rem; padding: 0 1rem; }
|
||||
.article-body .lead { font-size: 1.15rem; color: #0F2D52; font-weight: 500; line-height: 1.7; }
|
||||
.article-body h2 { color: #08755B; font-size: 1.4rem; margin: 2.5rem 0 0.75rem; }
|
||||
.article-body h3 { color: #0F2D52; font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
|
||||
.article-body p { color: #4a5568; font-size: 1.05rem; line-height: 1.75; margin-bottom: 1.25rem; }
|
||||
.article-body ul { padding-left: 1.5rem; margin-bottom: 1.25rem; }
|
||||
.article-body li { color: #4a5568; font-size: 1rem; line-height: 1.7; margin-bottom: 0.5rem; }
|
||||
.article-body code { background: #f0f4f8; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.9em; color: #08755B; }
|
||||
.article-footer-cta {
|
||||
background: linear-gradient(135deg, #0F2D52 0%, #08755B 100%);
|
||||
border-radius: 12px;
|
||||
padding: 2.5rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
.article-footer-cta h3 { color: white; font-size: 1.4rem; margin-bottom: 0.75rem; }
|
||||
.article-footer-cta p { color: #f8da78; font-size: 1rem; margin-bottom: 1.5rem !important; }
|
||||
.article-footer-cta .cta-button { margin: 0 auto; }
|
||||
|
||||
/* ===== ENTERPRISE LANDING PAGE ===== */
|
||||
.enterprise-hero {
|
||||
background: linear-gradient(135deg, #1a0a2e 0%, #0F2D52 40%, #08755B 100%);
|
||||
padding: 4rem 2rem 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.enterprise-hero h1 { color: white; font-size: 2.2rem; max-width: 800px; margin: 0 auto 1rem; line-height: 1.3; }
|
||||
.hero-desc { color: #cbd5e0; font-size: 1.15rem; max-width: 700px; margin: 0 auto 2rem; line-height: 1.6; }
|
||||
.badge.red { background: #c53030; color: white; }
|
||||
|
||||
/* ===== CRISIS SECTION ===== */
|
||||
.crisis-section { max-width: 800px; margin: 3rem auto 2rem; padding: 0 1rem; }
|
||||
|
||||
/* ===== PILLAR CARDS ===== */
|
||||
.pillar-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
text-align: center;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.pillar-card:hover { transform: translateY(-4px); }
|
||||
.pillar-icon {
|
||||
font-size: 2rem;
|
||||
color: #08755B;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pillar-card h3 { color: #0F2D52; font-size: 1.1rem; margin-bottom: 0.75rem; }
|
||||
.pillar-card p { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
|
||||
/* ===== TIMELINE ===== */
|
||||
.timeline { max-width: 700px; margin: 2rem auto 0; }
|
||||
.step {
|
||||
border-left: 3px solid #08755B;
|
||||
padding: 0 0 2rem 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
.step:last-child { padding-bottom: 0; }
|
||||
.step::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 4px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background: #08755B;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.step h4 { color: #0F2D52; font-size: 1.05rem; margin: 0 0 0.4rem; }
|
||||
.step p { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
|
||||
/* ===== ABOUT PAGE ===== */
|
||||
.about-grid { align-items: start; gap: 3rem; }
|
||||
.bio-text { color: #2d3748; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #e8f5f0;
|
||||
color: #08755B;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bio-body { font-size: 1.05rem; line-height: 1.7; margin-bottom: 1.25rem; color: #4a5568; }
|
||||
.profile-frame { text-align: center; }
|
||||
.profile-photo { width: 100%; max-width: 320px; border-radius: 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
|
||||
.frame-caption { color: #7f948f; font-size: 0.85rem; margin-top: 0.75rem; }
|
||||
|
||||
/* ===== EVOLUTION PILLARS ===== */
|
||||
.evolution-pillars {
|
||||
max-width: 1100px;
|
||||
margin: 3rem auto 2rem;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.evolution-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.evolution-card:hover { transform: translateY(-4px); }
|
||||
.evolution-card.highlight { border: 2px solid #08755B; box-shadow: 0 6px 20px rgba(8,117,91,0.15); }
|
||||
.evolution-card h3 { color: #0F2D52; font-size: 1.1rem; margin: 0.5rem 0 0.75rem; }
|
||||
.evolution-card p { color: #4a5568; font-size: 0.95rem; line-height: 1.6; margin: 0; }
|
||||
.era-date {
|
||||
display: inline-block;
|
||||
background: #f0f4f8;
|
||||
color: #4a5568;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.evolution-card.highlight .era-date { background: #08755B; color: white; }
|
||||
|
||||
/* ===== RESUME CTA ===== */
|
||||
.resume-cta {
|
||||
background: linear-gradient(135deg, #0F2D52 0%, #08755B 100%);
|
||||
margin: 3rem auto;
|
||||
max-width: 60%;
|
||||
border-radius: 12px;
|
||||
padding: 2.5rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.resume-cta h2 { color: white; font-size: 1.8rem; margin-bottom: 1rem; }
|
||||
.resume-cta p { color: #f8da78; font-size: 1rem; max-width: 600px; margin: 0 auto 2rem; line-height: 1.6; }
|
||||
.cta-buttons { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; }
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.about-grid { grid-template-columns: 1fr; gap: 1.5rem; }
|
||||
.profile-photo { max-width: 240px; }
|
||||
.resume-cta { max-width: 90%; padding: 2rem 1.25rem; }
|
||||
.cta-buttons { flex-direction: column; align-items: center; }
|
||||
}
|
||||
235
html/cdn/css/mobile.css
Normal file
235
html/cdn/css/mobile.css
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/* ===== MOBILE RESPONSIVE DESIGN ===== */
|
||||
@media screen and (max-width: 768px) {
|
||||
.content-wrapper {
|
||||
min-height: calc(100vh - 60px - 200px);
|
||||
}
|
||||
/* Navigation */
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
justify-content: space-between;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.nav-logo-title {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.nav-logo img {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
position: fixed;
|
||||
left: -100%;
|
||||
top: 70px;
|
||||
background-color: #08755B;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
transition: 0.3s;
|
||||
box-shadow: 0 10px 27px rgba(0,0,0,0.1);
|
||||
padding: 2rem 0;
|
||||
gap: 0;
|
||||
z-index: 999;
|
||||
max-height: calc(100vh - 70px);
|
||||
overflow-y: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.nav-menu.active {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
font-size: 1.2rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.nav-menu a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.hamburger.active .bar:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hamburger.active .bar:nth-child(1) {
|
||||
transform: translateY(8px) rotate(45deg);
|
||||
}
|
||||
|
||||
.hamburger.active .bar:nth-child(3) {
|
||||
transform: translateY(-8px) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* Dropdown mobile styles */
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
min-width: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
max-width: 90%;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
max-width: 90%;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
padding: 2rem 1rem;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
max-width: 90%;
|
||||
padding: 2rem 1rem;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
/* Content Sections */
|
||||
.content-section {
|
||||
max-width: 90%;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-content ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
width: 80%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cta-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.gc-button {
|
||||
padding: 0.8rem 1.5rem !important;
|
||||
font-size: 1rem !important;
|
||||
min-width: 180px !important;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
|
||||
.footer-info p,
|
||||
.footer-copyright p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.content-wrapper {
|
||||
min-height: calc(100vh - 60px - 180px); /* Adjust for smaller screens */
|
||||
}
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0.3rem;
|
||||
}
|
||||
|
||||
.nav-logo img {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.nav-logo-title {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
top: 60px;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-title {
|
||||
font-size: 1.8rem;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
/* Content Sections */
|
||||
.content-section {
|
||||
max-width: 90%;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.footer-info p {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.footer-copyright p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
89
html/class/config.php
Normal file
89
html/class/config.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
## ./class/config.php — Application Configuration
|
||||
## Copy this file to config.php and fill in your values
|
||||
|
||||
/// DEFAULT TIMEZONE
|
||||
date_default_timezone_set('GMT');
|
||||
|
||||
//== ENVIRONMENT DETECTION
|
||||
$host = $_SERVER["HTTP_HOST"] ?? 'localhost';
|
||||
$subdomain = '';
|
||||
|
||||
if (strpos($host, '.') !== false) {
|
||||
$parts = explode('.', $host);
|
||||
if (count($parts) >= 3) {
|
||||
$subdomain = $parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ($subdomain === 'www' || $subdomain === '') {
|
||||
define('ENVIRONMENT', 'production');
|
||||
define('DEBUG', false);
|
||||
} else {
|
||||
define('ENVIRONMENT', 'development');
|
||||
define('DEBUG', true);
|
||||
}
|
||||
|
||||
// PHP error reporting
|
||||
if (DEBUG) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', '0');
|
||||
}
|
||||
|
||||
//== BASICS
|
||||
define('__ROOT__', dirname(dirname(__FILE__)));
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
define('__URL__', $scheme . '://' . $host . '/');
|
||||
|
||||
//== SITE META — customize these for your project
|
||||
define('SITE_NAME', 'bare-site');
|
||||
define('SITE_DESCRIPTION', 'A zero-dependency PHP micro-framework for static multi-language sites');
|
||||
define('SITE_AUTHOR', '');
|
||||
define('SITE_KEYWORDS', '');
|
||||
|
||||
//== API KEYS — optional, leave empty to disable features
|
||||
define('GEO_KEY',''); // ipgeolocation.io — for visitor geo-location
|
||||
define('TELEGRAM_BOT_TOKEN',''); // Telegram bot token — for visitor notifications
|
||||
define('TELEGRAM_CHAT_ID', ''); // Telegram chat ID
|
||||
|
||||
//== Bot Detection
|
||||
define('BLOCK_THRESHOLD', '5');
|
||||
define('SUSPICIOUS_THRESHOLD', '3');
|
||||
define('ALERT_THRESHOLD', '10');
|
||||
|
||||
//== SUPPORTED LANGUAGES — add/remove as needed
|
||||
$languages = ['en' => 'English', 'de' => 'Deutsch', 'fr' => 'Française', 'it' => 'Italiano', 'nl' => 'Nederlands', 'es' => 'Español', 'pt' => 'Português'];
|
||||
define('LANGUAGES', $languages);
|
||||
|
||||
//== DEFINE GLOBAL SLUGS — map URL paths to page templates
|
||||
define('PAGE_00_SLUG','home');
|
||||
define('PAGE_01_SLUG','about');
|
||||
define('PAGE_02_SLUG','start');
|
||||
define('PAGE_03_SLUG','');
|
||||
define('PAGE_04_SLUG','');
|
||||
define('PAGE_05_SLUG','not-used');
|
||||
|
||||
//== SECURITY CONSTANTS
|
||||
define('BOT_USER_AGENT_PATTERNS', [
|
||||
'/bot/i', '/crawl/i', '/spider/i', '/scanner/i', '/monitor/i',
|
||||
'/python-requests/i', '/python-urllib/i', '/curl/i', '/wget/i',
|
||||
'/zgrab/i', '/httpx/i', '/go-http-client/i', '/java/i',
|
||||
'/masscan/i', '/aiohttp/i', '/censysinspect/i', '/amazonbot/i',
|
||||
'/dotbot/i', '/360spider/i', '/dnbcrawler/i', '/semrushbot/i',
|
||||
'/ahrefsbot/i', '/yandexbot/i', '/facebookexternalhit/i',
|
||||
'/facebot/i', '/twitterbot/i', '/bingbot/i', '/duckduckbot/i',
|
||||
'/nmap/i', '/nikto/i', '/nessus/i',
|
||||
'/(?:libwww-perl|lwp-request)/i', '/screaming frog/i', '/postman/i'
|
||||
]);
|
||||
|
||||
define('BLOCKED_PATHS', [
|
||||
'/.env', '/.git', '/wp-admin', '/administrator', '/config',
|
||||
'/backup', '/sql', '/database', '/admin', '/cgi-bin',
|
||||
'/wp-login.php', '/readme.html', '/license.txt', '/phpinfo.php',
|
||||
'/pma', '/phpmyadmin', '/install.php', '/shell.php',
|
||||
'/vendor', '/composer.json', '/passwd', '/etc/passwd',
|
||||
'/logs', '/velocity.csv'
|
||||
]);
|
||||
332
html/class/functions.php
Normal file
332
html/class/functions.php
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
<?php
|
||||
//====================================================
|
||||
// LOGGING FUNCTIONS
|
||||
//====================================================
|
||||
|
||||
function logging_errors($error_code, $data) {
|
||||
switch ($error_code) {
|
||||
case '400': $redirect = 'error/400'; break;
|
||||
case '401': $redirect = 'error/401'; break;
|
||||
case '403': $redirect = 'error/403'; break;
|
||||
case '404': $redirect = 'error/404'; break;
|
||||
case '405': $redirect = 'error/405'; break;
|
||||
case '408': $redirect = 'error/408'; break;
|
||||
case '409': $redirect = 'error/409'; break;
|
||||
case '410': $redirect = 'error/410'; break;
|
||||
case '415': $redirect = 'error/415'; break;
|
||||
case '429': $redirect = 'error/429'; break;
|
||||
case '500': $redirect = 'error/500'; break;
|
||||
case '501': $redirect = 'error/501'; break;
|
||||
case '502': $redirect = 'error/502'; break;
|
||||
case '503': $redirect = 'error/503'; break;
|
||||
case '504': $redirect = 'error/504'; break;
|
||||
default: $redirect = 'error/default'; break;
|
||||
}
|
||||
|
||||
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
||||
|
||||
$log_data = [
|
||||
'correlation_id' => $_SESSION['ID'] ?? 'unknown',
|
||||
'timestamp' => gmdate(DATE_ATOM),
|
||||
'error_code' => $error_code,
|
||||
'log_data' => $data,
|
||||
'ip' => $visitor['ip'] ?? 'unknown',
|
||||
'state' => $visitor['state'] ?? 'unknown',
|
||||
'country' => $visitor['country'] ?? 'unknown'
|
||||
];
|
||||
|
||||
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
|
||||
$week = gmdate('o-\WW');
|
||||
file_put_contents(__ROOT__ . '/logs/errors_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
function logging_requests() {
|
||||
$requests_per_second = calculateRequestVelocity($_SESSION['ID']);
|
||||
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
||||
|
||||
if($requests_per_second < BLOCK_THRESHOLD) {
|
||||
$log_data = [
|
||||
'correlation_id' => $_SESSION['ID'],
|
||||
'timestamp' => gmdate(DATE_ATOM),
|
||||
];
|
||||
|
||||
if(isRealVisitor() == true) {
|
||||
$log_data['type'] = 'visitor';
|
||||
sendVisitorNotification();
|
||||
} else {
|
||||
$log_data['type'] = 'bot';
|
||||
}
|
||||
|
||||
$log_data = array_merge($log_data, [
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
|
||||
'uri' => $_SERVER['REQUEST_URI'] ?? 'UNKNOWN',
|
||||
'ip' => $visitor['ip'] ?? 'unknown',
|
||||
'state' => $visitor['state'] ?? 'unknown',
|
||||
'country' => $visitor['country'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN',
|
||||
'requests_per_second' => $requests_per_second
|
||||
]);
|
||||
|
||||
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
|
||||
$week = gmdate('o-\WW');
|
||||
file_put_contents(__ROOT__ . '/logs/requests_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
} else {
|
||||
$redirect = logging_errors("429", $_SERVER['REQUEST_URI']);
|
||||
header('Location: ' . __URL__ . $redirect);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
//====================================================
|
||||
// SECURITY FUNCTIONS
|
||||
//====================================================
|
||||
|
||||
function calculateRequestVelocity($session_id) {
|
||||
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
||||
$now = time();
|
||||
$csv_line = $session_id . ',' . $now . PHP_EOL;
|
||||
file_put_contents($velocity_file, $csv_line, FILE_APPEND | LOCK_EX);
|
||||
|
||||
$time_window = 1;
|
||||
$requests_per_second = calculateRecentRequests($session_id, $time_window);
|
||||
return $requests_per_second;
|
||||
}
|
||||
|
||||
function calculateRecentRequests($session_id, $time_window = 1) {
|
||||
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
||||
$now = time();
|
||||
$count = 0;
|
||||
|
||||
if (!file_exists($velocity_file)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file = fopen($velocity_file, 'r');
|
||||
while (($line = fgetcsv($file)) !== FALSE) {
|
||||
if (count($line) >= 2) {
|
||||
$stored_session = $line[0];
|
||||
$timestamp = (int)$line[1];
|
||||
if ($stored_session === $session_id && ($now - $timestamp) <= $time_window) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($file);
|
||||
return $count;
|
||||
}
|
||||
|
||||
function calculateTotalSessionRequests($session_id) {
|
||||
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
||||
$count = 0;
|
||||
|
||||
if (!file_exists($velocity_file)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file = fopen($velocity_file, 'r');
|
||||
while (($line = fgetcsv($file)) !== FALSE) {
|
||||
if (count($line) >= 2 && $line[0] === $session_id) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
fclose($file);
|
||||
return $count;
|
||||
}
|
||||
|
||||
function sendVisitorNotification() {
|
||||
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) return;
|
||||
if (isset($_SESSION['visitor_notification_sent'])) return;
|
||||
|
||||
$total_requests = calculateTotalSessionRequests($_SESSION['ID']);
|
||||
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
||||
|
||||
if ($total_requests >= 2) {
|
||||
$_SESSION['visitor_notification_sent'] = true;
|
||||
|
||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN';
|
||||
$ua_short = strlen($ua) > 80 ? substr($ua, 0, 77) . '...' : $ua;
|
||||
$referrer = $_SERVER['HTTP_REFERER'] ?? 'direct';
|
||||
$timestamp = gmdate('Y-m-d H:i:s') . ' UTC';
|
||||
|
||||
$message = "👤 *Visitor — " . ($visitor['country'] ?? 'Unknown') . "*\n"
|
||||
. "• *Time:* $timestamp\n"
|
||||
. "• *Page:* " . ($_SERVER['REQUEST_URI'] ?? '/') . "\n"
|
||||
. "• *Referrer:* $referrer\n"
|
||||
. "• *IP:* " . ($visitor['ip'] ?? 'unknown') . "\n"
|
||||
. "• *Location:* " . ($visitor['city'] ?? '') . ", " . ($visitor['country'] ?? '') . "\n"
|
||||
. "• *Pages:* " . $total_requests . "\n"
|
||||
. "• *Browser:* $ua_short\n"
|
||||
. "• *Session:* `" . $_SESSION['ID'] . "`";
|
||||
|
||||
send2telegram($message);
|
||||
}
|
||||
}
|
||||
|
||||
function isRealVisitor() {
|
||||
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? '';
|
||||
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
$accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||
|
||||
$browser_patterns = [
|
||||
'/Mozilla\/5\.0.*(Chrome|Firefox|Safari|Edg)\//i',
|
||||
'/Mozilla\/5\.0.*Mobile.*Safari/i',
|
||||
'/Mozilla\/5\.0.*(iPhone|Android).*(Chrome|Safari)/i',
|
||||
];
|
||||
foreach ($browser_patterns as $pattern) {
|
||||
if (preg_match($pattern, $user_agent)) return true;
|
||||
}
|
||||
|
||||
foreach (BOT_USER_AGENT_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $user_agent)) return false;
|
||||
}
|
||||
|
||||
if (empty($accept) || empty($accept_lang)) return false;
|
||||
if (empty($user_agent) || $user_agent === 'UNKNOWN' || strlen($user_agent) < 20) return false;
|
||||
|
||||
foreach (BLOCKED_PATHS as $path) {
|
||||
if (strpos($uri, $path) !== false) return false;
|
||||
}
|
||||
|
||||
$allowed_methods = ['GET', 'POST', 'HEAD'];
|
||||
if (!in_array($method, $allowed_methods)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//====================================================
|
||||
// UTILITY FUNCTIONS
|
||||
//====================================================
|
||||
|
||||
function getVisitor() {
|
||||
if (!defined('GEO_KEY') || empty(GEO_KEY)) return false;
|
||||
|
||||
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$ip = trim($ipList[0]);
|
||||
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => 'https://api.ipgeolocation.io/v2/ipgeo?apiKey='.GEO_KEY.'&ip='.$ip,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 2,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
]);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($curl);
|
||||
curl_close($curl);
|
||||
|
||||
if ($response === false || $httpCode !== 200) {
|
||||
$err = $curlError ?: "HTTP $httpCode";
|
||||
error_log("Geo API failed ($err) for IP $ip");
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data)) {
|
||||
return [
|
||||
'ip' => $data['ip'],
|
||||
'city' => $data['location']['city'],
|
||||
'state' => $data['location']['state_prov'],
|
||||
'country' => $data['location']['country_name']
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function loadLanguage() {
|
||||
$language = !empty($_SESSION['SITE']['language']) ? $_SESSION['SITE']['language'] : 'en';
|
||||
$languageFile = __ROOT__ . "/lng/{$language}/menu.php";
|
||||
|
||||
if (file_exists($languageFile)) {
|
||||
require_once($languageFile);
|
||||
} else {
|
||||
require_once(__ROOT__ . "/lng/en/menu.php");
|
||||
}
|
||||
}
|
||||
|
||||
function generateCorrelationId(): string {
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
function cleanupVelocityLog() {
|
||||
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
||||
$temp_file = __ROOT__ . '/logs/velocity_temp.csv';
|
||||
$now = time();
|
||||
$max_age = 86400;
|
||||
|
||||
if (!file_exists($velocity_file)) return;
|
||||
|
||||
$input = fopen($velocity_file, 'r');
|
||||
$output = fopen($temp_file, 'w');
|
||||
|
||||
while (($line = fgetcsv($input)) !== FALSE) {
|
||||
if (count($line) >= 2) {
|
||||
$timestamp = (int)$line[1];
|
||||
if (($now - $timestamp) <= $max_age) {
|
||||
fputcsv($output, $line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose($input);
|
||||
fclose($output);
|
||||
rename($temp_file, $velocity_file);
|
||||
}
|
||||
|
||||
//====================================================
|
||||
// TELEGRAM FUNCTIONS — optional, requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
|
||||
//====================================================
|
||||
|
||||
function send2telegram($message) {
|
||||
if (!defined('TELEGRAM_BOT_TOKEN') || !defined('TELEGRAM_CHAT_ID') ||
|
||||
empty(TELEGRAM_BOT_TOKEN) || empty(TELEGRAM_CHAT_ID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = "https://api.telegram.org/bot" . TELEGRAM_BOT_TOKEN . "/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => TELEGRAM_CHAT_ID,
|
||||
'text' => $message,
|
||||
'parse_mode' => 'Markdown'
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $data,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 3,
|
||||
CURLOPT_CONNECTTIMEOUT => 2
|
||||
]);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error || $httpCode !== 200) {
|
||||
error_log("Telegram API failed: " . ($error ?: "HTTP $httpCode"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
15
html/class/site.php
Normal file
15
html/class/site.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
# Set Site
|
||||
if(empty($_SESSION)){
|
||||
$_SESSION['ID']=generateCorrelationId();
|
||||
$_SESSION['TIMESTAMP']=gmdate(DATE_ATOM);
|
||||
$_SESSION['VISITOR'] = getVisitor();
|
||||
$_SESSION['SITE']=array();
|
||||
$_SESSION['SITE']['slug']='';
|
||||
$_SESSION['SITE']['language']='';
|
||||
$activeLanguage=substr($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en', 0, 2);
|
||||
$supported = array_keys(LANGUAGES);
|
||||
$_SESSION['SITE']['language'] = in_array($activeLanguage, $supported) ? $activeLanguage : 'en';
|
||||
cleanupVelocityLog();
|
||||
}
|
||||
logging_requests();
|
||||
19
html/index.php
Normal file
19
html/index.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
session_start([
|
||||
'cookie_lifetime' => 86400,
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'Lax',
|
||||
'use_strict_mode' => true
|
||||
]);
|
||||
|
||||
require_once('./class/config.php');
|
||||
require_once('./class/functions.php');
|
||||
require_once('./class/site.php');
|
||||
|
||||
if (!empty($_POST)) { $HTML_REQUEST = filter_input_array(INPUT_POST, FILTER_SANITIZE_FULL_SPECIAL_CHARS);}
|
||||
if (!empty($_GET)) { $HTML_REQUEST = filter_input_array(INPUT_GET, FILTER_SANITIZE_FULL_SPECIAL_CHARS);}
|
||||
if (!empty($HTML_REQUEST)) { require_once(__ROOT__.'/app/controler/request.php'); }
|
||||
else {
|
||||
$PARAMS = explode("/", $_SERVER['REQUEST_URI']);
|
||||
require_once(__ROOT__.'/app/controler/start.php');
|
||||
}
|
||||
1
html/lng/de/about.phtml
Normal file
1
html/lng/de/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Wie es funktioniert</h2><div class="section-content"><h3>Anfragefluss</h3><p>Jede Anfrage trifft <code>index.php</code>. Das ist die einzige PHP-Datei, die nginx ausführen lässt — alle anderen geben 404 zurück.</p><h3>Routing</h3><p>URLs werden über Slug-Konstanten in <code>class/config.php</code> auf Seitenvorlagen abgebildet. Neue Seite hinzufügen: Slug definieren, Menütext in <code>lng/{lang}/menu.php</code>, Vorlage in <code>lng/{lang}/{slug}.phtml</code>.</p><h3>Sprachen</h3><p>Jede Sprache hat ein eigenes Verzeichnis unter <code>lng/</code> mit <code>menu.php</code> und <code>.phtml</code>-Vorlagen. Der Browser <code>Accept-Language</code>-Header erkennt die Sprache automatisch.</p><h3>Sicherheit</h3><ul><li>nginx blockiert alle <code>.php</code> außer <code>index.php</code></li><li>Session-Cookies: httponly, SameSite=Lax</li><li>Bot-Erkennung mit 30+ Mustern</li><li>Rate-Limiting bei 5 Anfragen/Sekunde</li></ul></div></section>
|
||||
1
html/lng/de/home.phtml
Normal file
1
html/lng/de/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Was ist bare-site?</h2><div class="section-content"><p><strong>bare-site</strong> ist ein PHP-Mikroframework für statische Inhaltswebsites. Keine Datenbank. Kein Composer. Kein ORM. Kein Build-Schritt. Nur Dateien auf der Festplatte.</p><p>Es wurde aus 8 Jahren Produktionseinsatz entwickelt — für Unternehmenswebsites, Enterprise-Identity-Demos und mehrsprachige Broschürenseiten. Der Router ist 80 Zeilen lang.</p><h3>Wofür es geeignet ist</h3><ul><li>Unternehmens-Websites mit mehreren Sprachen</li><li>API-Integrationsdemos ohne Infrastruktur</li><li>Dokumentationsportale</li><li>Alles, was keine Datenbank benötigt</li></ul><p><strong>Philosophie:</strong> Man braucht keinen Presslufthammer, um ein Loch in Pappe zu stanzen. Die meisten Websites sind Pappe. Verwenden Sie das richtige Werkzeug.</p></div></section>
|
||||
13
html/lng/de/menu.php
Normal file
13
html/lng/de/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Sprache');
|
||||
define('PAGE_00_TXT','Was');
|
||||
define('PAGE_01_TXT','Wie');
|
||||
define('PAGE_02_TXT','Start');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Ungültige Anforderung</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Nicht gefunden</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Interner Serverfehler</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Ein Fehler ist aufgetreten</strong></center>');
|
||||
5
html/lng/de/start.phtml
Normal file
5
html/lng/de/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Loslegen</h2><div class="section-content"><h3>⚠️ Schritt 1: Webserver konfigurieren</h3><p><strong>Nicht optional.</strong> bare-site funktioniert nur mit diesen nginx-Regeln. Kopieren Sie <code>nginx/default.conf</code> — die drei kritischen Regeln:</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. NUR index.php darf PHP ausführen
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Alles über index.php routen
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Dateien auf Server ablegen</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config bearbeiten</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, Slugs, Sprachen setzen.</p><h3>4. Seiten erstellen</h3><p><code>.phtml</code>-Dateien in <code>lng/{lang}/</code> — reines HTML, keine Template-Engine.</p><h3>5. Sprache hinzufügen</h3><p>In <code>config.php</code> eintragen, Verzeichnis unter <code>lng/</code> erstellen, übersetzen. Erscheint automatisch im Nav-Dropdown.</p></div></section>
|
||||
31
html/lng/en/about.phtml
Normal file
31
html/lng/en/about.phtml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<section class="content-section">
|
||||
<h2 class="section-title">How It Works</h2>
|
||||
<div class="section-content">
|
||||
|
||||
<h3>Request Flow</h3>
|
||||
<p>Every request hits <code>index.php</code>. That's the only PHP file nginx allows to execute — everything else returns 404. From there:</p>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">Browser → nginx → index.php
|
||||
├── POST/GET form? → app/controler/request.php
|
||||
└── Clean URL? → app/controler/start.php → match slug → page</pre>
|
||||
|
||||
<h3>Routing</h3>
|
||||
<p>URLs are mapped to page templates via slug constants in <code>class/config.php</code>:</p>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">define('PAGE_00_SLUG','home'); // → /home
|
||||
define('PAGE_01_SLUG','about'); // → /about
|
||||
define('PAGE_02_SLUG','start'); // → /start</pre>
|
||||
<p>Add a new page: define the slug in config, add menu text in <code>lng/{lang}/menu.php</code>, create the template in <code>lng/{lang}/{slug}.phtml</code>. Done.</p>
|
||||
|
||||
<h3>Languages</h3>
|
||||
<p>Multi-language support is file-based. Each language gets its own directory under <code>lng/</code> with a <code>menu.php</code> (navigation text + error messages) and <code>.phtml</code> templates for each page. Browser <code>Accept-Language</code> header auto-detects the language. Users can switch via the nav dropdown.</p>
|
||||
|
||||
<h3>Security (no config required)</h3>
|
||||
<ul>
|
||||
<li>nginx blocks all <code>.php</code> except <code>index.php</code></li>
|
||||
<li>Session cookies: httponly, SameSite=Lax, strict_mode</li>
|
||||
<li>Input sanitized via <code>FILTER_SANITIZE_FULL_SPECIAL_CHARS</code></li>
|
||||
<li>Bot detection: 30+ UA patterns + browser heuristic bypass</li>
|
||||
<li>Rate limiting: blocks at 5 req/s per session</li>
|
||||
<li>Path traversal protection via <code>basename()</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
25
html/lng/en/home.phtml
Normal file
25
html/lng/en/home.phtml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<section class="content-section">
|
||||
<h2 class="section-title">A Zero-Dependency PHP Micro-Framework</h2>
|
||||
<div class="section-content">
|
||||
<p><strong>bare-site</strong> is a PHP micro-framework for static content websites. No database. No Composer. No ORM. No build step. Just files on disk.</p>
|
||||
|
||||
<p>It was built from 8 years of production use across company websites, enterprise identity-verification demos, and multi-language brochure sites. The core router is 80 lines. The entire framework — routing, bot detection, rate limiting, geo-location, Telegram notifications, structured logging, and session management — fits in 3 class files.</p>
|
||||
|
||||
<h3>What it's for</h3>
|
||||
<ul>
|
||||
<li>Company brochure sites with multiple languages</li>
|
||||
<li>API-integration demos that need zero infrastructure</li>
|
||||
<li>Documentation portals or static content delivery</li>
|
||||
<li>Anything that doesn't need a database</li>
|
||||
</ul>
|
||||
|
||||
<h3>What it's NOT</h3>
|
||||
<ul>
|
||||
<li>A WordPress alternative (no admin panel, no users, no plugins)</li>
|
||||
<li>A headless CMS (no API, no content editing interface)</li>
|
||||
<li>A Laravel competitor (no ORM, no migrations, no queues)</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Philosophy:</strong> You don't need a hammer drill to punch a hole in cardboard. Most websites are cardboard. Use the right tool.</p>
|
||||
</div>
|
||||
</section>
|
||||
26
html/lng/en/menu.php
Normal file
26
html/lng/en/menu.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
// Language file — EN
|
||||
define('LANGUAGE_TITLE','Language');
|
||||
define('PAGE_00_TXT','What');
|
||||
define('PAGE_01_TXT','How');
|
||||
define('PAGE_02_TXT','Start');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Bad Request</strong></center>');
|
||||
define( 'ERROR_401', '<center><strong>401 Unauthorized</strong></center>');
|
||||
define( 'ERROR_403', '<center><strong>403 Forbidden</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Not Found</strong></center>');
|
||||
define( 'ERROR_405', '<center><strong>405 Method Not Allowed</strong></center>');
|
||||
define( 'ERROR_408', '<center><strong>408 Request Timeout</strong></center>');
|
||||
define( 'ERROR_409', '<center><strong>409 Conflict</strong></center>');
|
||||
define( 'ERROR_410', '<center><strong>410 Gone</strong></center>');
|
||||
define( 'ERROR_415', '<center><strong>415 Unsupported Media Type</strong></center>');
|
||||
define( 'ERROR_429', '<center><strong>429 Too Many Requests</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Internal Server Error</strong></center>');
|
||||
define( 'ERROR_501', '<center><strong>501 Not Implemented</strong></center>');
|
||||
define( 'ERROR_502', '<center><strong>502 Bad Gateway</strong></center>');
|
||||
define( 'ERROR_503', '<center><strong>503 Service Unavailable</strong></center>');
|
||||
define( 'ERROR_504', '<center><strong>504 Gateway Timeout</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>An Error Occurred</strong></center>');
|
||||
85
html/lng/en/start.phtml
Normal file
85
html/lng/en/start.phtml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<section class="content-section">
|
||||
<h2 class="section-title">Get Started</h2>
|
||||
<div class="section-content">
|
||||
|
||||
<h3>⚠️ Step 1: Configure your web server</h3>
|
||||
<p><strong>This is not optional.</strong> bare-site's entire security model depends on the web server blocking direct access to PHP files. The framework <em>will not work</em> without these rules.</p>
|
||||
|
||||
<h4>nginx (recommended)</h4>
|
||||
<p>Copy <code>nginx/default.conf</code> to your server and update the paths. The three critical rules:</p>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. ONLY index.php can execute — all other PHP files are blocked
|
||||
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.
|
||||
|
||||
# 2. Everything routes through index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre>
|
||||
|
||||
<h4>Apache (.htaccess)</h4>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;">RewriteEngine On
|
||||
# Route everything through index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
|
||||
# Block direct access to PHP files except index.php
|
||||
RewriteCond %{REQUEST_URI} !^/index\.php$
|
||||
RewriteRule \.php$ - [R=404,L]</pre>
|
||||
|
||||
<h4>Caddy</h4>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;">your-domain.com {
|
||||
root * /var/www/html
|
||||
php_fastcgi unix//run/php/php8.4-fpm.sock
|
||||
file_server
|
||||
# Block direct PHP access except index.php
|
||||
@blockedPhp path_regexp /(?!index\.php$).*\.php$
|
||||
respond @blockedPhp 404
|
||||
}</pre>
|
||||
|
||||
<h3>2. Drop the files on a server</h3>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/
|
||||
# or just copy the html/ directory contents</pre>
|
||||
|
||||
<h3>3. Edit your config</h3>
|
||||
<p>Open <code>class/config.php</code> and set:</p>
|
||||
<ul>
|
||||
<li><code>SITE_NAME</code> — your site title</li>
|
||||
<li><code>SITE_DESCRIPTION</code> — meta description</li>
|
||||
<li>Page slugs (<code>PAGE_00_SLUG</code> through <code>PAGE_05_SLUG</code>)</li>
|
||||
<li>Supported languages in <code>$languages</code> array</li>
|
||||
<li>Optional: <code>GEO_KEY</code> for visitor location, <code>TELEGRAM_*</code> for notifications</li>
|
||||
</ul>
|
||||
|
||||
<h3>4. Create your pages</h3>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">lng/en/
|
||||
├── menu.php # navigation labels + error messages
|
||||
├── home.phtml # your content — raw HTML, no template engine
|
||||
├── about.phtml # another page
|
||||
└── start.phtml</pre>
|
||||
<p>Content files are plain HTML inside a <code><section></code>. No template syntax to learn. You can use PHP inline if needed — <code><?php echo date('Y'); ?></code>.</p>
|
||||
|
||||
<h3>5. Add a language</h3>
|
||||
<pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;"># 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.</pre>
|
||||
|
||||
<h3>Optional features</h3>
|
||||
<table style="width:100%;border-collapse:collapse;margin:1rem 0;">
|
||||
<tr style="border-bottom:1px solid #e2e8f0;"><td style="padding:0.5rem;"><strong>Geo-location</strong></td><td style="padding:0.5rem;">Set <code>GEO_KEY</code> (free at ipgeolocation.io)</td></tr>
|
||||
<tr style="border-bottom:1px solid #e2e8f0;"><td style="padding:0.5rem;"><strong>Telegram alerts</strong></td><td style="padding:0.5rem;">Set <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></td></tr>
|
||||
<tr style="border-bottom:1px solid #e2e8f0;"><td style="padding:0.5rem;"><strong>Weekly logs</strong></td><td style="padding:0.5rem;">Auto-rotated: <code>logs/errors_2026-W26.log</code></td></tr>
|
||||
<tr style="border-bottom:1px solid #e2e8f0;"><td style="padding:0.5rem;"><strong>Article sub-pages</strong></td><td style="padding:0.5rem;">Create <code>lng/en/start/{slug}.phtml</code> → routed at <code>/start/{slug}</code></td></tr>
|
||||
<tr><td style="padding:0.5rem;"><strong>Multi-language</strong></td><td style="padding:0.5rem;">Auto-detect from <code>Accept-Language</code> header. This demo ships with 7 languages.</td></tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
1
html/lng/es/about.phtml
Normal file
1
html/lng/es/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Cómo funciona</h2><div class="section-content"><h3>Flujo de solicitudes</h3><p>Cada solicitud llega a <code>index.php</code>. Es el único archivo PHP que nginx permite ejecutar — todos los demás devuelven 404.</p><h3>Enrutamiento</h3><p>Las URLs se mapean a plantillas mediante constantes en <code>class/config.php</code>. Nueva página: define el slug, el texto del menú, crea la plantilla.</p><h3>Idiomas</h3><p>Cada idioma tiene su propio directorio bajo <code>lng/</code>. El encabezado <code>Accept-Language</code> del navegador detecta automáticamente el idioma.</p><h3>Seguridad</h3><ul><li>nginx bloquea todos los <code>.php</code> excepto <code>index.php</code></li><li>Cookies de sesión: httponly, SameSite=Lax</li><li>Detección de bots con más de 30 patrones</li><li>Limitación de tasa a 5 req/s</li></ul></div></section>
|
||||
1
html/lng/es/home.phtml
Normal file
1
html/lng/es/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">¿Qué es bare-site?</h2><div class="section-content"><p><strong>bare-site</strong> es un micro-framework PHP para sitios web de contenido estático. Sin base de datos. Sin Composer. Sin ORM. Sin paso de compilación. Solo archivos en disco.</p><p>Desarrollado a partir de 8 años de uso en producción para sitios corporativos, demos de identidad y sitios multilingües. El router tiene 80 líneas.</p><h3>Casos de uso</h3><ul><li>Sitios corporativos multilingües</li><li>Demos de integración API sin infraestructura</li><li>Portales de documentación</li><li>Todo lo que no necesita base de datos</li></ul><p><strong>Filosofía:</strong> No necesitas un martillo perforador para hacer un agujero en cartón. La mayoría de los sitios web son de cartón. Usa la herramienta adecuada.</p></div></section>
|
||||
13
html/lng/es/menu.php
Normal file
13
html/lng/es/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Idioma');
|
||||
define('PAGE_00_TXT','Qué');
|
||||
define('PAGE_01_TXT','Cómo');
|
||||
define('PAGE_02_TXT','Empezar');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Solicitud incorrecta</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 No encontrado</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Error interno del servidor</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Se ha producido un error</strong></center>');
|
||||
5
html/lng/es/start.phtml
Normal file
5
html/lng/es/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Empezar</h2><div class="section-content"><h3>⚠️ Paso 1: Configurar el servidor web</h3><p><strong>No es opcional.</strong> bare-site requiere estas reglas de nginx. Copie <code>nginx/default.conf</code> — las tres reglas críticas:</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. SOLO index.php puede ejecutar PHP
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Todo pasa por index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Colocar los archivos</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, slugs, idiomas.</p><h3>4. Páginas</h3><p>Archivos <code>.phtml</code> en <code>lng/{lang}/</code> — HTML puro.</p><h3>5. Idioma</h3><p>Añadir en <code>config.php</code>, crear directorio, traducir. Aparece automáticamente.</p></div></section>
|
||||
1
html/lng/fr/about.phtml
Normal file
1
html/lng/fr/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Comment ça marche</h2><div class="section-content"><h3>Flux de requêtes</h3><p>Chaque requête arrive sur <code>index.php</code>. C’est le seul fichier PHP que nginx autorise — tous les autres renvoient 404.</p><h3>Routage</h3><p>Les URLs sont mappées aux templates via des constantes dans <code>class/config.php</code>. Ajouter une page : définir le slug, le texte du menu, créer le template.</p><h3>Langues</h3><p>Chaque langue a son propre répertoire sous <code>lng/</code>. L’en-tête <code>Accept-Language</code> du navigateur détecte automatiquement la langue.</p><h3>Sécurité</h3><ul><li>nginx bloque tous les <code>.php</code> sauf <code>index.php</code></li><li>Cookies de session : httponly, SameSite=Lax</li><li>Détection de bots avec 30+ motifs</li><li>Limitation de débit à 5 req/s</li></ul></div></section>
|
||||
1
html/lng/fr/home.phtml
Normal file
1
html/lng/fr/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Qu’est-ce que bare-site ?</h2><div class="section-content"><p><strong>bare-site</strong> est un micro-framework PHP pour sites web à contenu statique. Pas de base de données. Pas de Composer. Pas d’ORM. Pas d’étape de build. Juste des fichiers sur disque.</p><p>Développé à partir de 8 ans d’utilisation en production pour des sites d’entreprise, des démos d’identité et des sites multilingues. Le routeur fait 80 lignes.</p><h3>Cas d’usage</h3><ul><li>Sites vitrine multilingues</li><li>Démos d’intégration API sans infrastructure</li><li>Portails de documentation</li><li>Tout ce qui n’a pas besoin de base de données</li></ul><p><strong>Philosophie :</strong> On n’utilise pas un marteau-piqueur pour faire un trou dans du carton. La plupart des sites sont en carton. Utilisez le bon outil.</p></div></section>
|
||||
13
html/lng/fr/menu.php
Normal file
13
html/lng/fr/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Langue');
|
||||
define('PAGE_00_TXT','Quoi');
|
||||
define('PAGE_01_TXT','Comment');
|
||||
define('PAGE_02_TXT','Démarrer');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Requête incorrecte</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Page introuvable</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Erreur interne du serveur</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Une erreur s’est produite</strong></center>');
|
||||
5
html/lng/fr/start.phtml
Normal file
5
html/lng/fr/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Démarrer</h2><div class="section-content"><h3>⚠️ Étape 1 : Configurer le serveur web</h3><p><strong>Non optionnel.</strong> bare-site nécessite ces règles nginx. Copiez <code>nginx/default.conf</code> — les trois règles critiques :</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. SEUL index.php peut exécuter PHP
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Tout passe par index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Déposer les fichiers</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, slugs, langues.</p><h3>4. Pages</h3><p>Fichiers <code>.phtml</code> dans <code>lng/{lang}/</code> — HTML pur.</p><h3>5. Langue</h3><p>Ajouter dans <code>config.php</code>, créer le répertoire, traduire. Apparaît automatiquement.</p></div></section>
|
||||
1
html/lng/it/about.phtml
Normal file
1
html/lng/it/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Come funziona</h2><div class="section-content"><h3>Flusso delle richieste</h3><p>Ogni richiesta arriva a <code>index.php</code>. È l’unico file PHP che nginx permette di eseguire — tutti gli altri restituiscono 404.</p><h3>Routing</h3><p>Gli URL sono mappati ai template tramite costanti in <code>class/config.php</code>. Nuova pagina: definisci lo slug, il testo del menu, crea il template.</p><h3>Lingue</h3><p>Ogni lingua ha la propria directory sotto <code>lng/</code>. L’header <code>Accept-Language</code> del browser rileva automaticamente la lingua.</p><h3>Sicurezza</h3><ul><li>nginx blocca tutti i <code>.php</code> tranne <code>index.php</code></li><li>Cookie di sessione: httponly, SameSite=Lax</li><li>Rilevamento bot con 30+ pattern</li><li>Rate limiting a 5 req/s</li></ul></div></section>
|
||||
1
html/lng/it/home.phtml
Normal file
1
html/lng/it/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Cos’è bare-site?</h2><div class="section-content"><p><strong>bare-site</strong> è un micro-framework PHP per siti web a contenuto statico. Nessun database. Nessun Composer. Nessun ORM. Nessun build step. Solo file su disco.</p><p>Sviluppato da 8 anni di utilizzo in produzione per siti aziendali, demo di identità e siti multilingue. Il router è di 80 righe.</p><h3>Casi d’uso</h3><ul><li>Siti vetrina multilingue</li><li>Demo di integrazione API senza infrastruttura</li><li>Portali di documentazione</li><li>Tutto ciò che non necessita di un database</li></ul></div></section>
|
||||
13
html/lng/it/menu.php
Normal file
13
html/lng/it/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Lingua');
|
||||
define('PAGE_00_TXT','Cosa');
|
||||
define('PAGE_01_TXT','Come');
|
||||
define('PAGE_02_TXT','Inizia');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Richiesta non valida</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Non trovato</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Errore interno del server</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Si è verificato un errore</strong></center>');
|
||||
5
html/lng/it/start.phtml
Normal file
5
html/lng/it/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Iniziare</h2><div class="section-content"><h3>⚠️ Passo 1: Configurare il server web</h3><p><strong>Non opzionale.</strong> bare-site richiede queste regole nginx. Copia <code>nginx/default.conf</code> — le tre regole critiche:</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. SOLO index.php può eseguire PHP
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Tutto passa attraverso index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Caricare i file</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, slug, lingue.</p><h3>4. Pagine</h3><p>File <code>.phtml</code> in <code>lng/{lang}/</code> — HTML puro.</p><h3>5. Lingua</h3><p>Aggiungi in <code>config.php</code>, crea la directory, traduci. Appare automaticamente.</p></div></section>
|
||||
1
html/lng/nl/about.phtml
Normal file
1
html/lng/nl/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Hoe het werkt</h2><div class="section-content"><h3>Verzoekstroom</h3><p>Elk verzoek komt binnen op <code>index.php</code>. Dat is het enige PHP-bestand dat nginx toestaat — alle andere retourneren 404.</p><h3>Routering</h3><p>URL’s worden via constanten in <code>class/config.php</code> aan templates gekoppeld. Nieuwe pagina: definieer de slug, menutekst, maak de template.</p><h3>Talen</h3><p>Elke taal heeft een eigen map onder <code>lng/</code>. De <code>Accept-Language</code>-header van de browser detecteert automatisch de taal.</p><h3>Beveiliging</h3><ul><li>nginx blokkeert alle <code>.php</code> behalve <code>index.php</code></li><li>Sessiecookies: httponly, SameSite=Lax</li><li>Botdetectie met 30+ patronen</li><li>Rate limiting op 5 req/s</li></ul></div></section>
|
||||
1
html/lng/nl/home.phtml
Normal file
1
html/lng/nl/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Wat is bare-site?</h2><div class="section-content"><p><strong>bare-site</strong> is een PHP-microframework voor statische contentwebsites. Geen database. Geen Composer. Geen ORM. Geen build-stap. Alleen bestanden op schijf.</p><p>Ontwikkeld vanuit 8 jaar productiegebruik voor bedrijfswebsites, identiteitsdemo’s en meertalige brochuresites. De router is 80 regels.</p><h3>Gebruiksscenario’s</h3><ul><li>Bedrijfswebsites met meerdere talen</li><li>API-integratiedemo’s zonder infrastructuur</li><li>Documentatieportalen</li><li>Alles wat geen database nodig heeft</li></ul></div></section>
|
||||
13
html/lng/nl/menu.php
Normal file
13
html/lng/nl/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Taal');
|
||||
define('PAGE_00_TXT','Wat');
|
||||
define('PAGE_01_TXT','Hoe');
|
||||
define('PAGE_02_TXT','Start');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Ongeldig verzoek</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Niet gevonden</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Interne serverfout</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Er is een fout opgetreden</strong></center>');
|
||||
5
html/lng/nl/start.phtml
Normal file
5
html/lng/nl/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Starten</h2><div class="section-content"><h3>⚠️ Stap 1: Webserver configureren</h3><p><strong>Niet optioneel.</strong> bare-site vereist deze nginx-regels. Kopieer <code>nginx/default.conf</code> — de drie kritieke regels:</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. ALLEEN index.php mag PHP uitvoeren
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Alles loopt via index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Bestanden plaatsen</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, slugs, talen.</p><h3>4. Pagina’s</h3><p><code>.phtml</code>-bestanden in <code>lng/{lang}/</code> — pure HTML.</p><h3>5. Taal</h3><p>Toevoegen aan <code>config.php</code>, directory aanmaken, vertalen. Verschijnt automatisch.</p></div></section>
|
||||
1
html/lng/pt/about.phtml
Normal file
1
html/lng/pt/about.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">Como funciona</h2><div class="section-content"><h3>Fluxo de solicitações</h3><p>Cada solicitação chega ao <code>index.php</code>. É o único arquivo PHP que o nginx permite executar — todos os outros retornam 404.</p><h3>Roteamento</h3><p>URLs são mapeadas para templates através de constantes em <code>class/config.php</code>. Nova página: defina o slug, o texto do menu, crie o template.</p><h3>Idiomas</h3><p>Cada idioma tem seu próprio diretório sob <code>lng/</code>. O cabeçalho <code>Accept-Language</code> do navegador detecta automaticamente o idioma.</p><h3>Segurança</h3><ul><li>nginx bloqueia todos os <code>.php</code> exceto <code>index.php</code></li><li>Cookies de sessão: httponly, SameSite=Lax</li><li>Detecção de bots com mais de 30 padrões</li><li>Limitação de taxa a 5 req/s</li></ul></div></section>
|
||||
1
html/lng/pt/home.phtml
Normal file
1
html/lng/pt/home.phtml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<section class="content-section"><h2 class="section-title">O que é bare-site?</h2><div class="section-content"><p><strong>bare-site</strong> é um micro-framework PHP para sites de conteúdo estático. Sem banco de dados. Sem Composer. Sem ORM. Sem etapa de build. Apenas arquivos em disco.</p><p>Desenvolvido a partir de 8 anos de uso em produção para sites corporativos, demonstrações de identidade e sites multilíngues. O roteador tem 80 linhas.</p><h3>Casos de uso</h3><ul><li>Sites corporativos multilíngues</li><li>Demonstrações de integração API sem infraestrutura</li><li>Portais de documentação</li><li>Tudo que não precisa de banco de dados</li></ul><p><strong>Filosofia:</strong> Você não precisa de uma britadeira para furar papelão. A maioria dos sites é de papelão. Use a ferramenta certa.</p></div></section>
|
||||
13
html/lng/pt/menu.php
Normal file
13
html/lng/pt/menu.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
define('LANGUAGE_TITLE','Idioma');
|
||||
define('PAGE_00_TXT','O quê');
|
||||
define('PAGE_01_TXT','Como');
|
||||
define('PAGE_02_TXT','Iniciar');
|
||||
define('PAGE_03_TXT','');
|
||||
define('PAGE_04_TXT','');
|
||||
define('PAGE_05_TXT','');
|
||||
define('DISCLAIMER','');
|
||||
define( 'ERROR_400', '<center><strong>400 Solicitação inválida</strong></center>');
|
||||
define( 'ERROR_404', '<center><strong>404 Não encontrado</strong></center>');
|
||||
define( 'ERROR_500', '<center><strong>500 Erro interno do servidor</strong></center>');
|
||||
define( 'ERROR_default', '<center><strong>Ocorreu um erro</strong></center>');
|
||||
5
html/lng/pt/start.phtml
Normal file
5
html/lng/pt/start.phtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<section class="content-section"><h2 class="section-title">Iniciar</h2><div class="section-content"><h3>⚠️ Passo 1: Configurar o servidor web</h3><p><strong>Não é opcional.</strong> O bare-site requer estas regras do nginx. Copie <code>nginx/default.conf</code> — as três regras críticas:</p><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.85rem;line-height:1.5;"># 1. APENAS index.php pode executar PHP
|
||||
location = /index.php { fastcgi_pass ...; }
|
||||
location ~ \.php$ { return 404; }
|
||||
# 2. Tudo passa pelo index.php
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }</pre><h3>2. Colocar os arquivos</h3><pre style="background:#f5f5f5;padding:1rem;border-radius:6px;font-size:0.9rem;">git clone https://your-repo/bare-site.git /var/www/html/</pre><h3>3. Config</h3><p><code>class/config.php</code> — <code>SITE_NAME</code>, slugs, idiomas.</p><h3>4. Páginas</h3><p>Arquivos <code>.phtml</code> em <code>lng/{lang}/</code> — HTML puro.</p><h3>5. Idioma</h3><p>Adicionar em <code>config.php</code>, criar diretório, traduzir. Aparece automaticamente.</p></div></section>
|
||||
62
nginx/default.conf
Executable file
62
nginx/default.conf
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
# bare-site nginx configuration
|
||||
# Place this in /etc/nginx/sites-available/ and symlink to sites-enabled/
|
||||
# Or include it in your main nginx.conf
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name your-domain.com;
|
||||
root /var/www/html; # ← set to your web root
|
||||
index index.php index.html index.htm;
|
||||
charset utf-8;
|
||||
|
||||
access_log /var/log/nginx/bare-site-access.log;
|
||||
error_log /var/log/nginx/bare-site-error.log;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# === THE CRITICAL RULES ===
|
||||
# These three location blocks are the security foundation of bare-site.
|
||||
# Do NOT change the order — nginx processes locations in order.
|
||||
|
||||
# 1. ONLY index.php can execute PHP
|
||||
location = /index.php {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock; # ← adjust to your PHP-FPM socket
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
}
|
||||
|
||||
# 2. BLOCK all other .php files from direct execution
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 3. Everything else routes through index.php
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# === Static file caching ===
|
||||
location ~ \.css$ {
|
||||
access_log off;
|
||||
expires max;
|
||||
add_header Content-Type text/css;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location ~* ^.+\.(jpg|jpeg|gif|png|webp|js|ico|svg|woff|woff2|ttf)$ {
|
||||
access_log off;
|
||||
expires max;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# === Block sensitive files ===
|
||||
location ~ /\.ht { deny all; }
|
||||
location ~* \.(env|log|sql|md|yml|yaml)$ { deny all; }
|
||||
}
|
||||
12
nginx/security.conf
Executable file
12
nginx/security.conf
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
# Additional security configurations
|
||||
# This file is automatically included by nginx.conf
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||
|
||||
# Security headers for all responses
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# Hide server version
|
||||
server_tokens off;
|
||||
Loading…
Reference in a new issue