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:
- Create CT → δώσε hostname (π.χ.
website-builder) - Template:
debian-13-standard(κατέβασέ το από Datacenter → Storage → CT Templates → Templates) - Disk: Τουλάχιστον 4GB
- RAM: 512MB minimum (1GB recommended)
- Network: Δώσε static IP ή DHCP
- 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 → Options → Start at boot →
Yes
Μέθοδος 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/.
No Comments