Skip to main content

Website Builder

Custom website builder

  • Backend: PHP (απλό, ευρέως υποστηριζόμενο, ιδανικό για self-hosted λύσεις με Caddy)
  • Drag & Drop Editor: GrapesJS — το πιο ισχυρό open-source drag-and-drop page builder
  • Database: SQLite (μηδενική διαμόρφωση) ή MySQL/MariaDB
  • Frontend: Vanilla JS + Bootstrap 5 (responsive σε όλες τις οθόνες)

Ακολουθεί η πλήρης υλοποίηση:


📁 Δομή Αρχείων

/website-builder/
├── index.php              ← Public frontend (εμφανίζει τις σελίδες)
├── admin/
│   ├── index.php          ← Login page
│   ├── dashboard.php      ← Λίστα σελίδων
│   ├── editor.php         ← Drag & Drop editor (GrapesJS)
│   ├── logout.php
│   └── save.php           ← API endpoint για αποθήκευση
├── includes/
│   ├── auth.php           ← Authentication helper
│   ├── db.php             ← Database connection
│   └── security.php       ← Security helpers
├── assets/
│   └── (CSS/JS για frontend)
├── data/
│   └── pages.db           ← SQLite database
└── config.php             ← Κεντρική διαμόρφωση

⚙️ config.php

<?php
// Αλλάξτε αυτές τις τιμές πριν το deployment
define('ADMIN_USERNAME', 'admin');
define('ADMIN_PASSWORD_HASH', password_hash('changeme123!', PASSWORD_BCRYPT));
define('DB_PATH', __DIR__ . '/data/pages.db');
define('SESSION_LIFETIME', 3600); // 1 ώρα
define('APP_NAME', 'My Website');
define('TIMEZONE', 'Europe/Athens');

date_default_timezone_set(TIMEZONE);

// Security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');

🗄️ includes/db.php

<?php
require_once __DIR__ . '/../config.php';

