-
-
diff --git a/db/config.php b/db/config.php
index 7644cf3..f135f1f 100644
--- a/db/config.php
+++ b/db/config.php
@@ -1,247 +1,21 @@
$host !== '' ? $host : 'localhost',
- 'DB_PORT' => $port !== '' ? $port : '3306',
- 'DB_NAME' => $name,
- 'DB_USER' => $user,
- 'DB_PASS' => $password,
- ];
+if (DB_PASS === '') {
+ throw new RuntimeException('DB_PASS environment variable is not set.');
}
-function db_env_settings(): array
-{
- $settings = [];
-
- foreach (['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASS'] as $key) {
- $value = getenv($key);
- if ($value !== false) {
- $settings[$key] = $value;
- }
- }
-
- return db_normalize_settings($settings);
-}
-
-function db_local_settings(): array
-{
- static $settings = null;
-
- if (is_array($settings)) {
- return $settings;
- }
-
- if (!is_file(DB_INSTALLER_CONFIG_FILE)) {
- $settings = db_normalize_settings([]);
- return $settings;
- }
-
- $loaded = include DB_INSTALLER_CONFIG_FILE;
- $settings = is_array($loaded) ? db_normalize_settings($loaded) : db_normalize_settings([]);
-
- return $settings;
-}
-
-function db_settings_complete(array $settings): bool
-{
- return $settings['DB_HOST'] !== ''
- && $settings['DB_NAME'] !== ''
- && $settings['DB_USER'] !== '';
-}
-
-function db_resolved_settings(): array
-{
- static $settings = null;
-
- if (is_array($settings)) {
- return $settings;
- }
-
- $env = db_env_settings();
- $local = db_local_settings();
- $envComplete = db_settings_complete($env);
- $localComplete = db_settings_complete($local);
-
- if ($envComplete && $localComplete) {
- $error = null;
- if (db_test_connection($env, true, $error)) {
- $env['__source'] = 'env';
- $settings = $env;
- return $settings;
- }
-
- $local['__source'] = 'file';
- $settings = $local;
- return $settings;
- }
-
- if ($envComplete) {
- $env['__source'] = 'env';
- $settings = $env;
- return $settings;
- }
-
- if ($localComplete) {
- $local['__source'] = 'file';
- $settings = $local;
- return $settings;
- }
-
- $settings = db_normalize_settings([]);
- $settings['__source'] = 'missing';
-
- return $settings;
-}
-
-$resolvedDbSettings = db_resolved_settings();
-define('DB_HOST', $resolvedDbSettings['DB_HOST']);
-define('DB_PORT', $resolvedDbSettings['DB_PORT']);
-define('DB_NAME', $resolvedDbSettings['DB_NAME']);
-define('DB_USER', $resolvedDbSettings['DB_USER']);
-define('DB_PASS', $resolvedDbSettings['DB_PASS']);
-unset($resolvedDbSettings);
-
-function db_config_source(): string
-{
- return db_resolved_settings()['__source'] ?? 'missing';
-}
-
-function db_has_required_config(): bool
-{
- return db_settings_complete(db_resolved_settings());
-}
-
-function db_build_dsn(array $settings, bool $withDatabase = true): string
-{
- $settings = db_normalize_settings($settings);
- $dsn = 'mysql:host=' . $settings['DB_HOST'] . ';port=' . $settings['DB_PORT'] . ';charset=utf8mb4';
-
- if ($withDatabase && $settings['DB_NAME'] !== '') {
- $dsn .= ';dbname=' . $settings['DB_NAME'];
- }
-
- return $dsn;
-}
-
-function db_create_pdo(array $settings, bool $withDatabase = true): PDO
-{
- $settings = db_normalize_settings($settings);
-
- return new PDO(
- db_build_dsn($settings, $withDatabase),
- $settings['DB_USER'],
- $settings['DB_PASS'],
- [
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- PDO::ATTR_TIMEOUT => 5,
- ]
- );
-}
-
-function db_test_connection(array $settings, bool $withDatabase = true, ?string &$error = null): bool
-{
- try {
- db_create_pdo($settings, $withDatabase)->query('SELECT 1');
- $error = null;
- return true;
- } catch (Throwable $exception) {
- $error = $exception->getMessage();
- return false;
- }
-}
-
-function db_connection_error(?array $settings = null): ?string
-{
- $settings = $settings !== null ? db_normalize_settings($settings) : db_resolved_settings();
-
- if (!db_settings_complete($settings)) {
- return 'Database belum dikonfigurasi lengkap.';
- }
-
- $error = null;
- db_test_connection($settings, true, $error);
-
- return $error;
-}
-
-function db(): PDO
-{
- static $pdo = null;
-
- if ($pdo instanceof PDO) {
- return $pdo;
- }
-
- if (!db_has_required_config()) {
- throw new RuntimeException('Database belum dikonfigurasi.');
- }
-
- $pdo = db_create_pdo(db_resolved_settings(), true);
- return $pdo;
-}
-
-function db_error_is_missing_database(string $message): bool
-{
- return stripos($message, 'Unknown database') !== false
- || str_contains($message, '[1049]');
-}
-
-function db_quote_identifier(string $identifier): string
-{
- $identifier = trim($identifier);
- if ($identifier === '' || preg_match('/[[:cntrl:]]/', $identifier)) {
- throw new RuntimeException('Nama database tidak valid.');
- }
-
- return '`' . str_replace('`', '``', $identifier) . '`';
-}
-
-function db_create_database_if_missing(array $settings): void
-{
- $settings = db_normalize_settings($settings);
-
- if ($settings['DB_NAME'] === '') {
- throw new RuntimeException('Nama database wajib diisi.');
- }
-
- $pdo = db_create_pdo($settings, false);
- $pdo->exec(
- 'CREATE DATABASE IF NOT EXISTS ' . db_quote_identifier($settings['DB_NAME']) .
- ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'
- );
-}
-
-function db_save_local_settings(array $settings): void
-{
- $settings = db_normalize_settings($settings);
- $payload = " $settings['DB_HOST'],
- 'DB_PORT' => $settings['DB_PORT'],
- 'DB_NAME' => $settings['DB_NAME'],
- 'DB_USER' => $settings['DB_USER'],
- 'DB_PASS' => $settings['DB_PASS'],
- ], true) . ";
-";
-
- $bytes = file_put_contents(DB_INSTALLER_CONFIG_FILE, $payload, LOCK_EX);
- if ($bytes === false) {
- throw new RuntimeException('Gagal menyimpan file konfigurasi database. Pastikan folder db bisa ditulis.');
- }
+function db() {
+ static $pdo;
+ if (!$pdo) {
+ $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ ]);
+ }
+ return $pdo;
}
diff --git a/db/database.sql b/db/database.sql
deleted file mode 100644
index 220bd9f..0000000
--- a/db/database.sql
+++ /dev/null
@@ -1,58 +0,0 @@
--- database.sql
--- Import file ini ke database yang sudah Anda buat di hosting / phpMyAdmin.
--- Tidak memakai CREATE DATABASE supaya bisa dipakai untuk nama database apa pun.
--- Setelah import, installer tinggal diisi sesuai host, port, nama database, username, dan password.
-
-SET NAMES utf8mb4;
-SET time_zone = '+00:00';
-
-CREATE TABLE IF NOT EXISTS backlink_posts (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- title VARCHAR(180) NOT NULL,
- slug VARCHAR(190) NOT NULL,
- category VARCHAR(80) NOT NULL DEFAULT 'Artikel',
- excerpt TEXT NOT NULL,
- content MEDIUMTEXT NOT NULL,
- cta_text VARCHAR(120) NOT NULL DEFAULT 'Kunjungi apknusa.com',
- cta_url VARCHAR(255) NOT NULL DEFAULT 'https://apknusa.com',
- featured TINYINT(1) NOT NULL DEFAULT 0,
- published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- UNIQUE KEY uniq_backlink_posts_slug (slug),
- KEY idx_backlink_posts_category (category),
- KEY idx_backlink_posts_published_at (published_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS faqs (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- keywords VARCHAR(255) NOT NULL,
- answer TEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- KEY idx_faqs_keywords (keywords)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS messages (
- id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
- user_message TEXT NOT NULL,
- ai_response MEDIUMTEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS settings (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- setting_key VARCHAR(120) NOT NULL,
- setting_value TEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- UNIQUE KEY uniq_settings_key (setting_key)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
--- Catatan:
--- 1) Isi tabel backlink_posts boleh dibiarkan kosong. Aplikasi akan mengisi contoh artikel default saat pertama dibuka.
--- 2) Tabel faqs, messages, dan settings dipakai endpoint chat / Telegram jika nanti Anda aktifkan fitur itu.
diff --git a/db/migrations/20260517_create_backlink_posts.sql b/db/migrations/20260517_create_backlink_posts.sql
deleted file mode 100644
index 1c3be6f..0000000
--- a/db/migrations/20260517_create_backlink_posts.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE IF NOT EXISTS backlink_posts (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- title VARCHAR(180) NOT NULL,
- slug VARCHAR(190) NOT NULL,
- category VARCHAR(80) NOT NULL DEFAULT 'Artikel',
- excerpt TEXT NOT NULL,
- content MEDIUMTEXT NOT NULL,
- cta_text VARCHAR(120) NOT NULL DEFAULT 'Kunjungi apknusa.com',
- cta_url VARCHAR(255) NOT NULL DEFAULT 'https://apknusa.com',
- featured TINYINT(1) NOT NULL DEFAULT 0,
- published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- UNIQUE KEY uniq_backlink_posts_slug (slug),
- KEY idx_backlink_posts_category (category),
- KEY idx_backlink_posts_published_at (published_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/db/migrations/20260517_create_support_tables.sql b/db/migrations/20260517_create_support_tables.sql
deleted file mode 100644
index e3b1f15..0000000
--- a/db/migrations/20260517_create_support_tables.sql
+++ /dev/null
@@ -1,27 +0,0 @@
-CREATE TABLE IF NOT EXISTS faqs (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- keywords VARCHAR(255) NOT NULL,
- answer TEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- KEY idx_faqs_keywords (keywords)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS messages (
- id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
- user_message TEXT NOT NULL,
- ai_response MEDIUMTEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS settings (
- id INT UNSIGNED NOT NULL AUTO_INCREMENT,
- setting_key VARCHAR(120) NOT NULL,
- setting_value TEXT NOT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- UNIQUE KEY uniq_settings_key (setting_key)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/healthz.php b/healthz.php
deleted file mode 100644
index fe27cdb..0000000
--- a/healthz.php
+++ /dev/null
@@ -1,41 +0,0 @@
- 'setup_required',
- 'php_version' => PHP_VERSION,
- 'time_utc' => gmdate('c'),
- 'db' => 'setup_required',
- 'message' => $status['message'],
- ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- exit;
-}
-
-try {
- init_blog_storage();
- $count = post_count();
-
- http_response_code(200);
- echo json_encode([
- 'status' => 'ok',
- 'php_version' => PHP_VERSION,
- 'time_utc' => gmdate('c'),
- 'posts' => $count,
- 'db' => 'ok',
- ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
-} catch (Throwable $exception) {
- http_response_code(503);
- echo json_encode([
- 'status' => 'error',
- 'php_version' => PHP_VERSION,
- 'time_utc' => gmdate('c'),
- 'db' => 'error',
- ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
-}
diff --git a/includes/layout.php b/includes/layout.php
deleted file mode 100644
index 03884b1..0000000
--- a/includes/layout.php
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
- = e($pageTitle) ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Lewati ke konten utama
-
-
-
-
-
-
-
-
-
- false,
- 'source' => db_config_source(),
- 'message' => '',
- 'details' => '',
- ];
-
- if (!db_has_required_config()) {
- $status['message'] = 'Koneksi database belum diatur.';
- $status['details'] = 'Masukkan host, port, nama database, username, dan password di installer.';
- return $status;
- }
-
- $error = db_connection_error();
- if ($error !== null) {
- $status['message'] = 'Aplikasi belum bisa terhubung ke database.';
- $status['details'] = $error;
- return $status;
- }
-
- $status['ready'] = true;
- $status['message'] = 'Koneksi database siap.';
-
- return $status;
-}
-
-function ensure_site_installed(): void
-{
- if (current_script_name() === 'install.php') {
- return;
- }
-
- $status = site_installation_status();
- if ($status['ready']) {
- return;
- }
-
- $nextPath = safe_install_next_path((string)($_SERVER['REQUEST_URI'] ?? '/'));
- header('Location: install.php?next=' . rawurlencode($nextPath));
- exit;
-}
-
-function post_url(array $post): string
-{
- return 'post.php?slug=' . rawurlencode((string)$post['slug']);
-}
-
-function word_count_indonesian(string $text): int
-{
- if (preg_match_all('/[\p{L}\p{N}]+/u', strip_tags($text), $matches) !== false) {
- return count($matches[0]);
- }
-
- return 0;
-}
-
-function reading_time_minutes(string $text): int
-{
- return max(1, (int)ceil(word_count_indonesian($text) / 180));
-}
-
-function excerpt_text(string $text, int $limit = 160): string
-{
- $plain = trim(preg_replace('/\s+/u', ' ', strip_tags($text)) ?? '');
- if (text_length($plain) <= $limit) {
- return $plain;
- }
-
- return rtrim(text_substr($plain, 0, $limit - 1)) . '…';
-}
-
-function slugify(string $text): string
-{
- $text = trim(text_lower($text));
- $text = preg_replace('/[^\p{L}\p{N}]+/u', '-', $text) ?? '';
- $text = trim($text, '-');
-
- return $text !== '' ? $text : 'artikel';
-}
-
-function allowed_apknusa_hosts(): array
-{
- return ['apknusa.com', 'www.apknusa.com'];
-}
-
-function validate_apknusa_url(string $url): bool
-{
- if (filter_var($url, FILTER_VALIDATE_URL) === false) {
- return false;
- }
-
- $host = strtolower((string)parse_url($url, PHP_URL_HOST));
- $scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
-
- return in_array($host, allowed_apknusa_hosts(), true) && in_array($scheme, ['http', 'https'], true);
-}
-
-function init_blog_storage(): void
-{
- static $initialized = false;
-
- if ($initialized) {
- return;
- }
-
- $migrationFiles = glob(project_root_path('db/migrations/*.sql'));
- if ($migrationFiles === false || $migrationFiles === []) {
- throw new RuntimeException('Database migration files not found.');
- }
-
- sort($migrationFiles, SORT_STRING);
-
- foreach ($migrationFiles as $migrationFile) {
- $sql = file_get_contents($migrationFile);
- if ($sql === false || trim($sql) === '') {
- throw new RuntimeException('Database migration file is empty: ' . basename($migrationFile));
- }
-
- db()->exec($sql);
- }
-
- $initialized = true;
- seed_default_posts();
-}
-
-function boot_site(): void
-{
- ensure_site_installed();
- init_blog_storage();
- refresh_seo_artifacts();
-}
-
-function unique_slug(string $title): string
-{
- init_blog_storage();
-
- $baseSlug = slugify($title);
- $slug = $baseSlug;
- $suffix = 2;
-
- $statement = db()->prepare('SELECT COUNT(*) FROM backlink_posts WHERE slug = :slug');
-
- while (true) {
- $statement->bindValue(':slug', $slug);
- $statement->execute();
- if ((int)$statement->fetchColumn() === 0) {
- return $slug;
- }
-
- $slug = $baseSlug . '-' . $suffix;
- $suffix++;
- }
-}
-
-function seed_default_posts(): void
-{
- $count = (int)db()->query('SELECT COUNT(*) FROM backlink_posts')->fetchColumn();
- if ($count > 0) {
- return;
- }
-
- $posts = [
- [
- 'title' => 'Cara Memilih Sumber APK yang Aman Sebelum Instal',
- 'category' => 'Keamanan APK',
- 'excerpt' => 'Checklist sederhana untuk menilai file APK, versi, izin aplikasi, dan reputasi sumber sebelum dipasang di Android.',
- 'content' => "Saat mencari file APK, hal pertama yang perlu diperiksa adalah reputasi sumber informasinya. Pengunjung biasanya ingin jawaban cepat, tetapi keputusan instalasi tetap perlu didukung informasi versi, pembaruan terakhir, dan ringkasan fungsi aplikasi. Artikel yang rapi akan membantu pembaca merasa aman sebelum melanjutkan.\n\nLangkah berikutnya adalah membandingkan izin aplikasi dengan fungsi utamanya. Jika aplikasi sederhana meminta akses yang berlebihan, pembaca perlu waspada. Konten seperti ini biasanya punya performa SEO yang baik karena menjawab pertanyaan praktis yang sering dicari pengguna Android.\n\nWebsite pendukung seperti microsite ini bisa menjadi titik awal untuk edukasi singkat. Setelah pembaca memahami dasar keamanannya, mereka bisa diarahkan ke sumber utama untuk melihat referensi yang lebih lengkap. Itu sebabnya tombol ajakan bertindak di halaman ini selalu mengarah ke apknusa.com.\n\nUntuk referensi yang lebih lengkap tentang aplikasi Android, pembaruan, dan tips penggunaan, lanjutkan ke apknusa.com agar pembaca mendapat halaman utama yang menjadi tujuan akhir.",
- 'cta_text' => 'Lihat referensi lengkap di apknusa.com',
- 'cta_url' => APKNUSA_URL,
- 'featured' => 1,
- 'published_at' => date('Y-m-d H:i:s', strtotime('-18 days')),
- ],
- [
- 'title' => 'Panduan Update Aplikasi Android Tanpa Kehilangan Data',
- 'category' => 'Update Android',
- 'excerpt' => 'Urutan aman saat memperbarui aplikasi Android supaya data tetap aman dan prosesnya tidak membingungkan.',
- 'content' => "Banyak pengguna menunda update aplikasi karena khawatir data mereka hilang. Padahal, dengan langkah yang benar, pembaruan bisa dilakukan tanpa membuat pengalaman penggunaan terganggu. Konten edukatif seperti ini sangat cocok untuk menarik trafik organik dari pencarian yang berniat tinggi.\n\nSebelum update, pembaca perlu memastikan ruang penyimpanan cukup dan mengetahui versi aplikasi yang sedang digunakan. Setelah itu, mereka sebaiknya membaca catatan perubahan agar tahu apakah pembaruan membawa perbaikan keamanan, fitur baru, atau sekadar penyesuaian ringan.\n\nMicrosite pendukung berguna untuk menjelaskan proses tersebut secara singkat, lalu memberi jalur yang jelas menuju website utama. Model seperti ini membuat konten tetap fokus, cepat dimuat, dan memiliki tujuan konversi yang tegas.\n\nJika ingin melihat referensi aplikasi Android yang lebih lengkap dan pembahasan lanjutan, pengunjung bisa langsung menuju apknusa.com dari tombol CTA di bawah artikel ini.",
- 'cta_text' => 'Buka panduan lanjutan di apknusa.com',
- 'cta_url' => APKNUSA_URL,
- 'featured' => 1,
- 'published_at' => date('Y-m-d H:i:s', strtotime('-14 days')),
- ],
- [
- 'title' => 'Kenapa Review Aplikasi Penting Sebelum Download',
- 'category' => 'Review Aplikasi',
- 'excerpt' => 'Review singkat membantu pembaca menilai kualitas aplikasi, manfaat, dan potensi risiko sebelum mengunduh.',
- 'content' => "Sebelum pengguna menekan tombol download, mereka biasanya ingin tahu satu hal: apakah aplikasi ini memang layak dipakai. Di situlah peran review aplikasi menjadi penting. Review yang ringkas tetapi informatif membantu pembaca memahami fungsi utama, kelebihan, dan keterbatasan aplikasi.\n\nDari sisi SEO, artikel review punya peluang baik karena menjawab kebutuhan pencarian yang spesifik. Orang yang mengetik nama aplikasi biasanya sedang berada di tahap evaluasi. Jika artikel bisa membantu mereka mengambil keputusan, peluang klik ke website utama akan jauh lebih besar.\n\nMicrosite backlink sebaiknya tidak mencoba menampung semua informasi sekaligus. Tugasnya adalah membuka konteks, memberi jawaban awal, lalu membawa pembaca ke sumber utama yang lebih lengkap dan lebih kuat secara brand. Struktur seperti ini terasa alami bagi pengunjung dan jelas bagi mesin pencari.\n\nUntuk melihat referensi lain seputar aplikasi Android dan sumber informasi yang lebih lengkap, arahkan pembaca ke apknusa.com sebagai tujuan utama perjalanan mereka.",
- 'cta_text' => 'Lanjut ke apknusa.com',
- 'cta_url' => APKNUSA_URL,
- 'featured' => 1,
- 'published_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
- ],
- [
- 'title' => 'Tips Menyimpan File APK Supaya Mudah Dicari Lagi',
- 'category' => 'Tips Android',
- 'excerpt' => 'Strategi folder, penamaan file, dan kebiasaan kecil agar file APK tetap rapi dan mudah ditemukan saat dibutuhkan.',
- 'content' => "File APK sering tersimpan di berbagai folder unduhan sehingga mudah terlupakan. Dengan membuat struktur folder yang jelas, pengguna bisa lebih cepat menemukan file lama saat perlu memasang ulang aplikasi atau membandingkan versi tertentu.\n\nGunakan nama file yang konsisten, misalnya menggabungkan nama aplikasi dan versi. Kebiasaan kecil seperti ini terlihat sederhana, tetapi sangat membantu saat jumlah file unduhan mulai bertambah. Artikel semacam ini cocok untuk blog pendukung karena informatif dan mudah dibaca.\n\nSetelah pembaca merasa terbantu, langkah berikutnya adalah membawa mereka ke halaman utama yang menyediakan referensi lebih luas. CTA yang jelas membuat alur pengguna lebih halus dan membantu microsite menjalankan fungsi backlink dengan baik.\n\nUntuk informasi tambahan seputar aplikasi Android dan referensi lain yang lebih lengkap, arahkan pembaca ke apknusa.com agar mereka bisa melanjutkan eksplorasi dari sumber utama.",
- 'cta_text' => 'Kunjungi apknusa.com untuk info lain',
- 'cta_url' => APKNUSA_URL,
- 'featured' => 0,
- 'published_at' => date('Y-m-d H:i:s', strtotime('-6 days')),
- ],
- [
- 'title' => 'Perbedaan APK, XAPK, dan File Instalasi Lain yang Perlu Dipahami',
- 'category' => 'Panduan Instalasi',
- 'excerpt' => 'Penjelasan singkat tentang format file instalasi Android agar pembaca tahu apa yang mereka buka dan instal.',
- 'content' => "Bagi banyak pengguna, semua file instalasi Android terlihat mirip. Padahal, format seperti APK dan XAPK memiliki perbedaan cara penggunaan dan kebutuhan file pendukung. Menjelaskan perbedaan ini secara ringkas membantu pembaca memahami konteks sebelum melakukan tindakan.\n\nKonten edukatif yang sederhana sering kali efektif sebagai pintu masuk trafik organik. Pengunjung datang karena pertanyaan dasar, lalu diarahkan secara elegan ke sumber utama untuk membaca informasi yang lebih luas dan lebih terstruktur.\n\nMicrosite backlink yang baik tidak terasa seperti halaman iklan. Ia terasa seperti halaman bantuan yang memang menjawab kebutuhan awal pengguna. Dari situ, tautan ke website utama terasa relevan, bukan dipaksakan.\n\nJika pembaca ingin melihat pembahasan lanjutan seputar aplikasi Android, format file, dan referensi lain, tombol CTA di bawah akan membawa mereka ke apknusa.com.",
- 'cta_text' => 'Baca referensi utama di apknusa.com',
- 'cta_url' => APKNUSA_URL,
- 'featured' => 0,
- 'published_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
- ],
- ];
-
- $statement = db()->prepare(
- 'INSERT INTO backlink_posts (title, slug, category, excerpt, content, cta_text, cta_url, featured, published_at)
- VALUES (:title, :slug, :category, :excerpt, :content, :cta_text, :cta_url, :featured, :published_at)'
- );
-
- foreach ($posts as $post) {
- $statement->execute([
- ':title' => $post['title'],
- ':slug' => unique_slug($post['title']),
- ':category' => $post['category'],
- ':excerpt' => $post['excerpt'],
- ':content' => $post['content'],
- ':cta_text' => $post['cta_text'],
- ':cta_url' => $post['cta_url'],
- ':featured' => $post['featured'],
- ':published_at' => $post['published_at'],
- ]);
- }
-}
-
-function post_count(): int
-{
- init_blog_storage();
- return (int)db()->query('SELECT COUNT(*) FROM backlink_posts')->fetchColumn();
-}
-
-function fetch_categories(): array
-{
- init_blog_storage();
-
- $rows = db()->query('SELECT DISTINCT category FROM backlink_posts ORDER BY category ASC')->fetchAll();
- return array_values(array_filter(array_map(static fn(array $row): string => (string)$row['category'], $rows)));
-}
-
-function fetch_posts(?string $category = null, int $limit = 0): array
-{
- init_blog_storage();
-
- $sql = 'SELECT * FROM backlink_posts';
- $params = [];
-
- if ($category !== null && $category !== '') {
- $sql .= ' WHERE category = :category';
- $params[':category'] = $category;
- }
-
- $sql .= ' ORDER BY published_at DESC, id DESC';
-
- if ($limit > 0) {
- $sql .= ' LIMIT ' . (int)$limit;
- }
-
- $statement = db()->prepare($sql);
- $statement->execute($params);
-
- return $statement->fetchAll();
-}
-
-function fetch_featured_posts(int $limit = 3): array
-{
- init_blog_storage();
-
- $statement = db()->prepare(
- 'SELECT * FROM backlink_posts WHERE featured = 1 ORDER BY published_at DESC, id DESC LIMIT ' . (int)$limit
- );
- $statement->execute();
- $posts = $statement->fetchAll();
-
- if ($posts !== []) {
- return $posts;
- }
-
- return fetch_posts(null, $limit);
-}
-
-function fetch_post_by_slug(string $slug): ?array
-{
- init_blog_storage();
-
- $statement = db()->prepare('SELECT * FROM backlink_posts WHERE slug = :slug LIMIT 1');
- $statement->execute([':slug' => $slug]);
- $post = $statement->fetch();
-
- return $post !== false ? $post : null;
-}
-
-function fetch_related_posts(int $excludeId, string $category, int $limit = 3): array
-{
- init_blog_storage();
-
- $statement = db()->prepare(
- 'SELECT * FROM backlink_posts
- WHERE id <> :id AND category = :category
- ORDER BY published_at DESC, id DESC
- LIMIT ' . (int)$limit
- );
- $statement->execute([
- ':id' => $excludeId,
- ':category' => $category,
- ]);
-
- $posts = $statement->fetchAll();
- if (count($posts) >= $limit) {
- return $posts;
- }
-
- $fallback = db()->prepare(
- 'SELECT * FROM backlink_posts
- WHERE id <> :id
- ORDER BY published_at DESC, id DESC
- LIMIT ' . (int)$limit
- );
- $fallback->execute([':id' => $excludeId]);
-
- return $fallback->fetchAll();
-}
-
-function create_post(array $input): array
-{
- init_blog_storage();
-
- $data = [
- 'title' => trim((string)($input['title'] ?? '')),
- 'category' => trim((string)($input['category'] ?? '')),
- 'excerpt' => trim((string)($input['excerpt'] ?? '')),
- 'content' => trim((string)($input['content'] ?? '')),
- 'cta_text' => trim((string)($input['cta_text'] ?? '')),
- 'cta_url' => trim((string)($input['cta_url'] ?? APKNUSA_URL)),
- 'featured' => !empty($input['featured']) ? 1 : 0,
- ];
-
- if ($data['category'] === '') {
- $data['category'] = 'Artikel';
- }
- if ($data['cta_text'] === '') {
- $data['cta_text'] = 'Kunjungi apknusa.com';
- }
- if ($data['cta_url'] === '') {
- $data['cta_url'] = APKNUSA_URL;
- }
-
- $errors = [];
-
- if (text_length($data['title']) < 8) {
- $errors['title'] = 'Judul minimal 8 karakter.';
- }
- if (text_length($data['excerpt']) < 24) {
- $errors['excerpt'] = 'Ringkasan minimal 24 karakter.';
- }
- if (text_length($data['content']) < 140) {
- $errors['content'] = 'Konten minimal 140 karakter supaya artikel terasa lengkap.';
- }
- if (text_length($data['cta_text']) < 4) {
- $errors['cta_text'] = 'CTA minimal 4 karakter.';
- }
- if (!validate_apknusa_url($data['cta_url'])) {
- $errors['cta_url'] = 'Tautan CTA harus mengarah ke apknusa.com.';
- }
-
- if ($errors !== []) {
- return [
- 'success' => false,
- 'errors' => $errors,
- 'input' => $data,
- ];
- }
-
- $slug = unique_slug($data['title']);
-
- $statement = db()->prepare(
- 'INSERT INTO backlink_posts (title, slug, category, excerpt, content, cta_text, cta_url, featured, published_at)
- VALUES (:title, :slug, :category, :excerpt, :content, :cta_text, :cta_url, :featured, NOW())'
- );
-
- $statement->execute([
- ':title' => $data['title'],
- ':slug' => $slug,
- ':category' => $data['category'],
- ':excerpt' => $data['excerpt'],
- ':content' => $data['content'],
- ':cta_text' => $data['cta_text'],
- ':cta_url' => $data['cta_url'],
- ':featured' => $data['featured'],
- ]);
-
- refresh_seo_artifacts();
-
- return [
- 'success' => true,
- 'slug' => $slug,
- 'input' => $data,
- ];
-}
-
-function render_post_content(string $content): string
-{
- $paragraphs = preg_split('/\R{2,}/', trim($content)) ?: [];
- $html = [];
-
- foreach ($paragraphs as $paragraph) {
- $paragraph = trim($paragraph);
- if ($paragraph === '') {
- continue;
- }
-
- $html[] = '
Pindah hosting? Tinggal isi data database, lalu website jalan lagi.
-
Kalau script ini dipindah ke server baru, halaman ini akan muncul otomatis saat koneksi database belum siap. Anda hanya perlu memasukkan host, port, nama database, username, dan password MySQL.
-
-
-
-
Yang perlu disiapkan
-
-
Host database, seringnya localhost.
-
Port MySQL, default 3306.
-
Nama database dari hosting/cPanel.
-
Username dan password database.
-
-
Jika database yang Anda tulis belum ada, installer akan mencoba membuatnya otomatis jika akun database punya izin.
-
-
-
-
-
-
-
-
-
Status saat ini
-
-
Aplikasi sudah terhubung ke database.
-
Sumber konfigurasi aktif: = e(installer_source_label((string)$status['source'])) ?>.
-
Kalau Anda hanya ingin memakai website, tidak perlu mengisi ulang form di kanan.
-
-
- = e((string)$status['message']) ?>
-
-
= e((string)$status['details']) ?>
-
-
-
Begitu data database benar, aplikasi akan membuat tabel blog otomatis saat Anda masuk ke halaman utama.
-
-
-
-
-
-
-
-
- Form koneksi
-
Isi data MySQL / MariaDB
-
Data ini biasanya ada di menu Database pada panel hosting atau PHPMyAdmin.