'Deutsch', 'en' => 'English', 'fr' => 'Français', 'es' => 'Español', 'it' => 'Italiano', 'pt' => 'Português', 'nl' => 'Nederlands', 'pl' => 'Polski', 'cs' => 'Čeština', 'da' => 'Dansk', 'sv' => 'Svenska', 'no' => 'Norsk', 'fi' => 'Suomi', 'hu' => 'Magyar', 'ro' => 'Română', 'hr' => 'Hrvatski', 'sk' => 'Slovenčina', 'sl' => 'Slovenščina', 'bg' => 'Български', 'el' => 'Ελληνικά', 'tr' => 'Türkçe', 'ru' => 'Русский', 'uk' => 'Українська', 'ja' => '日本語', 'zh' => '中文', 'ko' => '한국어', 'ar' => 'العربية', ]; $selectedSecondaryLang = $_POST['secondary_lang'] ?? 'de'; if ($selectedSecondaryLang !== '' && !isset($setupLanguages[$selectedSecondaryLang])) { $selectedSecondaryLang = 'de'; } /** * Generate starter site files after config is created. * Creates: language dirs, homepage + demo page templates, starter content, * nav-config, settings, news posts, .htaccess update. */ function generateStarterSite($primaryLang, $secondaryLang, $siteName, $setupLanguages) { require_once __DIR__ . '/starter-content.php'; $root = dirname(__DIR__); $languages = [$primaryLang]; if (!empty($secondaryLang)) $languages[] = $secondaryLang; $i18n = getStarterI18n($siteName); $contentDir = $root . '/content/pages'; if (!is_dir($contentDir)) @mkdir($contentDir, 0755, true); $newsDir = $root . '/content/news'; if (!is_dir($newsDir)) @mkdir($newsDir, 0755, true); // 1. Create language directories + page templates foreach ($languages as $lang) { $dir = $root . '/' . $lang; if (!is_dir($dir)) @mkdir($dir, 0755, true); $t = $i18n[$lang] ?? $i18n['en']; // Homepage template (renders sections from JSON) $homeTpl = <<<'PHPTPL'
PHPTPL; $homeTpl = str_replace(['{SITE_NAME}', '{LANG}'], [$siteName, $lang], $homeTpl); file_put_contents($dir . '/index.php', $homeTpl); // Components page template (custom layout with render functions) $compTpl = getComponentsTemplate($lang, $t['comp_title']); $compTpl = str_replace(['{TITLE}', '{LANG}'], [$t['comp_title'], $lang], $compTpl); file_put_contents($dir . '/components.php', $compTpl); // News listing template $newsTpl = getNewsTemplate($lang, $t['news_page_title'], $t['news_intro']); $newsTpl = str_replace(['{TITLE}', '{INTRO}', '{LANG}'], [$t['news_page_title'], $t['news_intro'], $lang], $newsTpl); file_put_contents($dir . '/news.php', $newsTpl); // News post detail template $newsPath = ($lang === $primaryLang) ? 'news' : $lang . '/news'; $newsPostTpl = getNewsPostTemplate($lang, $t['news_back']); $newsPostTpl = str_replace(['{LANG}', '{NEWS_PATH}', '{BACK_LABEL}'], [$lang, $newsPath, $t['news_back']], $newsPostTpl); file_put_contents($dir . '/news-post.php', $newsPostTpl); // 2. Create content JSON files // Homepage $homeContent = getHomeContent($lang, $siteName, $t); file_put_contents($contentDir . '/' . $lang . '_home.json', json_encode($homeContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // About page (JSON only — front controller serves it) $aboutContent = getAboutContent($lang, $t); file_put_contents($contentDir . '/' . $lang . '_about.json', json_encode($aboutContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Block types demo (JSON only — front controller serves it) $blocksContent = getBlocksContent($lang, $t); file_put_contents($contentDir . '/' . $lang . '_blocks.json', json_encode($blocksContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Components page (JSON data for the PHP template) $compContent = getComponentsContent($lang, $t); file_put_contents($contentDir . '/' . $lang . '_components.json', json_encode($compContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Contact page (JSON only — front controller serves it + includes contact form) $contactSlug = $lang === 'de' ? 'kontakt' : ($lang === 'es' ? 'contacto' : 'contact'); $contactTitle = $lang === 'de' ? 'Kontakt' : ($lang === 'es' ? 'Contacto' : 'Contact'); $contactHeadingText = $lang === 'de' ? 'Kontakt aufnehmen' : ($lang === 'es' ? 'Ponte en contacto' : 'Get in Touch'); $contactSubtitle = $lang === 'de' ? 'Wir freuen uns auf Ihre Nachricht' : ($lang === 'es' ? 'Nos encantaría saber de ti' : 'We\'d love to hear from you'); $contactIntro = $lang === 'de' ? '

Haben Sie eine Frage, Feedback oder möchten Sie einfach Hallo sagen? Füllen Sie das Formular aus und wir melden uns so schnell wie möglich bei Ihnen.

' : ($lang === 'es' ? '

¿Tienes una pregunta, comentario o simplemente quieres saludar? Rellena el formulario y te responderemos lo antes posible.

' : '

Have a question, feedback, or just want to say hello? Fill out the form below and we\'ll get back to you as soon as possible.

'); $contactContent = [ 'page' => $lang . '_' . $contactSlug, 'lang' => $lang, 'title' => $contactTitle, 'description' => $contactTitle, 'sections' => [ ['id' => 's1', 'type' => 'heading', 'text' => $contactHeadingText, 'level' => 'h1', 'subtitle' => $contactSubtitle], ['id' => 's2', 'type' => 'text', 'title' => '', 'content' => $contactIntro], ], ]; file_put_contents($contentDir . '/' . $lang . '_' . $contactSlug . '.json', json_encode($contactContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 3. Create demo news posts $posts = getNewsPosts($lang, $t); foreach ($posts as $post) { file_put_contents($newsDir . '/' . $post['filename'], json_encode($post['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } // 4. Create footer.json $footerData = [ 'page' => 'footer', 'lastModified' => null, 'tagline' => [], 'services' => [], 'claim' => [], 'contact' => ['phone' => '', 'email' => ''], 'credit' => ['text' => '', 'link' => '', 'linkText' => ''], 'contactHeading' => [], // The [id="adminAccess"] shortcode wraps the year in a that // includes/footer.php uses as the hidden admin-login double-click target. 'copyright' => '© [id="adminAccess"]' . date('Y') . '[/id] ' . htmlspecialchars($siteName), ]; foreach ($languages as $lang) { if ($lang === 'de') { $footerData['tagline'][$lang] = 'Ein leichtes Flat-File-CMS für editierbare Websites.'; $footerData['services'][$lang] = 'Inhalte inline bearbeiten, Seiten als JSON verwalten und individuelle Layouts sauber pflegen.'; $footerData['claim'][$lang] = 'Keine Datenbank. Kein schwerer Stack. Nur Inhalte, die Kunden selbst aktualisieren können.'; } elseif ($lang === 'es') { $footerData['tagline'][$lang] = 'Un CMS ligero de archivos planos para sitios web editables.'; $footerData['services'][$lang] = 'Edita contenido en línea, gestiona páginas como JSON y mantén diseños personalizados.'; $footerData['claim'][$lang] = 'Sin base de datos. Sin stack pesado. Solo contenido que tus clientes pueden actualizar.'; } else { $footerData['tagline'][$lang] = 'A lightweight flat-file CMS for editable websites.'; $footerData['services'][$lang] = 'Edit content inline, manage pages as JSON, and keep custom layouts maintainable.'; $footerData['claim'][$lang] = 'No database. No heavy stack. Just content your clients can update.'; } $footerData['contactHeading'][$lang] = $lang === 'de' ? 'Kontakt' : ($lang === 'es' ? 'Contacto' : 'Contact'); } file_put_contents($root . '/content/pages/footer.json', json_encode($footerData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 4b. Create menus.json (menu registry) $menusData = ['menus' => [ 'header' => ['label' => [], 'type' => 'standard', 'weight' => 0], 'footer-pages' => ['label' => [], 'weight' => 10], 'footer-legal' => ['label' => [], 'weight' => 20], ]]; foreach ($languages as $lang) { $menusData['menus']['header']['label'][$lang] = $lang === 'de' ? 'Kopfzeile' : ($lang === 'es' ? 'Encabezado' : 'Header'); $menusData['menus']['footer-pages']['label'][$lang] = $lang === 'de' ? 'Seiten' : ($lang === 'es' ? 'Páginas' : 'Pages'); $menusData['menus']['footer-legal']['label'][$lang] = $lang === 'de' ? 'Rechtliches' : 'Info'; } file_put_contents($root . '/content/menus.json', json_encode($menusData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 5. Create nav-config.php (includes all demo pages) file_put_contents($root . '/includes/nav-config.php', getNavConfig($languages, $primaryLang, $setupLanguages)); // 6. Create settings.json $settings = [ 'favicon' => '/assets/images/favicon.svg', 'branding' => [ 'logo' => '', 'name' => $siteName, 'showBranding' => true, 'logoDisplay' => 'both', ], 'theme' => [ 'adminTheme' => 'light', 'primaryColor' => '#3858e9', 'accentColor' => '#3858e9', ], 'email' => [ 'method' => 'inactive', 'recipientEmail' => '', 'bccEmail' => '', 'fromEmail' => '', 'fromName' => '', 'smtpHost' => '', 'smtpPort' => 587, 'smtpUsername' => '', 'smtpPassword' => '', 'smtpEncryption' => 'tls', ], ]; file_put_contents($root . '/content/settings.json', json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 7. Create starter events $starterEvents = getStarterEvents($languages); file_put_contents($root . '/content/events.json', json_encode($starterEvents, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 8. Create demo contact messages $starterMails = getStarterMails(); file_put_contents($root . '/content/mails.json', json_encode($starterMails, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 9. Update .htaccess primary language if not 'en' $htaccessPath = $root . '/.htaccess'; if ($primaryLang !== 'en' && file_exists($htaccessPath)) { $htaccess = file_get_contents($htaccessPath); $htaccess = str_replace( ['%{DOCUMENT_ROOT}/en/%1.php', 'en/$1.php [L]'], ['%{DOCUMENT_ROOT}/' . $primaryLang . '/%1.php', $primaryLang . '/$1.php [L]'], $htaccess ); file_put_contents($htaccessPath, $htaccess); } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $siteName = trim($_POST['site_name'] ?? ''); $username = trim($_POST['username'] ?? ''); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; $passwordConfirm = $_POST['password_confirm'] ?? ''; $primaryLang = $_POST['primary_lang'] ?? 'en'; $secondaryLang = $_POST['secondary_lang'] ?? ''; // Validate language selections if (!isset($setupLanguages[$primaryLang])) { $primaryLang = 'en'; } if (!empty($secondaryLang) && !isset($setupLanguages[$secondaryLang])) { $secondaryLang = ''; } if ($secondaryLang === $primaryLang) { $secondaryLang = ''; } // Validation if (!is_string($_POST['csrf_token'] ?? null) || !hash_equals($_SESSION['setup_csrf'], $_POST['csrf_token'])) { $error = t('api.csrf'); } elseif (empty($siteName)) { $error = t('setup.error_site_name'); } elseif (empty($username)) { $error = t('setup.error_username'); } elseif (strlen($username) < 3) { $error = t('setup.error_username_short'); } elseif (empty($password)) { $error = t('setup.error_password'); } elseif ($password !== $passwordConfirm) { $error = t('setup.error_password_mismatch'); } elseif (isPasswordWeak($password)) { $error = t('setup.error_password_weak'); } else { // Generate hash and write config $hash = password_hash($password, PASSWORD_DEFAULT); $escapedSiteName = addcslashes($siteName, "'\\"); // Build $SITE_LANGUAGES array $languages = [$primaryLang => $setupLanguages[$primaryLang]]; if (!empty($secondaryLang)) { $languages[$secondaryLang] = $setupLanguages[$secondaryLang]; } // Build languages PHP code $langLines = []; foreach ($languages as $code => $name) { $langLines[] = " '" . $code . "' => '" . addcslashes($name, "'\\") . "'"; } $langArrayCode = implode(",\n", $langLines); $configContent = << "favicon". define('RAIJU_DEFAULT_FAVICON', '/assets/images/favicon.svg'); // ============================================================ // SITE SETTINGS // ============================================================ define('SITE_NAME', '{$escapedSiteName}'); // Development login. Existing admin users can log in with password "dev" only // from loopback hosts (localhost, 127.0.0.1, ::1). This is ignored on non-local // hosts even if accidentally left enabled. Set false to disable it locally too. define('RAIJU_DEV_LOGIN', true); // ============================================================ // LANGUAGES // ============================================================ // Primary language: pages live at root (no URL prefix). // Additional languages: pages live under /{code}/ (e.g. /en/). define('SITE_LANG_DEFAULT', '{$primaryLang}'); \$SITE_LANGUAGES = [ {$langArrayCode}, ]; // ============================================================ // PATHS (relative to webroot) // ============================================================ define('CONTENT_PATH', __DIR__ . '/../content/pages/'); define('PAGES_TRASH_PATH', __DIR__ . '/../content/pages-trash/'); define('BACKUP_PATH', __DIR__ . '/../backups/'); define('EVENTS_PATH', __DIR__ . '/../content/events.json'); define('IMAGES_PATH', __DIR__ . '/../assets/images/'); define('IMAGES_TRASH_PATH', __DIR__ . '/../assets/images-trash/'); define('AUDIO_PATH', __DIR__ . '/../assets/audio/'); define('AUDIO_TRASH_PATH', __DIR__ . '/../assets/audio-trash/'); define('VIDEO_PATH', __DIR__ . '/../assets/videos/'); define('VIDEO_TRASH_PATH', __DIR__ . '/../assets/videos-trash/'); define('DOCUMENTS_PATH', __DIR__ . '/../assets/documents/'); define('DOCUMENTS_TRASH_PATH', __DIR__ . '/../assets/documents-trash/'); define('SETTINGS_PATH', __DIR__ . '/../content/settings.json'); define('USERS_PATH', __DIR__ . '/../content/users.json'); // ============================================================ // LIMITS // ============================================================ define('MAX_BACKUPS', 30); define('SESSION_LIFETIME', 3600); // 1 hour CONFIGTPL; $result = file_put_contents(__DIR__ . '/config.php', $configContent, LOCK_EX); if ($result === false) { $error = t('setup.error_write'); } else { // Create users.json with the first admin user require_once __DIR__ . '/users.php'; $usersData = [ 'users' => [ [ 'id' => 'u_' . bin2hex(random_bytes(5)), 'username' => $username, 'email' => $email, 'role' => 'admin', 'passwordHash' => $hash, 'createdAt' => gmdate('c'), 'createdBy' => 'setup', 'lastLogin' => null, 'resetToken' => null, 'resetTokenExpiry' => null, ] ] ]; saveUsers($usersData); // Generate starter site files generateStarterSite($primaryLang, $secondaryLang, $siteName, $setupLanguages); $success = true; } } } ?> <?php echo t('setup.page_title'); ?>

$selectedSecondaryLang]) : ''; ?>