function getDB(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        try {
            $pdo = new PDO('sqlite:' . DB_PATH, null, null, [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
            // WAL mode για καλύτερη απόδοση
            $pdo->exec('PRAGMA journal_mode=WAL');
            $pdo->exec('PRAGMA foreign_keys=ON');
            initDB($pdo);
        } catch (PDOException $e) {
            error_log('DB Error: ' . $e->getMessage());
            die('Database connection failed.');
        }
    }
    return $pdo;
}

function initDB(PDO $pdo): void {
    $pdo->exec("
        CREATE TABLE IF NOT EXISTS pages (
            id        INTEGER PRIMARY KEY AUTOINCREMENT,
            slug      TEXT NOT NULL UNIQUE,
            title     TEXT NOT NULL,
            html      TEXT NOT NULL DEFAULT '',
            css       TEXT NOT NULL DEFAULT '',
            gjsData   TEXT NOT NULL DEFAULT '{}',
            is_home   INTEGER NOT NULL DEFAULT 0,
            published INTEGER NOT NULL DEFAULT 0,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS settings (
            key   TEXT PRIMARY KEY,
            value TEXT NOT NULL DEFAULT ''
        );
    ");

    // Εισαγωγή αρχικής σελίδας αν δεν υπάρχει
    $stmt = $pdo->query("SELECT COUNT(*) as cnt FROM pages");
    $row  = $stmt->fetch();
    if ((int)$row['cnt'] === 0) {
        $pdo->exec("
            INSERT INTO pages (slug, title, html, css, gjsData, is_home, published)
            VALUES ('home', 'Home', '', '', '{}', 1, 1)
        ");
    }
}

🔐 includes/auth.php

<?php
require_once __DIR__ . '/../config.php';

function startSecureSession(): void {
    if (session_status() === PHP_SESSION_NONE) {
        session_set_cookie_params([
            'lifetime' => SESSION_LIFETIME,
            'path'     => '/',
            'secure'   => false, // Caddy handles HTTPS
            'httponly' => true,
            'samesite' => 'Strict',
        ]);
        session_start();
    }
}

function isLoggedIn(): bool {
    startSecureSession();
    if (!isset($_SESSION['admin_logged_in'], $_SESSION['last_activity'])) {
        return false;
    }
    if (time() - $_SESSION['last_activity'] > SESSION_LIFETIME) {
        session_destroy();
        return false;
    }
    // Session fixation protection
    if (!isset($_SESSION['ip']) || $_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        return false;
    }
    $_SESSION['last_activity'] = time();
    return $_SESSION['admin_logged_in'] === true;
}

function requireLogin(): void {
    if (!isLoggedIn()) {
        header('Location: /admin/index.php');
        exit;
    }
}

function login(string $username, string $password): bool {
    startSecureSession();
    if ($username !== ADMIN_USERNAME) {
        return false;
    }
    if (!password_verify($password, ADMIN_PASSWORD_HASH)) {
        return false;
    }
    // Regenerate session ID μετά το login (session fixation prevention)
    session_regenerate_id(true);
    $_SESSION['admin_logged_in'] = true;
    $_SESSION['last_activity']   = time();
    $_SESSION['ip']              = $_SERVER['REMOTE_ADDR'];
    $_SESSION['csrf_token']      = bin2hex(random_bytes(32));
    return true;
}

function getCsrfToken(): string {
    startSecureSession();
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function verifyCsrf(string $token): bool {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

🔑 admin/index.php (Login Page)

<?php
require_once __DIR__ . '/../includes/auth.php';

if (isLoggedIn()) {
    header('Location: /admin/dashboard.php');
    exit;
}

$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    if (login($username, $password)) {
        header('Location: /admin/dashboard.php');
        exit;
    } else {
        // Σκόπιμη καθυστέρηση για brute-force protection
        sleep(2);
        $error = 'Invalid username or password.';
    }
}
?>
<!DOCTYPE html>
<html lang="el">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Login – <?= htmlspecialchars(APP_NAME) ?></title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
    <style>
        body { background: #0f172a; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
        .login-card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 2.5rem; width: 100%; max-width: 400px; }
        .login-card h2 { color: #f1f5f9; font-weight: 700; margin-bottom: 1.5rem; text-align: center; }
        .form-control { background: #0f172a; border: 1px solid #475569; color: #f1f5f9; }
        .form-control:focus { background: #0f172a; border-color: #6366f1; color: #f1f5f9; box-shadow: 0 0 0 0.2rem rgba(99,102,241,.25); }
        .form-label { color: #94a3b8; }
        .btn-login { background: #6366f1; border: none; width: 100%; padding: 0.75rem; font-weight: 600; border-radius: 8px; }
        .btn-login:hover { background: #4f46e5; }
        .site-name { color: #6366f1; font-size: 1.1rem; text-align: center; margin-bottom: 0.5rem; }
    </style>
</head>
<body>
    <div class="login-card">
        <div class="site-name">⚡ <?= htmlspecialchars(APP_NAME) ?></div>
        <h2>Admin Panel</h2>
        <?php if ($error): ?>
            <div class="alert alert-danger py-2"><?= htmlspecialchars($error) ?></div>
        <?php endif; ?>
        <form method="POST" autocomplete="off" novalidate>
            <div class="mb-3">
                <label class="form-label" for="username">Username</label>
                <input type="text" id="username" name="username" class="form-control" required autofocus>
            </div>
            <div class="mb-3">
                <label class="form-label" for="password">Password</label>
                <input type="password" id="password" name="password" class="form-control" required>
            </div>
            <button type="submit" class="btn btn-primary btn-login">Sign In</button>
        </form>
    </div>
</body>
</html>

📋 admin/dashboard.php

<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/db.php';
requireLogin();

$db    = getDB();
$csrf  = getCsrfToken();
$pages = $db->query("SELECT * FROM pages ORDER BY is_home DESC, title ASC")->fetchAll();

// Δημιουργία νέας σελίδας
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        die('CSRF validation failed.');
    }

    if ($_POST['action'] === 'create') {
        $title = trim($_POST['title'] ?? '');
        $slug  = preg_replace('/[^a-z0-9-]/', '', strtolower(trim($_POST['slug'] ?? '')));
        if ($title && $slug) {
            try {
                $stmt = $db->prepare("INSERT INTO pages (slug, title, html, css, gjsData) VALUES (?,?,?,?,?)");
                $stmt->execute([$slug, $title, '', '', '{}']);
                $newId = $db->lastInsertId();
                header("Location: /admin/editor.php?id=$newId");
                exit;
            } catch (PDOException $e) {
                $createError = 'Slug already exists.';
            }
        }
    }

    if ($_POST['action'] === 'delete') {
        $id = (int)($_POST['page_id'] ?? 0);
        $db->prepare("DELETE FROM pages WHERE id = ? AND is_home = 0")->execute([$id]);
        header('Location: /admin/dashboard.php');
        exit;
    }

    if ($_POST['action'] === 'toggle_publish') {
        $id  = (int)($_POST['page_id'] ?? 0);
        $pub = (int)($_POST['published'] ?? 0);
        $db->prepare("UPDATE pages SET published = ? WHERE id = ?")->execute([$pub ? 0 : 1, $id]);
        header('Location: /admin/dashboard.php');
        exit;
    }
}
?>
<!DOCTYPE html>
<html lang="el">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard – <?= htmlspecialchars(APP_NAME) ?></title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
    <style>
        body { background: #0f172a; color: #e2e8f0; }
        .navbar { background: #1e293b !important; border-bottom: 1px solid #334155; }
        .navbar-brand { color: #6366f1 !important; font-weight: 700; }
        .card { background: #1e293b; border: 1px solid #334155; border-radius: 10px; }
        .card-header { background: #0f172a; border-bottom: 1px solid #334155; color: #94a3b8; font-size: .85rem; text-transform: uppercase; letter-spacing: .05em; }
        .table { color: #e2e8f0; }
        .table td, .table th { border-color: #334155; vertical-align: middle; }
        .badge-home { background: #6366f1; }
        .btn-edit { background: #0ea5e9; border: none; }
        .btn-edit:hover { background: #0284c7; }
        .btn-delete { background: #ef4444; border: none; }
        .form-control, .form-select { background: #0f172a; border: 1px solid #475569; color: #f1f5f9; }
        .form-control:focus, .form-select:focus { background: #0f172a; color: #f1f5f9; border-color: #6366f1; box-shadow: none; }
        .modal-content { background: #1e293b; color: #e2e8f0; border: 1px solid #334155; }
        .modal-header { border-bottom: 1px solid #334155; }
        .modal-footer { border-top: 1px solid #334155; }
    </style>
</head>
<body>
<nav class="navbar navbar-expand-lg">
    <div class="container-fluid px-4">
        <a class="navbar-brand" href="/admin/dashboard.php">⚡ <?= htmlspecialchars(APP_NAME) ?> Admin</a>
        <div class="ms-auto d-flex gap-2 align-items-center">
            <a href="/" target="_blank" class="btn btn-sm btn-outline-secondary">
                <i class="bi bi-globe"></i> View Site
            </a>
            <a href="/admin/logout.php" class="btn btn-sm btn-outline-danger">
                <i class="bi bi-box-arrow-right"></i> Logout
            </a>
        </div>
    </div>
</nav>

<div class="container-fluid px-4 py-4">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h4 class="mb-0">Pages</h4>
        <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
            <i class="bi bi-plus-lg"></i> New Page
        </button>
    </div>

    <?php if (!empty($createError)): ?>
        <div class="alert alert-danger"><?= htmlspecialchars($createError) ?></div>
    <?php endif; ?>

    <div class="card">
        <div class="card-header">All Pages (<?= count($pages) ?>)</div>
        <div class="table-responsive">
            <table class="table table-hover mb-0">
                <thead>
                    <tr>
                        <th>Title</th>
                        <th>Slug</th>
                        <th>Status</th>
                        <th>Last Updated</th>
                        <th class="text-end">Actions</th>
                    </tr>
                </thead>
                <tbody>
                <?php foreach ($pages as $page): ?>
                    <tr>
                        <td>
                            <?= htmlspecialchars($page['title']) ?>
                            <?php if ($page['is_home']): ?>
                                <span class="badge badge-home ms-1">Home</span>
                            <?php endif; ?>
                        </td>
                        <td><code class="text-info">/<?= htmlspecialchars($page['slug']) ?></code></td>
                        <td>
                            <form method="POST" class="d-inline">
                                <input type="hidden" name="csrf_token" value="<?= $csrf ?>">
                                <input type="hidden" name="action" value="toggle_publish">
                                <input type="hidden" name="page_id" value="<?= $page['id'] ?>">
                                <input type="hidden" name="published" value="<?= $page['published'] ?>">
                                <button type="submit" class="btn btn-sm <?= $page['published'] ? 'btn-success' : 'btn-secondary' ?>" style="min-width:90px">
                                    <?= $page['published'] ? '✅ Published' : '⏸ Draft' ?>
                                </button>
                            </form>
                        </td>
                        <td class="text-muted small"><?= $page['updated_at'] ?></td>
                        <td class="text-end">
                            <a href="/admin/editor.php?id=<?= $page['id'] ?>" class="btn btn-sm btn-edit text-white me-1">
                                <i class="bi bi-pencil-fill"></i> Edit
                            </a>
                            <a href="/<?= $page['slug'] === 'home' ? '' : $page['slug'] ?>" target="_blank" class="btn btn-sm btn-outline-info me-1">
                                <i class="bi bi-eye"></i>
                            </a>
                            <?php if (!$page['is_home']): ?>
                            <form method="POST" class="d-inline" onsubmit="return confirm('Delete this page?')">
                                <input type="hidden" name="csrf_token" value="<?= $csrf ?>">
                                <input type="hidden" name="action" value="delete">
                                <input type="hidden" name="page_id" value="<?= $page['id'] ?>">
                                <button type="submit" class="btn btn-sm btn-delete text-white">
                                    <i class="bi bi-trash-fill"></i>
                                </button>
                            </form>
                            <?php endif; ?>
                        </td>
                    </tr>
                <?php endforeach; ?>
                </tbody>
            </table>
        </div>
    </div>
</div>

<!-- Create Page Modal -->
<div class="modal fade" id="createModal" tabindex="-1">
    <div class="modal-dialog">
        <form method="POST">
            <input type="hidden" name="csrf_token" value="<?= $csrf ?>">
            <input type="hidden" name="action" value="create">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Create New Page</h5>
                    <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div class="mb-3">
                        <label class="form-label">Page Title</label>
                        <input type="text" name="title" class="form-control" placeholder="e.g. About Us" required id="pageTitle">
                    </div>
                    <div class="mb-3">
                        <label class="form-label">URL Slug</label>
                        <div class="input-group">
                            <span class="input-group-text bg-dark text-muted border-secondary">/</span>
                            <input type="text" name="slug" class="form-control" placeholder="about-us" required id="pageSlug" pattern="[a-z0-9-]+">
                        </div>
                        <div class="form-text text-muted">Lowercase letters, numbers, and hyphens only</div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="submit" class="btn btn-primary">Create & Edit</button>
                </div>
            </div>
        </form>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Auto-generate slug from title
document.getElementById('pageTitle').addEventListener('input', function() {
    const slug = this.value.toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^a-z0-9-]/g, '')
        .replace(/-+/g, '-')
        .replace(/^-|-$/g, '');
    document.getElementById('pageSlug').value = slug;
});
</script>
</body>
</html>

✏️ admin/editor.php

<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/db.php';
requireLogin();

$id   = (int)($_GET['id'] ?? 0);
$db   = getDB();
$page = $db->prepare("SELECT * FROM pages WHERE id = ?")->execute([$id]) ? null : null;
$stmt = $db->prepare("SELECT * FROM pages WHERE id = ?");
$stmt->execute([$id]);
$page = $stmt->fetch();

if (!$page) {
    header('Location: /admin/dashboard.php');
    exit;
}

$csrf      = getCsrfToken();
$gjsData   = json_decode($page['gjsData'] ?: '{}', true);
$gjsJson   = json_encode($gjsData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<!DOCTYPE html>
<html lang="el">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Edit: <?= htmlspecialchars($page['title']) ?> – Admin</title>

    <!-- GrapesJS -->
    <link rel="stylesheet" href="https://unpkg.com/grapesjs@0.21.13/dist/css/grapes.min.css">

    <!-- GrapesJS Plugins CSS -->
    <link rel="stylesheet" href="https://unpkg.com/grapesjs-preset-webpage@1.0.2/dist/grapesjs-preset-webpage.min.css">

    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; }

        /* ─── Top Bar ─── */
        #topbar {
            height: 48px;
            background: #1e293b;
            border-bottom: 1px solid #334155;
            display: flex;
            align-items: center;
            padding: 0 1rem;
            gap: .75rem;
            z-index: 9999;
            position: relative;
        }
        #topbar .site-name { color: #6366f1; font-weight: 700; font-size: .95rem; margin-right: auto; }
        #topbar .page-title { color: #94a3b8; font-size: .85rem; }
        #topbar button, #topbar a {
            padding: .35rem .85rem;
            border-radius: 6px;
            font-size: .82rem;
            font-weight: 600;
            cursor: pointer;
            border: none;
            text-decoration: none;
            display: inline-flex;
            align-items: center;
            gap: .35rem;
        }
        .btn-save    { background: #22c55e; color: #fff; }
        .btn-save:hover { background: #16a34a; }
        .btn-preview { background: #0ea5e9; color: #fff; }
        .btn-preview:hover { background: #0284c7; }
        .btn-back    { background: #475569; color: #fff; }
        .btn-back:hover { background: #334155; }
        .device-btns button { background: #334155; color: #94a3b8; padding: .3rem .6rem; }
        .device-btns button.active { background: #6366f1; color: #fff; }
        #save-status { font-size: .78rem; color: #64748b; min-width: 100px; }

        /* ─── Editor ─── */
        #gjs {
            height: calc(100vh - 48px);
            overflow: hidden;
        }

        /* ─── GrapesJS Theme Overrides ─── */
        .gjs-one-bg { background-color: #1e293b; }
        .gjs-two-bg { background-color: #0f172a; }
        .gjs-three-bg { background-color: #334155; }
        .gjs-four-clr { color: #6366f1; }
        .gjs-pn-panel { background-color: #1e293b; border-color: #334155; }
        .gjs-pn-views { border-color: #334155; }
        .gjs-cv-canvas { background-color: #e2e8f0; }
        .gjs-sm-sector-title { background: #0f172a; color: #94a3b8; }
        .gjs-clm-tag { background-color: #334155; }
        .gjs-block { background: #0f172a; border-color: #334155; color: #e2e8f0; }
        .gjs-block:hover { background: #334155; }
        .gjs-block__media { color: #6366f1; }
        .gjs-pn-btn { color: #94a3b8; }
        .gjs-pn-btn:hover, .gjs-pn-btn.gjs-pn-active { color: #6366f1; }
        .gjs-toolbar { background: #6366f1; }
    </style>
</head>
<body>

<!-- Top Bar -->
<div id="topbar">
    <a href="/admin/dashboard.php" class="btn-back">← Back</a>
    <span class="site-name">⚡ Admin</span>
    <span class="page-title">Editing: <strong style="color:#e2e8f0"><?= htmlspecialchars($page['title']) ?></strong></span>

    <div class="device-btns d-flex gap-1">
        <button id="btn-desktop" class="active" title="Desktop" onclick="setDevice('Desktop')">🖥</button>
        <button id="btn-tablet"  title="Tablet"  onclick="setDevice('Tablet')">📱</button>
        <button id="btn-mobile"  title="Mobile"  onclick="setDevice('Mobile portrait')">📲</button>
    </div>

    <span id="save-status"></span>
    <a href="/<?= $page['slug'] === 'home' ? '' : htmlspecialchars($page['slug']) ?>" target="_blank" class="btn-preview">👁 Preview</a>
    <button class="btn-save" onclick="savePage()">💾 Save</button>
</div>

<!-- GrapesJS Container -->
<div id="gjs"></div>

<!-- GrapesJS -->
<script src="https://unpkg.com/grapesjs@0.21.13/dist/grapes.min.js"></script>
<script src="https://unpkg.com/grapesjs-preset-webpage@1.0.2/dist/grapesjs-preset-webpage.min.js"></script>
<script src="https://unpkg.com/grapesjs-blocks-basic@1.0.1/dist/grapesjs-blocks-basic.min.js"></script>

<script>
const PAGE_ID    = <?= $page['id'] ?>;
const CSRF_TOKEN = <?= json_encode($csrf) ?>;
const savedData  = <?= $gjsJson ?>;

// ─── Initialise GrapesJS ───────────────────────────────────────────────────
const editor = grapesjs.init({
    container: '#gjs',
    fromElement: false,
    height: '100%',
    width: 'auto',
    storageManager: false,   // Χρησιμοποιούμε custom save
    noticeOnUnload: true,

    plugins: ['gjs-preset-webpage', 'grapesjs-blocks-basic'],
    pluginsOpts: {
        'gjs-preset-webpage': {
            modalImportTitle: 'Import HTML',
            modalImportButton: 'Import',
            modalImportLabel: '',
            modalImportContent: '',
            filestackOpts: null,
            aviaryOpts: false,
            blocksBasicOpts: { flexGrid: true },
            customStyleManager: [],
        },
        'grapesjs-blocks-basic': {
            flexGrid: true,
        }
    },

    canvas: {
        styles: [
            'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
        ],
        scripts: [
            'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js',
        ],
    },

    deviceManager: {
        devices: [
            { name: 'Desktop',          width: '' },
            { name: 'Tablet',           width: '768px', widthMedia: '992px' },
            { name: 'Mobile portrait',  width: '375px', widthMedia: '480px' },
        ]
    },

    styleManager: {
        sectors: [
            { name: 'General',    open: false, buildProps: ['float','display','position','top','right','left','bottom'] },
            { name: 'Dimension',  open: false, buildProps: ['width','height','max-width','min-height','margin','padding'] },
            { name: 'Typography', open: true,  buildProps: ['font-family','font-size','font-weight','letter-spacing','color','line-height','text-align','text-decoration','text-shadow'] },
            { name: 'Background', open: false, buildProps: ['background-color','background','background-repeat','background-position','background-size'] },
            { name: 'Border',     open: false, buildProps: ['border','border-radius'] },
            { name: 'Extra',      open: false, buildProps: ['opacity','cursor','overflow','transition'] },
            { name: 'Flex',       open: false, buildProps: ['align-items','justify-content','flex-direction','flex-wrap'] },
        ]
    },

    // ─── Extra Blocks ──────────────────────────────────────────────────────
    blockManager: {
        appendTo: '#blocks',
        blocks: []
    },

    panels: {
        defaults: [
            { id: 'layers',   el: '#layers-panel' },
            { id: 'styles',   el: '#styles-panel' },
            { id: 'traits',   el: '#traits-panel' },
            { id: 'blocks',   el: '#blocks-panel' },
        ]
    },
});

// Φόρτωση αποθηκευμένων δεδομένων
if (savedData && Object.keys(savedData).length > 0) {
    try { editor.loadProjectData(savedData); } catch(e) { console.warn('Load error:', e); }
}

// ─── Extra Custom Blocks ──────────────────────────────────────────────────
const bm = editor.BlockManager;

bm.add('hero-section', {
    label: 'Hero Section',
    category: 'Sections',
    media: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="1" y="4" width="22" height="16" rx="2"/></svg>',
    content: `
        <section style="padding:80px 20px;text-align:center;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;">
            <div style="max-width:700px;margin:0 auto;">
                <h1 style="font-size:3rem;font-weight:800;margin-bottom:1rem;line-height:1.2">Your Amazing Headline</h1>
                <p style="font-size:1.2rem;margin-bottom:2rem;opacity:.9">Add a compelling subtitle here that explains your value proposition clearly.</p>
                <a href="#" style="display:inline-block;background:#fff;color:#667eea;padding:14px 40px;border-radius:50px;font-weight:700;text-decoration:none;font-size:1rem">Get Started</a>
            </div>
        </section>`,
});

bm.add('features-3col', {
    label: '3-Col Features',
    category: 'Sections',
    media: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="1" y="6" width="6" height="12" rx="1"/><rect x="9" y="6" width="6" height="12" rx="1"/><rect x="17" y="6" width="6" height="12" rx="1"/></svg>',
    content: `
        <section style="padding:60px 20px;background:#f8fafc;">
            <div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:30px;">
                <div style="text-align:center;padding:30px;">
                    <div style="font-size:3rem;margin-bottom:1rem">🚀</div>
                    <h3 style="margin-bottom:.5rem;color:#1e293b">Feature One</h3>
                    <p style="color:#64748b;line-height:1.6">Describe this amazing feature and why it matters to your users.</p>
                </div>
                <div style="text-align:center;padding:30px;">
                    <div style="font-size:3rem;margin-bottom:1rem">⚡</div>
                    <h3 style="margin-bottom:.5rem;color:#1e293b">Feature Two</h3>
                    <p style="color:#64748b;line-height:1.6">Describe this amazing feature and why it matters to your users.</p>
                </div>
                <div style="text-align:center;padding:30px;">
                    <div style="font-size:3rem;margin-bottom:1rem">🎯</div>
                    <h3 style="margin-bottom:.5rem;color:#1e293b">Feature Three</h3>
                    <p style="color:#64748b;line-height:1.6">Describe this amazing feature and why it matters to your users.</p>
                </div>
            </div>
        </section>`,
});

bm.add('cta-section', {
    label: 'Call to Action',
    category: 'Sections',
    content: `
        <section style="padding:70px 20px;text-align:center;background:#1e293b;color:#fff;">
            <h2 style="font-size:2.2rem;font-weight:700;margin-bottom:1rem">Ready to Get Started?</h2>
            <p style="color:#94a3b8;font-size:1.1rem;margin-bottom:2rem">Join thousands of satisfied customers today.</p>
            <a href="#" style="display:inline-block;background:#6366f1;color:#fff;padding:14px 40px;border-radius:50px;font-weight:700;text-decoration:none">Start Free Trial</a>
        </section>`,
});

bm.add('navbar-block', {
    label: 'Navbar',
    category: 'Sections',
    content: `
        <nav style="background:#1e293b;padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
            <div style="color:#6366f1;font-weight:800;font-size:1.4rem">Logo</div>
            <div style="display:flex;gap:2rem;flex-wrap:wrap;">
                <a href="#" style="color:#e2e8f0;text-decoration:none;font-weight:500">Home</a>
                <a href="#" style="color:#94a3b8;text-decoration:none;font-weight:500">About</a>
                <a href="#" style="color:#94a3b8;text-decoration:none;font-weight:500">Services</a>
                <a href="#" style="color:#94a3b8;text-decoration:none;font-weight:500">Contact</a>
            </div>
            <a href="#" style="background:#6366f1;color:#fff;padding:.6rem 1.5rem;border-radius:6px;text-decoration:none;font-weight:600">Get Started</a>
        </nav>`,
});

bm.add('footer-block', {
    label: 'Footer',
    category: 'Sections',
    content: `
        <footer style="background:#0f172a;color:#94a3b8;padding:40px 20px;text-align:center;border-top:1px solid #334155;">
            <div style="max-width:800px;margin:0 auto;">
                <p style="font-size:1.2rem;font-weight:700;color:#e2e8f0;margin-bottom:.5rem">My Website</p>
                <p style="margin-bottom:1.5rem;font-size:.9rem">Making the web a better place.</p>
                <div style="display:flex;justify-content:center;gap:2rem;margin-bottom:1.5rem;flex-wrap:wrap;">
                    <a href="#" style="color:#64748b;text-decoration:none">Privacy</a>
                    <a href="#" style="color:#64748b;text-decoration:none">Terms</a>
                    <a href="#" style="color:#64748b;text-decoration:none">Contact</a>
                </div>
                <p style="font-size:.8rem">© 2026 My Website. All rights reserved.</p>
            </div>
        </footer>`,
});

bm.add('testimonial', {
    label: 'Testimonials',
    category: 'Sections',
    content: `
        <section style="padding:60px 20px;background:#fff;">
            <div style="max-width:1000px;margin:0 auto;">
                <h2 style="text-align:center;margin-bottom:3rem;color:#1e293b;font-size:2rem">What Our Customers Say</h2>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:24px;">
                    <div style="background:#f8fafc;border-radius:12px;padding:24px;border-left:4px solid #6366f1;">
                        <p style="color:#475569;line-height:1.7;margin-bottom:1rem">"This product completely changed the way I work. Absolutely recommend it!"</p>
                        <strong style="color:#1e293b">— Jane Doe</strong>
                    </div>
                    <div style="background:#f8fafc;border-radius:12px;padding:24px;border-left:4px solid #6366f1;">
                        <p style="color:#475569;line-height:1.7;margin-bottom:1rem">"Amazing support and great features. Worth every penny without doubt."</p>
                        <strong style="color:#1e293b">— John Smith</strong>
                    </div>
                </div>
            </div>
        </section>`,
});

bm.add('contact-form', {
    label: 'Contact Form',
    category: 'Forms',
    content: `
        <section style="padding:60px 20px;background:#f8fafc;">
            <div style="max-width:550px;margin:0 auto;">
                <h2 style="text-align:center;margin-bottom:2rem;color:#1e293b">Contact Us</h2>
                <form style="display:flex;flex-direction:column;gap:1rem;">
                    <input type="text" placeholder="Your Name" style="padding:12px 16px;border:1px solid #cbd5e1;border-radius:8px;font-size:1rem;outline:none;background:#fff">
                    <input type="email" placeholder="Email Address" style="padding:12px 16px;border:1px solid #cbd5e1;border-radius:8px;font-size:1rem;outline:none;background:#fff">
                    <textarea rows="5" placeholder="Your message..." style="padding:12px 16px;border:1px solid #cbd5e1;border-radius:8px;font-size:1rem;outline:none;background:#fff;resize:vertical"></textarea>
                    <button type="submit" style="background:#6366f1;color:#fff;padding:14px;border:none;border-radius:8px;font-size:1rem;font-weight:700;cursor:pointer">Send Message</button>
                </form>
            </div>
        </section>`,
});

// ─── Device switching ──────────────────────────────────────────────────────
function setDevice(name) {
    editor.setDevice(name);
    document.querySelectorAll('.device-btns button').forEach(b => b.classList.remove('active'));
    const map = { 'Desktop': 'btn-desktop', 'Tablet': 'btn-tablet', 'Mobile portrait': 'btn-mobile' };
    document.getElementById(map[name])?.classList.add('active');
}

// ─── Save ──────────────────────────────────────────────────────────────────
async function savePage() {
    const status = document.getElementById('save-status');
    status.textContent = '💾 Saving...';

    const payload = {
        id:       PAGE_ID,
        csrf:     CSRF_TOKEN,
        html:     editor.getHtml(),
        css:      editor.getCss(),
        gjsData:  JSON.stringify(editor.getProjectData()),
    };

    try {
        const res = await fetch('/admin/save.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
        });
        const data = await res.json();
        if (data.success) {
            status.textContent = '✅ Saved!';
            setTimeout(() => status.textContent = '', 3000);
        } else {
            status.textContent = '❌ Error: ' + (data.message || 'Unknown');
        }
    } catch (e) {
        status.textContent = '❌ Network error';
    }
}

// Ctrl+S shortcut
document.addEventListener('keydown', e => {
    if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); savePage(); }
});
</script>
</body>
</html>

💾 admin/save.php

<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/db.php';

header('Content-Type: application/json');

if (!isLoggedIn()) {
    http_response_code(401);
    echo json_encode(['success' => false, 'message' => 'Unauthorized']);
    exit;
}

$raw = file_get_contents('php://input');
if (!$raw) {
    echo json_encode(['success' => false, 'message' => 'No data']);
    exit;
}

$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    echo json_encode(['success' => false, 'message' => 'Invalid JSON']);
    exit;
}

// CSRF check
if (!verifyCsrf($data['csrf'] ?? '')) {
    http_response_code(403);
    echo json_encode(['success' => false, 'message' => 'CSRF error']);
    exit;
}

$id      = (int)($data['id'] ?? 0);
$html    = $data['html'] ?? '';
$css     = $data['css'] ?? '';
$gjsData = $data['gjsData'] ?? '{}';

// Validate gjsData is valid JSON
json_decode($gjsData);
if (json_last_error() !== JSON_ERROR_NONE) {
    $gjsData = '{}';
}

// Sanitize: maximum size limits (prevent oversized data)
if (strlen($html) > 2_000_000 || strlen($css) > 500_000 || strlen($gjsData) > 5_000_000) {
    echo json_encode(['success' => false, 'message' => 'Content too large']);
    exit;
}

try {
    $db   = getDB();
    $stmt = $db->prepare("
        UPDATE pages SET html = ?, css = ?, gjsData = ?, updated_at = datetime('now')
        WHERE id = ?
    ");
    $stmt->execute([$html, $css, $gjsData, $id]);

    if ($stmt->rowCount() === 0) {
        echo json_encode(['success' => false, 'message' => 'Page not found']);
        exit;
    }

    echo json_encode(['success' => true]);
} catch (PDOException $e) {
    error_log('Save error: ' . $e->getMessage());
    echo json_encode(['success' => false, 'message' => 'Database error']);
}

🚪 admin/logout.php

<?php
require_once __DIR__ . '/../includes/auth.php';
startSecureSession();
session_destroy();
header('Location: /admin/index.php');
exit;

🌐 index.php (Public Frontend)

<?php
require_once __DIR__ . '/includes/db.php';

// Προσδιορισμός slug από URL
$uri  = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$slug = trim($uri, '/');
if ($slug === '') $slug = 'home';

// Αφαίρεση επικίνδυνων χαρακτήρων
$slug = preg_replace('/[^a-z0-9-]/', '', strtolower($slug));

$db   = getDB();
$stmt = $db->prepare("SELECT * FROM pages WHERE slug = ? AND published = 1");
$stmt->execute([$slug]);
$page = $stmt->fetch();

if (!$page) {
    http_response_code(404);
    echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>404 Not Found</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <style>body{font-family:system-ui;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}
    .box{text-align:center}.code{font-size:6rem;font-weight:900;color:#6366f1}.msg{font-size:1.2rem;color:#94a3b8;margin-bottom:2rem}
    a{color:#6366f1;text-decoration:none;font-weight:600}</style></head>
    <body><div class="box"><div class="code">404</div><div class="msg">Page not found</div><a href="/">← Go Home</a></div></body></html>';
    exit;
}

$title   = htmlspecialchars($page['title']);
$appName = htmlspecialchars(APP_NAME);
?>
<!DOCTYPE html>
<html lang="el">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="index, follow">
    <title><?= $title ?> – <?= $appName ?></title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
    <style>
        /* Reset GrapesJS-generated styles */
        * { box-sizing: border-box; }
        body { margin: 0; padding: 0; }
        img { max-width: 100%; height: auto; }
        <?= $page['css'] ?>
    </style>
</head>
<body>
    <?= $page['html'] ?>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

🔄 .htaccess (Apache) ή Caddy Config

Για Apache:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/admin/
RewriteCond %{REQUEST_URI} !^/assets/
RewriteRule ^(.*)$ /index.php [L,QSA]

Caddy Caddyfile:

yourdomain.com {
    root * /var/www/website-builder
    php_fastcgi unix//run/php/php8.2-fpm.sock
    file_server
    
    # Προστασία sensitive αρχείων
    @sensitive {
        path /data/* /config.php /includes/*
    }
    respond @sensitive 403
    
    # URL rewriting για clean URLs
    @notFound {
        not file
        not path /admin/* /assets/*
    }
    rewrite @notFound /index.php
    
    encode gzip
}

🚀 Εγκατάσταση

# 1. Clone / copy files to server
mkdir -p /var/www/website-builder/data
chown www-data:www-data /var/www/website-builder/data
chmod 755 /var/www/website-builder/data

# 2. Αλλάξτε το password στο config.php πριν το deploy!
# Γεννήστε νέο hash:
php -r "echo password_hash('your-strong-password', PASSWORD_BCRYPT);"

# 3. Ρυθμίστε τo Caddy και επανεκκινήστε
systemctl reload caddy

🔒 Security Features

  • CSRF Protection σε όλα τα forms
  • Session fixation prevention με session_regenerate_id()
  • Brute-force mitigation (sleep(2) σε λανθασμένο login)
  • Session timeout (1 ώρα αδράνειας)
  • IP binding session
  • Parameterized queries (PDO prepared statements — χωρίς SQL injection)
  • Input sanitization (htmlspecialchars, slug validation, JSON validation)
  • Content size limits στο save endpoint
  • Security headers (X-Frame-Options, XSS Protection, κτλ.)
  • Caddy blocks direct access σε /data/, /config.php, /includes/

Το project είναι έτοιμο για άμεση χρήση. Απλώς αλλάξτε το password στο config.php πριν το ανεβάσετε στον server!

🚀 Εγκατάσταση σε Proxmox LXC — Debian 13

Βήμα 1: Δημιουργία LXC Container στο PVE

Στο Proxmox web UI:

  1. Create CT → δώσε hostname (π.χ. website-builder)
  2. Template: debian-13-standard (κατέβασέ το από Datacenter → Storage → CT Templates → Templates)
  3. Disk: Τουλάχιστον 4GB
  4. RAM: 512MB minimum (1GB recommended)
  5. Network: Δώσε static IP ή DHCP
  6. Start after created:

Βήμα 2: Σύνδεση στο Container

# Από το PVE host shell (αντικατέστησε 100 με το CT ID σου)
pct enter 100

# Ή μέσω SSH αν έχεις ήδη δίκτυο
ssh root@<container-ip>

Βήμα 3: Βασικές Ενημερώσεις

apt update && apt upgrade -y
apt install -y curl wget git unzip nano

Βήμα 4: Εγκατάσταση Apache2 + PHP 8.4

# Apache
apt install -y apache2

# PHP 8.4 με όλα τα απαραίτητα modules
apt install -y php8.4 libapache2-mod-php8.4 php8.4-sqlite3 php8.4-mbstring php8.4-curl php8.4-xml php8.4-json

# Ενεργοποίηση mod_rewrite (για clean URLs)
a2enmod rewrite

# Επανεκκίνηση Apache
systemctl restart apache2

# Επαλήθευση
php -v
apache2 -v

Βήμα 5: Δημιουργία Δομής Φακέλων

# Βασικός φάκελος του project
mkdir -p /var/www/website-builder/{admin,includes,assets,data}

# Σωστά permissions
chown -R www-data:www-data /var/www/website-builder/data
chmod 755 /var/www/website-builder/data

# Το data directory πρέπει να είναι writable για το SQLite
chown www-data:www-data /var/www/website-builder/data
chmod 775 /var/www/website-builder/data

Η δομή που δημιουργείται:

/var/www/website-builder/
├── index.php
├── config.php
├── .htaccess
├── admin/
│   ├── index.php
│   ├── dashboard.php
│   ├── editor.php
│   ├── save.php
│   └── logout.php
├── includes/
│   ├── db.php
│   ├── auth.php
│   └── security.php
├── assets/
└── data/
    └── pages.db  ← (δημιουργείται αυτόματα)

Βήμα 6: Αντιγραφή Αρχείων

Δημιούργησε κάθε αρχείο με nano. Για παράδειγμα:

nano /var/www/website-builder/config.php
# → Επικόλλησε το περιεχόμενο του config.php από τον κώδικα παραπάνω
# → Ctrl+O (save) → Ctrl+X (exit)

nano /var/www/website-builder/index.php
nano /var/www/website-builder/.htaccess

nano /var/www/website-builder/includes/db.php
nano /var/www/website-builder/includes/auth.php

nano /var/www/website-builder/admin/index.php
nano /var/www/website-builder/admin/dashboard.php
nano /var/www/website-builder/admin/editor.php
nano /var/www/website-builder/admin/save.php
nano /var/www/website-builder/admin/logout.php

💡 Tip: Εναλλακτικά μπορείς να χρησιμοποιήσεις SCP/SFTP από τον υπολογιστή σου για να ανεβάσεις τα αρχεία μαζικά: bash > # Από τον υπολογιστή σου (όχι μέσα στο container) > scp -r ./website-builder/ root@<container-ip>:/var/www/ >


Βήμα 7: Σωστό Αρχείο .htaccess

nano /var/www/website-builder/.htaccess

Περιεχόμενο:

Options -Indexes
RewriteEngine On

# Προστασία sensitive αρχείων
<FilesMatch "(config\.php|\.db)$">
    Order allow,deny
    Deny from all
</FilesMatch>

# Block direct access σε includes/ και data/
RewriteRule ^(includes|data)/ - [F,L]

# Clean URLs - redirect όλα στο index.php
# εκτός από admin/, assets/, και υπάρχοντα αρχεία
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/admin/
RewriteCond %{REQUEST_URI} !^/assets/
RewriteRule ^(.*)$ /index.php [L,QSA]

Βήμα 8: Ρύθμιση Apache Virtual Host

nano /etc/apache2/sites-available/website-builder.conf

Περιεχόμενο:

<VirtualHost *:80>
    ServerName yourdomain.com
    DocumentRoot /var/www/website-builder

    <Directory /var/www/website-builder>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    # Απόκρυψη sensitive directories
    <Directory /var/www/website-builder/data>
        Require all denied
    </Directory>
    <Directory /var/www/website-builder/includes>
        Require all denied
    </Directory>

    ErrorLog  ${APACHE_LOG_DIR}/website-builder-error.log
    CustomLog ${APACHE_LOG_DIR}/website-builder-access.log combined
</VirtualHost>
# Ενεργοποίηση του site, απενεργοποίηση του default
a2ensite website-builder.conf
a2dissite 000-default.conf

# Test config
apache2ctl configtest

# Reload
systemctl reload apache2

Βήμα 9: Ρύθμιση PHP.ini

nano /etc/php/8.4/apache2/php.ini

Βρες και άλλαξε αυτές τις τιμές (με Ctrl+W για αναζήτηση):

; Timezone
date.timezone = "Europe/Athens"

; Μέγεθος upload (για εικόνες)
upload_max_filesize = 32M
post_max_size = 64M

; Ασφάλεια - απόκρυψη PHP version
expose_php = Off

; Μέγιστο μέγεθος memory
memory_limit = 256M

; Εμφάνιση errors μόνο σε development (βάλε Off σε production)
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
systemctl reload apache2

Βήμα 10: Permissions Τελικά

# Ιδιοκτησία όλων των αρχείων
chown -R www-data:www-data /var/www/website-builder

# Permissions αρχείων (644) και φακέλων (755)
find /var/www/website-builder -type f -exec chmod 644 {} \;
find /var/www/website-builder -type d -exec chmod 755 {} \;

# Data directory - writable για SQLite
chmod 775 /var/www/website-builder/data

# Config προστασία
chmod 640 /var/www/website-builder/config.php

Βήμα 11: Αλλαγή Password στο config.php

# Δημιούργησε νέο bcrypt hash με το password που θέλεις
php -r "echo password_hash('ΤΟ-ΔΙΚΟ-ΣΟΥ-PASSWORD', PASSWORD_BCRYPT) . PHP_EOL;"

Θα σου βγάλει κάτι σαν:

$2y$10$abc123xyz...

Αντέγραψε αυτό το hash και βάλτο στο config.php:

nano /var/www/website-builder/config.php
# Αντικατέστησε το ADMIN_PASSWORD_HASH με το νέο hash

Βήμα 12: Αυτόματη Εκκίνηση (Autostart)

Για το Apache (systemd — μέσα στο container):

# Ενεργοποίηση autostart Apache κατά την εκκίνηση του container
systemctl enable apache2

# Επαλήθευση
systemctl is-enabled apache2
# → enabled

Για το LXC Container (autostart κατά boot του PVE host):

Μέθοδος 1 — Από το PVE Web UI:

  • Πήγαινε στο container → OptionsStart at bootYes

Μέθοδος 2 — Από το PVE Shell:

# Στο PVE host (όχι μέσα στο container)
# Αντικατέστησε 100 με το CT ID σου
pct set 100 --onboot 1

# Επαλήθευση
pct config 100 | grep onboot
# → onboot: 1

Βήμα 13: Τελικός Έλεγχος

# Μέσα στο container - έλεγχος υπηρεσιών
systemctl status apache2
php -m | grep sqlite3

# Έλεγχος αρχείων
ls -la /var/www/website-builder/
ls -la /var/www/website-builder/data/

# Δες τα logs αν κάτι δεν πάει καλά
tail -f /var/log/apache2/website-builder-error.log

Βήμα 14: Ρύθμιση Caddy (στο PVE host ή άλλο container)

Αν το Caddy τρέχει στο PVE host ως reverse proxy:

yourdomain.com {
    reverse_proxy <container-ip>:80
    
    # Προστασία sensitive paths
    @blocked path /data/* /includes/* /config.php
    respond @blocked 403
    
    encode gzip
}

Αν το Caddy είναι μέσα στο ίδιο container:

apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy -y
systemctl enable caddy

✅ Σύνοψη Εντολών από την Αρχή

# Όλα μαζί — copy-paste σε ένα fresh Debian 13 LXC container
apt update && apt upgrade -y
apt install -y curl wget nano apache2 php8.4 libapache2-mod-php8.4 \
    php8.4-sqlite3 php8.4-mbstring php8.4-curl php8.4-xml php8.4-json
a2enmod rewrite
mkdir -p /var/www/website-builder/{admin,includes,assets,data}
chown -R www-data:www-data /var/www/website-builder
chmod 775 /var/www/website-builder/data
systemctl enable apache2
systemctl restart apache2

Μετά απλώς ανέβασε τα αρχεία, ενεργοποίησε το Virtual Host, και το site σου είναι έτοιμο στο http://<container-ip>/ και το admin panel στο http://<container-ip>/admin/.