Aslam
This commit is contained in:
parent
730982d703
commit
6ea9183450
171
admin.php
Normal file
171
admin.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
boot_site();
|
||||
|
||||
$formData = [
|
||||
'title' => '',
|
||||
'category' => '',
|
||||
'excerpt' => '',
|
||||
'content' => '',
|
||||
'cta_text' => 'Kunjungi apknusa.com',
|
||||
'cta_url' => APKNUSA_URL,
|
||||
'featured' => 0,
|
||||
];
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$result = create_post($_POST);
|
||||
if (!empty($result['success'])) {
|
||||
header('Location: admin.php?status=created&slug=' . rawurlencode((string)$result['slug']), true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = $result['errors'] ?? [];
|
||||
$formData = array_merge($formData, $result['input'] ?? []);
|
||||
}
|
||||
|
||||
$posts = fetch_posts();
|
||||
$latestSlug = trim((string)($_GET['slug'] ?? ''));
|
||||
$status = trim((string)($_GET['status'] ?? ''));
|
||||
|
||||
render_page_start([
|
||||
'title' => 'Kelola Konten Blog',
|
||||
'description' => 'Halaman admin sederhana untuk menambah artikel blog yang mengarahkan pembaca ke apknusa.com.',
|
||||
'canonical' => canonical_for('admin.php'),
|
||||
'robots' => 'noindex,nofollow',
|
||||
'keywords' => 'admin blog, tambah artikel apknusa',
|
||||
'body_class' => 'admin-page',
|
||||
]);
|
||||
?>
|
||||
<main id="main-content" class="page-wrap py-5">
|
||||
<div class="container">
|
||||
<?php if ($status === 'created' && $latestSlug !== ''): ?>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div class="toast text-bg-dark border-0" role="status" aria-live="polite" aria-atomic="true" data-autoshow="true" data-bs-delay="4500">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">Artikel berhasil ditambahkan dan sitemap.xml sudah diperbarui.</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Tutup"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-5">
|
||||
<section class="panel-card sticky-card">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="section-kicker">Tambah artikel baru</span>
|
||||
<h1 class="section-title mb-2">Workflow admin sederhana</h1>
|
||||
<p class="section-copy mb-0">Tulis artikel, tentukan CTA, lalu artikel otomatis masuk ke blog publik dan sitemap.</p>
|
||||
</div>
|
||||
<div class="alert alert-light border mb-4" role="alert">
|
||||
Tautan CTA dibatasi ke <strong>apknusa.com</strong> agar microsite tetap fokus sebagai website pendukung backlink.
|
||||
</div>
|
||||
<form method="post" action="admin.php" novalidate class="vstack gap-3">
|
||||
<div>
|
||||
<label class="form-label" for="title">Judul artikel</label>
|
||||
<input class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" type="text" id="title" name="title" value="<?= e((string)$formData['title']) ?>" data-slug-source maxlength="180" placeholder="Contoh: Cara memilih sumber APK yang aman" required>
|
||||
<div class="form-text">Slug dibuat otomatis: <span class="slug-preview" data-slug-target>slug-artikel-otomatis</span></div>
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= e($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="category">Kategori</label>
|
||||
<input class="form-control" type="text" id="category" name="category" value="<?= e((string)$formData['category']) ?>" maxlength="80" placeholder="Contoh: Tips Android">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="excerpt">Ringkasan singkat</label>
|
||||
<textarea class="form-control <?= isset($errors['excerpt']) ? 'is-invalid' : '' ?>" id="excerpt" name="excerpt" rows="3" placeholder="Tuliskan ringkasan artikel untuk kartu blog dan meta description."><?= e((string)$formData['excerpt']) ?></textarea>
|
||||
<?php if (isset($errors['excerpt'])): ?><div class="invalid-feedback"><?= e($errors['excerpt']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="content">Isi artikel</label>
|
||||
<textarea class="form-control <?= isset($errors['content']) ? 'is-invalid' : '' ?>" id="content" name="content" rows="10" placeholder="Tulis isi artikel di sini. Gunakan jeda baris kosong antar paragraf."><?= e((string)$formData['content']) ?></textarea>
|
||||
<?php if (isset($errors['content'])): ?><div class="invalid-feedback"><?= e($errors['content']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="cta_text">Teks CTA</label>
|
||||
<input class="form-control <?= isset($errors['cta_text']) ? 'is-invalid' : '' ?>" type="text" id="cta_text" name="cta_text" value="<?= e((string)$formData['cta_text']) ?>" maxlength="120">
|
||||
<?php if (isset($errors['cta_text'])): ?><div class="invalid-feedback"><?= e($errors['cta_text']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="cta_url">URL CTA</label>
|
||||
<input class="form-control <?= isset($errors['cta_url']) ? 'is-invalid' : '' ?>" type="url" id="cta_url" name="cta_url" value="<?= e((string)$formData['cta_url']) ?>" placeholder="https://apknusa.com">
|
||||
<?php if (isset($errors['cta_url'])): ?><div class="invalid-feedback"><?= e($errors['cta_url']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="featured" name="featured" value="1" <?= !empty($formData['featured']) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="featured">Jadikan artikel unggulan di halaman utama</label>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 pt-2">
|
||||
<button class="btn btn-dark" type="submit">Simpan artikel</button>
|
||||
<a class="btn btn-outline-dark" href="blog.php">Lihat blog publik</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<section class="panel-card mb-4">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3">
|
||||
<div>
|
||||
<span class="section-kicker">Status konten</span>
|
||||
<h2 class="section-title mb-1">Artikel yang sudah tayang</h2>
|
||||
<p class="section-copy mb-0">Daftar ini memperlihatkan konten yang aktif di blog dan ikut masuk ke sitemap.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="sitemap.xml">Buka sitemap</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="robots.txt">Buka robots</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0 admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikel</th>
|
||||
<th>Kategori</th>
|
||||
<th>CTA</th>
|
||||
<th>Tayang</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string)$post['title']) ?></div>
|
||||
<div class="small text-secondary"><?= e((string)$post['slug']) ?></div>
|
||||
</td>
|
||||
<td><span class="tag-badge"><?= e((string)$post['category']) ?></span></td>
|
||||
<td>
|
||||
<div class="small fw-semibold"><?= e((string)$post['cta_text']) ?></div>
|
||||
<div class="small text-secondary text-break"><?= e((string)$post['cta_url']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small"><?= e((string)format_article_date((string)$post['published_at'])) ?></div>
|
||||
<?php if (!empty($post['featured'])): ?><span class="small text-secondary">Unggulan</span><?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="<?= e(post_url($post)) ?>">Detail</a>
|
||||
<a class="btn btn-dark btn-sm" href="<?= e((string)$post['cta_url']) ?>" target="_blank" rel="noopener noreferrer">CTA</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php render_page_end(); ?>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,34 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
const slugify = (value) => {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'slug-artikel-otomatis';
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
const titleInput = document.querySelector('[data-slug-source]');
|
||||
const slugTarget = document.querySelector('[data-slug-target]');
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
if (titleInput && slugTarget) {
|
||||
const updateSlug = () => {
|
||||
slugTarget.textContent = slugify(titleInput.value);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
titleInput.addEventListener('input', updateSlug);
|
||||
updateSlug();
|
||||
}
|
||||
|
||||
if (window.bootstrap) {
|
||||
document.querySelectorAll('.toast').forEach((toastElement) => {
|
||||
const toast = new window.bootstrap.Toast(toastElement, {
|
||||
delay: Number.parseInt(toastElement.dataset.bsDelay || '5000', 10),
|
||||
autohide: true,
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
});
|
||||
|
||||
if (toastElement.dataset.autoshow !== 'false') {
|
||||
toast.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
104
blog.php
Normal file
104
blog.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
boot_site();
|
||||
|
||||
$categories = fetch_categories();
|
||||
$selectedCategory = trim((string)($_GET['category'] ?? ''));
|
||||
if ($selectedCategory !== '' && !in_array($selectedCategory, $categories, true)) {
|
||||
$selectedCategory = '';
|
||||
}
|
||||
|
||||
$posts = fetch_posts($selectedCategory !== '' ? $selectedCategory : null);
|
||||
$blogDescription = $selectedCategory !== ''
|
||||
? 'Artikel kategori ' . $selectedCategory . ' yang mengarahkan pembaca ke apknusa.com.'
|
||||
: 'Kumpulan artikel singkat seputar aplikasi Android dan topik terkait yang mengarahkan pembaca ke apknusa.com.';
|
||||
|
||||
render_page_start([
|
||||
'title' => $selectedCategory !== '' ? 'Blog: ' . $selectedCategory : 'Blog Backlink untuk apknusa.com',
|
||||
'description' => $blogDescription,
|
||||
'canonical' => $selectedCategory !== '' ? canonical_for('blog.php?category=' . rawurlencode($selectedCategory)) : canonical_for('blog.php'),
|
||||
'keywords' => 'blog apknusa, artikel android, backlink apknusa, tips apk',
|
||||
'body_class' => 'blog-page',
|
||||
]);
|
||||
?>
|
||||
<main id="main-content" class="page-wrap py-5">
|
||||
<div class="container">
|
||||
<section class="blog-hero mb-4">
|
||||
<div class="row g-4 align-items-stretch">
|
||||
<div class="col-lg-8">
|
||||
<div class="panel-card blog-hero-card h-100">
|
||||
<span class="section-kicker">Blog publik</span>
|
||||
<h1 class="section-title mb-2">Artikel singkat yang membangun konteks sebelum pengunjung lanjut ke apknusa.com</h1>
|
||||
<p class="section-copy mb-0"><?= e($blogDescription) ?> Setiap artikel di halaman ini tetap informatif, tetapi ujung perjalanannya adalah CTA yang membawa pembaca ke brand utama.</p>
|
||||
<div class="hero-chip-row mt-4">
|
||||
<span class="meta-pill"><strong><?= e((string)count($posts)) ?></strong> artikel tampil</span>
|
||||
<span class="meta-pill"><strong><?= e((string)count($categories)) ?></strong> kategori</span>
|
||||
<span class="meta-pill">Contextual CTA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="panel-card blog-side-card h-100">
|
||||
<div class="card-label">Tujuan akhir</div>
|
||||
<h2 class="h4 mb-2">Baca singkat, klik ke sumber utama.</h2>
|
||||
<p class="section-copy mb-3">Jika pengunjung sudah tertarik, setiap artikel di bawah sudah menyiapkan jalur ke apknusa.com dengan konteks yang lebih rapi.</p>
|
||||
<a class="btn btn-dark w-100" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card mb-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
||||
<div>
|
||||
<div class="card-label">Filter topik</div>
|
||||
<p class="section-copy mb-0">Pilih kategori untuk melihat kelompok artikel yang paling relevan dengan intent pembaca.</p>
|
||||
</div>
|
||||
<div class="tag-group">
|
||||
<a class="tag-chip <?= $selectedCategory === '' ? 'is-active' : '' ?>" href="blog.php">Semua</a>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<a class="tag-chip <?= $selectedCategory === $category ? 'is-active' : '' ?>" href="blog.php?category=<?= rawurlencode($category) ?>"><?= e($category) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($posts === []): ?>
|
||||
<section class="empty-state">
|
||||
<h2 class="h4 mb-2">Belum ada artikel pada kategori ini</h2>
|
||||
<p class="mb-3">Silakan pilih kategori lain atau kembali ke semua artikel yang tersedia.</p>
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
||||
<a class="btn btn-dark" href="blog.php">Lihat semua artikel</a>
|
||||
<a class="btn btn-outline-dark" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<article class="content-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-3">
|
||||
<span class="tag-badge"><?= e((string)$post['category']) ?></span>
|
||||
<span class="muted-meta"><?= e((string)reading_time_minutes((string)$post['content'])) ?> menit baca</span>
|
||||
</div>
|
||||
<h2 class="card-title h4"><a href="<?= e(post_url($post)) ?>"><?= e((string)$post['title']) ?></a></h2>
|
||||
<p class="card-copy"><?= e((string)$post['excerpt']) ?></p>
|
||||
<div class="article-meta mt-auto pt-3 border-top">
|
||||
<div class="muted-meta mb-3">Dipublikasikan <?= e((string)format_article_date((string)$post['published_at'])) ?></div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark btn-sm" href="<?= e(post_url($post)) ?>">Baca artikel</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="<?= e((string)$post['cta_url']) ?>" target="_blank" rel="noopener noreferrer">Menuju apknusa.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
<?php render_page_end(); ?>
|
||||
258
db/config.php
258
db/config.php
@ -1,21 +1,247 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — database password must come from process env.
|
||||
define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'app_default');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'app_default');
|
||||
define('DB_PASS', getenv('DB_PASS') ?: '');
|
||||
declare(strict_types=1);
|
||||
|
||||
if (DB_PASS === '') {
|
||||
throw new RuntimeException('DB_PASS environment variable is not set.');
|
||||
const DB_INSTALLER_CONFIG_FILE = __DIR__ . '/installer-config.php';
|
||||
|
||||
function db_normalize_settings(array $settings): array
|
||||
{
|
||||
$host = trim((string)($settings['DB_HOST'] ?? ''));
|
||||
$port = trim((string)($settings['DB_PORT'] ?? ''));
|
||||
$name = trim((string)($settings['DB_NAME'] ?? ''));
|
||||
$user = trim((string)($settings['DB_USER'] ?? ''));
|
||||
$password = isset($settings['DB_PASS']) ? (string)$settings['DB_PASS'] : '';
|
||||
|
||||
return [
|
||||
'DB_HOST' => $host !== '' ? $host : 'localhost',
|
||||
'DB_PORT' => $port !== '' ? $port : '3306',
|
||||
'DB_NAME' => $name,
|
||||
'DB_USER' => $user,
|
||||
'DB_PASS' => $password,
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
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 = "<?php
|
||||
";
|
||||
$payload .= "declare(strict_types=1);
|
||||
|
||||
";
|
||||
$payload .= 'return ' . var_export([
|
||||
'DB_HOST' => $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.');
|
||||
}
|
||||
}
|
||||
|
||||
18
db/migrations/20260517_create_backlink_posts.sql
Normal file
18
db/migrations/20260517_create_backlink_posts.sql
Normal file
@ -0,0 +1,18 @@
|
||||
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;
|
||||
41
healthz.php
Normal file
41
healthz.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
|
||||
$status = site_installation_status();
|
||||
if (empty($status['ready'])) {
|
||||
http_response_code(503);
|
||||
echo json_encode([
|
||||
'status' => '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);
|
||||
}
|
||||
122
includes/layout.php
Normal file
122
includes/layout.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/site.php';
|
||||
|
||||
function nav_link_class(string $scriptName): string
|
||||
{
|
||||
$currentScript = basename((string)($_SERVER['SCRIPT_NAME'] ?? 'index.php'));
|
||||
return $currentScript === $scriptName ? 'nav-link active' : 'nav-link';
|
||||
}
|
||||
|
||||
function render_page_start(array $options = []): void
|
||||
{
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: '';
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? getenv('PROJECT_NAME') ?: project_name();
|
||||
|
||||
$pageTitle = $options['title'] ?? project_name();
|
||||
$metaDescription = $options['description'] ?? ($projectDescription !== '' ? $projectDescription : project_description());
|
||||
$canonicalUrl = $options['canonical'] ?? current_url();
|
||||
$robots = $options['robots'] ?? 'index,follow';
|
||||
$keywords = $options['keywords'] ?? 'apknusa, aplikasi android, blog apk, tips android, backlink website';
|
||||
$bodyClass = $options['body_class'] ?? '';
|
||||
$jsonLd = $options['json_ld'] ?? null;
|
||||
$brandName = project_name();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e($pageTitle) ?></title>
|
||||
<meta name="description" content="<?= e($metaDescription) ?>">
|
||||
<meta name="robots" content="<?= e($robots) ?>">
|
||||
<meta name="keywords" content="<?= e($keywords) ?>">
|
||||
<meta name="author" content="<?= e($projectName) ?>">
|
||||
<link rel="canonical" href="<?= e($canonicalUrl) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="<?= e($pageTitle) ?>">
|
||||
<meta property="og:url" content="<?= e($canonicalUrl) ?>">
|
||||
<meta property="og:site_name" content="<?= e($brandName) ?>">
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:title" content="<?= e($pageTitle) ?>">
|
||||
<?php if ($metaDescription !== ''): ?>
|
||||
<meta property="og:description" content="<?= e($metaDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= e($metaDescription) ?>">
|
||||
<?php elseif ($projectDescription): ?>
|
||||
<meta property="og:description" content="<?= e($projectDescription) ?>">
|
||||
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="<?= e(asset_url('assets/css/custom.css')) ?>">
|
||||
<?php if (is_array($jsonLd) && $jsonLd !== []): ?>
|
||||
<script type="application/ld+json"><?= json_encode($jsonLd, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?></script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body class="<?= e($bodyClass) ?>">
|
||||
<a class="skip-link" href="#main-content">Lewati ke konten utama</a>
|
||||
<header class="site-header sticky-top border-bottom">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="index.php" aria-label="<?= e($brandName) ?>">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
<span class="brand-copy">
|
||||
<span class="brand-title"><?= e($brandName) ?></span>
|
||||
<span class="brand-subtitle">backlink microsite</span>
|
||||
</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#siteNav" aria-controls="siteNav" aria-expanded="false" aria-label="Buka navigasi">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="siteNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="<?= e(nav_link_class('index.php')) ?>" href="index.php">Beranda</a></li>
|
||||
<li class="nav-item"><a class="<?= e(nav_link_class('blog.php')) ?>" href="blog.php">Blog</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#strategi">Preview</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions d-flex align-items-center gap-2">
|
||||
<a class="btn btn-sm btn-outline-dark" href="sitemap.xml">Sitemap</a>
|
||||
<a class="btn btn-sm btn-dark" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Kunjungi apknusa.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<?php
|
||||
}
|
||||
|
||||
function render_page_end(): void
|
||||
{
|
||||
$brandName = project_name();
|
||||
?>
|
||||
<footer class="site-footer border-top">
|
||||
<div class="container py-4 d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-start align-items-lg-center">
|
||||
<div>
|
||||
<div class="footer-title"><?= e($brandName) ?></div>
|
||||
<p class="footer-copy mb-0">Artikel singkat, preview apknusa.com, dan CTA langsung ke website utama.</p>
|
||||
</div>
|
||||
<div class="footer-links d-flex flex-wrap gap-3">
|
||||
<a href="index.php">Beranda</a>
|
||||
<a href="blog.php">Blog</a>
|
||||
<a href="index.php#strategi">Preview</a>
|
||||
<a href="robots.txt">robots.txt</a>
|
||||
<a href="sitemap.xml">sitemap.xml</a>
|
||||
<a href="healthz.php">healthz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<script src="<?= e(asset_url('assets/js/main.js')) ?>" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
667
includes/site.php
Normal file
667
includes/site.php
Normal file
@ -0,0 +1,667 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
const APKNUSA_URL = 'https://apknusa.com';
|
||||
defined('PROJECT_ROOT') || define('PROJECT_ROOT', dirname(__DIR__));
|
||||
|
||||
function project_root_path(string $relativePath = ''): string
|
||||
{
|
||||
if ($relativePath === '') {
|
||||
return PROJECT_ROOT;
|
||||
}
|
||||
|
||||
return PROJECT_ROOT . '/' . ltrim($relativePath, '/');
|
||||
}
|
||||
|
||||
function env_value(string $key, string $default = ''): string
|
||||
{
|
||||
$serverValue = $_SERVER[$key] ?? null;
|
||||
if (is_string($serverValue) && trim($serverValue) !== '') {
|
||||
return trim($serverValue);
|
||||
}
|
||||
|
||||
$envValue = getenv($key);
|
||||
if (is_string($envValue) && trim($envValue) !== '') {
|
||||
return trim($envValue);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
function project_name(): string
|
||||
{
|
||||
return env_value('PROJECT_NAME', 'APKNusa Insight');
|
||||
}
|
||||
|
||||
function project_description(): string
|
||||
{
|
||||
return env_value(
|
||||
'PROJECT_DESCRIPTION',
|
||||
'Microsite pendukung SEO yang menampilkan artikel singkat dan mengarahkan pembaca ke apknusa.com.'
|
||||
);
|
||||
}
|
||||
|
||||
function e(string|int|float|null $value): string
|
||||
{
|
||||
return htmlspecialchars((string)($value ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function xml_escape(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
function text_length(string $value): int
|
||||
{
|
||||
return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
|
||||
}
|
||||
|
||||
function text_substr(string $value, int $start, ?int $length = null): string
|
||||
{
|
||||
if (function_exists('mb_substr')) {
|
||||
return $length === null ? mb_substr($value, $start) : mb_substr($value, $start, $length);
|
||||
}
|
||||
|
||||
return $length === null ? substr($value, $start) : substr($value, $start, $length);
|
||||
}
|
||||
|
||||
function text_lower(string $value): string
|
||||
{
|
||||
return function_exists('mb_strtolower') ? mb_strtolower($value) : strtolower($value);
|
||||
}
|
||||
|
||||
function asset_url(string $relativePath): string
|
||||
{
|
||||
$trimmedPath = ltrim($relativePath, '/');
|
||||
$absolutePath = project_root_path($trimmedPath);
|
||||
$version = is_file($absolutePath) ? (string)filemtime($absolutePath) : (string)time();
|
||||
|
||||
return '/' . $trimmedPath . '?v=' . $version;
|
||||
}
|
||||
|
||||
function request_is_secure(): bool
|
||||
{
|
||||
if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (string)($_SERVER['SERVER_PORT'] ?? '') === '443';
|
||||
}
|
||||
|
||||
function site_base_url(): string
|
||||
{
|
||||
$scheme = request_is_secure() ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '127.0.0.1';
|
||||
|
||||
return $scheme . '://' . $host;
|
||||
}
|
||||
|
||||
function current_url(): string
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
return site_base_url() . $uri;
|
||||
}
|
||||
|
||||
function canonical_for(string $path): string
|
||||
{
|
||||
$path = '/' . ltrim($path, '/');
|
||||
return site_base_url() . $path;
|
||||
}
|
||||
|
||||
function current_script_name(): string
|
||||
{
|
||||
return basename((string)($_SERVER['SCRIPT_NAME'] ?? 'index.php'));
|
||||
}
|
||||
|
||||
function safe_install_next_path(string $path): string
|
||||
{
|
||||
$trimmedPath = trim($path);
|
||||
if ($trimmedPath === '' || str_starts_with($trimmedPath, 'http://') || str_starts_with($trimmedPath, 'https://')) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (!str_starts_with($trimmedPath, '/')) {
|
||||
$trimmedPath = '/' . ltrim($trimmedPath, '/');
|
||||
}
|
||||
|
||||
if (str_contains($trimmedPath, 'install.php')) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return $trimmedPath;
|
||||
}
|
||||
|
||||
function site_installation_status(): array
|
||||
{
|
||||
static $status = null;
|
||||
|
||||
if (is_array($status)) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
$status = [
|
||||
'ready' => 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;
|
||||
}
|
||||
|
||||
$migrationFile = project_root_path('db/migrations/20260517_create_backlink_posts.sql');
|
||||
$sql = is_file($migrationFile) ? file_get_contents($migrationFile) : false;
|
||||
|
||||
if ($sql === false || trim($sql) === '') {
|
||||
throw new RuntimeException('Blog migration file not found.');
|
||||
}
|
||||
|
||||
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[] = '<p>' . nl2br(e($paragraph)) . '</p>';
|
||||
}
|
||||
|
||||
return implode("\n", $html);
|
||||
}
|
||||
|
||||
function format_article_date(string $dateTime): string
|
||||
{
|
||||
$timestamp = strtotime($dateTime);
|
||||
if ($timestamp === false) {
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
$months = [
|
||||
1 => 'Januari',
|
||||
2 => 'Februari',
|
||||
3 => 'Maret',
|
||||
4 => 'April',
|
||||
5 => 'Mei',
|
||||
6 => 'Juni',
|
||||
7 => 'Juli',
|
||||
8 => 'Agustus',
|
||||
9 => 'September',
|
||||
10 => 'Oktober',
|
||||
11 => 'November',
|
||||
12 => 'Desember',
|
||||
];
|
||||
|
||||
$monthNumber = (int)date('n', $timestamp);
|
||||
|
||||
return date('j ', $timestamp) . $months[$monthNumber] . date(' Y', $timestamp);
|
||||
}
|
||||
|
||||
function maybe_write_file(string $path, string $content): void
|
||||
{
|
||||
$existing = is_file($path) ? file_get_contents($path) : false;
|
||||
if ($existing === $content) {
|
||||
return;
|
||||
}
|
||||
|
||||
$directory = dirname($path);
|
||||
if (!is_dir($directory) || !is_writable($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@file_put_contents($path, $content, LOCK_EX);
|
||||
}
|
||||
|
||||
function refresh_seo_artifacts(): void
|
||||
{
|
||||
init_blog_storage();
|
||||
|
||||
$baseUrl = site_base_url();
|
||||
$posts = fetch_posts();
|
||||
$today = gmdate('c');
|
||||
|
||||
$entries = [
|
||||
[
|
||||
'loc' => $baseUrl . '/',
|
||||
'lastmod' => $today,
|
||||
'changefreq' => 'daily',
|
||||
'priority' => '1.0',
|
||||
],
|
||||
[
|
||||
'loc' => $baseUrl . '/blog.php',
|
||||
'lastmod' => $today,
|
||||
'changefreq' => 'daily',
|
||||
'priority' => '0.8',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$entries[] = [
|
||||
'loc' => $baseUrl . '/' . post_url($post),
|
||||
'lastmod' => gmdate('c', strtotime((string)($post['updated_at'] ?? $post['published_at'] ?? $today))),
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.7',
|
||||
];
|
||||
}
|
||||
|
||||
$sitemap = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$sitemap[] = ' <url>';
|
||||
$sitemap[] = ' <loc>' . xml_escape($entry['loc']) . '</loc>';
|
||||
$sitemap[] = ' <lastmod>' . xml_escape($entry['lastmod']) . '</lastmod>';
|
||||
$sitemap[] = ' <changefreq>' . xml_escape($entry['changefreq']) . '</changefreq>';
|
||||
$sitemap[] = ' <priority>' . xml_escape($entry['priority']) . '</priority>';
|
||||
$sitemap[] = ' </url>';
|
||||
}
|
||||
|
||||
$sitemap[] = '</urlset>';
|
||||
|
||||
$robots = implode("\n", [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
'Disallow: /admin.php',
|
||||
'Sitemap: ' . $baseUrl . '/sitemap.xml',
|
||||
'',
|
||||
]);
|
||||
|
||||
maybe_write_file(project_root_path('sitemap.xml'), implode("\n", $sitemap) . "\n");
|
||||
maybe_write_file(project_root_path('robots.txt'), $robots);
|
||||
}
|
||||
346
index.php
346
index.php
@ -1,150 +1,208 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
boot_site();
|
||||
|
||||
$featuredPosts = fetch_featured_posts(3);
|
||||
$latestPosts = fetch_posts(null, 5);
|
||||
if ($featuredPosts === []) {
|
||||
$featuredPosts = array_slice($latestPosts, 0, 3);
|
||||
}
|
||||
$categories = fetch_categories();
|
||||
$totalPosts = post_count();
|
||||
$categoryCount = count($categories);
|
||||
$homeUrl = canonical_for('');
|
||||
$heroDescription = 'Artikel singkat, live preview, dan jalur cepat ke apknusa.com.';
|
||||
|
||||
render_page_start([
|
||||
'title' => 'Backlink Microsite untuk apknusa.com',
|
||||
'description' => 'Artikel singkat, live preview, dan tombol cepat menuju apknusa.com.',
|
||||
'canonical' => $homeUrl,
|
||||
'keywords' => 'apknusa, backlink, preview website, blog aplikasi android, microsite seo',
|
||||
'body_class' => 'home-page',
|
||||
'json_ld' => [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'WebSite',
|
||||
'name' => project_name(),
|
||||
'url' => $homeUrl,
|
||||
'description' => $heroDescription,
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => project_name(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<main id="main-content">
|
||||
<section class="hero-section py-5 py-lg-6">
|
||||
<div class="container">
|
||||
<div class="hero-panel">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<span class="eyebrow">Backlink microsite</span>
|
||||
<h1 class="hero-title mt-3 mb-3">Lihat apknusa.com, baca cepat, lalu klik lanjut.</h1>
|
||||
<p class="hero-copy mb-4"><?= e($heroDescription) ?></p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark btn-cta" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
<a class="btn btn-outline-dark btn-cta" href="blog.php">Lihat artikel</a>
|
||||
</div>
|
||||
<div class="hero-chip-row mt-4">
|
||||
<span class="meta-pill"><strong><?= e((string)$totalPosts) ?></strong> artikel</span>
|
||||
<span class="meta-pill"><strong><?= e((string)$categoryCount) ?></strong> topik</span>
|
||||
<span class="meta-pill">Preview live</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="hero-visual has-preview">
|
||||
<div class="hero-glow hero-glow-a"></div>
|
||||
<div class="hero-glow hero-glow-b"></div>
|
||||
<div class="visual-browser preview-browser">
|
||||
<div class="visual-bar preview-bar">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<div class="preview-url">https://apknusa.com</div>
|
||||
</div>
|
||||
<div class="preview-screen">
|
||||
<iframe
|
||||
src="<?= e(APKNUSA_URL) ?>"
|
||||
title="Pratinjau apknusa.com"
|
||||
loading="lazy"
|
||||
referrerpolicy="strict-origin-when-cross-origin"></iframe>
|
||||
</div>
|
||||
<div class="preview-footer">
|
||||
<span class="visual-badge">Live preview</span>
|
||||
<a class="btn btn-sm btn-light" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka penuh</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0" id="strategi">
|
||||
<div class="container">
|
||||
<div class="spotlight-banner compact-spotlight">
|
||||
<div>
|
||||
<span class="section-kicker">Preview cepat</span>
|
||||
<h2 class="section-title mb-2">Website utama langsung tampil di landing ini.</h2>
|
||||
<p class="section-copy mb-0">Kalau iframe tidak muncul di browser, pakai tombol buka penuh.</p>
|
||||
</div>
|
||||
<div class="spotlight-pills">
|
||||
<span class="spotlight-pill">apknusa.com</span>
|
||||
<span class="spotlight-pill">Artikel ringkas</span>
|
||||
<span class="spotlight-pill">CTA langsung</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block" id="artikel">
|
||||
<div class="container">
|
||||
<div class="section-heading d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-end gap-3 mb-4">
|
||||
<div>
|
||||
<span class="section-kicker">Artikel unggulan</span>
|
||||
<h2 class="section-title mb-1">Pilih artikel singkat yang paling relevan.</h2>
|
||||
<p class="section-copy mb-0">Setelah itu, lanjutkan ke apknusa.com dari tombol CTA.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-dark btn-sm" href="blog.php">Lihat semua artikel</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($featuredPosts as $post): ?>
|
||||
<div class="col-lg-4">
|
||||
<article class="content-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-3">
|
||||
<span class="tag-badge"><?= e((string)$post['category']) ?></span>
|
||||
<span class="muted-meta"><?= e((string)reading_time_minutes((string)$post['content'])) ?> menit baca</span>
|
||||
</div>
|
||||
<h3 class="card-title"><a href="<?= e(post_url($post)) ?>"><?= e((string)$post['title']) ?></a></h3>
|
||||
<p class="card-copy"><?= e((string)$post['excerpt']) ?></p>
|
||||
<div class="card-footer-actions mt-auto d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark btn-sm" href="<?= e(post_url($post)) ?>">Baca detail</a>
|
||||
<a class="btn btn-light btn-sm" href="<?= e((string)$post['cta_url']) ?>" target="_blank" rel="noopener noreferrer">Ke apknusa.com</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-stretch">
|
||||
<div class="col-lg-6">
|
||||
<div class="panel-card h-100">
|
||||
<span class="section-kicker">Topik siap pakai</span>
|
||||
<h2 class="section-title">Kategori yang sudah siap jadi pintu masuk.</h2>
|
||||
<p class="section-copy mb-0">Pilih topik yang paling dekat dengan intent pengunjung.</p>
|
||||
<?php if ($categories !== []): ?>
|
||||
<div class="tag-group mt-3">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<a class="tag-chip" href="blog.php?category=<?= rawurlencode($category) ?>"><?= e($category) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="panel-card h-100">
|
||||
<span class="section-kicker">Quick links</span>
|
||||
<h2 class="section-title">Semua jalur utama ada di sini.</h2>
|
||||
<p class="section-copy mb-4">Tanpa banyak penjelasan tambahan.</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark btn-sm" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="blog.php">Buka blog</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="robots.txt">robots.txt</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="sitemap.xml">sitemap.xml</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block pt-0">
|
||||
<div class="container">
|
||||
<div class="section-heading mb-4">
|
||||
<span class="section-kicker">Artikel terbaru</span>
|
||||
<h2 class="section-title mb-1">Update konten terbaru</h2>
|
||||
</div>
|
||||
<?php if ($latestPosts === []): ?>
|
||||
<section class="empty-state">
|
||||
<h3 class="h4 mb-2">Belum ada artikel terbaru</h3>
|
||||
<p class="mb-3">Sementara, buka blog atau langsung ke apknusa.com.</p>
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
||||
<a class="btn btn-dark" href="blog.php">Buka blog</a>
|
||||
<a class="btn btn-outline-dark" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<div class="list-shell">
|
||||
<?php foreach ($latestPosts as $post): ?>
|
||||
<article class="list-row">
|
||||
<div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<span class="tag-badge"><?= e((string)$post['category']) ?></span>
|
||||
<span class="muted-meta"><?= e((string)format_article_date((string)$post['published_at'])) ?></span>
|
||||
</div>
|
||||
<h3 class="list-title mb-1"><a href="<?= e(post_url($post)) ?>"><?= e((string)$post['title']) ?></a></h3>
|
||||
<p class="list-copy mb-0"><?= e((string)$post['excerpt']) ?></p>
|
||||
</div>
|
||||
<div class="list-row-actions d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="<?= e(post_url($post)) ?>">Detail</a>
|
||||
<a class="btn btn-dark btn-sm" href="<?= e((string)$post['cta_url']) ?>" target="_blank" rel="noopener noreferrer">Ke apknusa.com</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<?php render_page_end(); ?>
|
||||
|
||||
213
install.php
Normal file
213
install.php
Normal file
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
function installer_source_label(string $source): string
|
||||
{
|
||||
return match ($source) {
|
||||
'env' => 'environment server',
|
||||
'file' => 'file installer lokal',
|
||||
default => 'belum ada',
|
||||
};
|
||||
}
|
||||
|
||||
$status = site_installation_status();
|
||||
$nextPath = safe_install_next_path((string)($_REQUEST['next'] ?? '/'));
|
||||
$formData = [
|
||||
'db_host' => DB_HOST !== '' ? DB_HOST : 'localhost',
|
||||
'db_port' => DB_PORT !== '' ? DB_PORT : '3306',
|
||||
'db_name' => DB_NAME,
|
||||
'db_user' => DB_USER,
|
||||
'db_pass' => DB_PASS,
|
||||
];
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
$formData = [
|
||||
'db_host' => trim((string)($_POST['db_host'] ?? 'localhost')),
|
||||
'db_port' => trim((string)($_POST['db_port'] ?? '3306')),
|
||||
'db_name' => trim((string)($_POST['db_name'] ?? '')),
|
||||
'db_user' => trim((string)($_POST['db_user'] ?? '')),
|
||||
'db_pass' => (string)($_POST['db_pass'] ?? ''),
|
||||
];
|
||||
$nextPath = safe_install_next_path((string)($_POST['next'] ?? '/'));
|
||||
|
||||
if ($formData['db_host'] === '') {
|
||||
$formData['db_host'] = 'localhost';
|
||||
}
|
||||
if ($formData['db_port'] === '') {
|
||||
$formData['db_port'] = '3306';
|
||||
}
|
||||
|
||||
if ($formData['db_name'] === '') {
|
||||
$errors['db_name'] = 'Nama database wajib diisi.';
|
||||
}
|
||||
if ($formData['db_user'] === '') {
|
||||
$errors['db_user'] = 'Username database wajib diisi.';
|
||||
}
|
||||
if (!ctype_digit($formData['db_port']) || (int)$formData['db_port'] < 1 || (int)$formData['db_port'] > 65535) {
|
||||
$errors['db_port'] = 'Port harus berupa angka 1 sampai 65535.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
$settings = [
|
||||
'DB_HOST' => $formData['db_host'],
|
||||
'DB_PORT' => $formData['db_port'],
|
||||
'DB_NAME' => $formData['db_name'],
|
||||
'DB_USER' => $formData['db_user'],
|
||||
'DB_PASS' => $formData['db_pass'],
|
||||
];
|
||||
|
||||
try {
|
||||
$connectionError = db_connection_error($settings);
|
||||
if ($connectionError !== null && db_error_is_missing_database($connectionError)) {
|
||||
db_create_database_if_missing($settings);
|
||||
$connectionError = db_connection_error($settings);
|
||||
}
|
||||
|
||||
if ($connectionError !== null) {
|
||||
throw new RuntimeException($connectionError);
|
||||
}
|
||||
|
||||
db_save_local_settings($settings);
|
||||
header('Location: ' . $nextPath);
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$flash = [
|
||||
'type' => 'danger',
|
||||
'title' => 'Koneksi database gagal.',
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$flash = [
|
||||
'type' => 'warning',
|
||||
'title' => 'Form belum lengkap.',
|
||||
'message' => 'Periksa lagi field yang diberi tanda merah.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
render_page_start([
|
||||
'title' => 'Installer Database',
|
||||
'description' => 'Form instalasi cepat untuk menghubungkan aplikasi ke database MySQL.',
|
||||
'canonical' => canonical_for('install.php'),
|
||||
'robots' => 'noindex,nofollow',
|
||||
'keywords' => 'installer database, setup mysql, setup php app',
|
||||
'body_class' => 'install-page',
|
||||
]);
|
||||
?>
|
||||
<main id="main-content" class="page-wrap py-5">
|
||||
<div class="container">
|
||||
<section class="hero-panel mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-7">
|
||||
<span class="eyebrow">Auto installer</span>
|
||||
<h1 class="hero-title mt-3 mb-3">Pindah hosting? Tinggal isi data database, lalu website jalan lagi.</h1>
|
||||
<p class="hero-copy mb-0">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.</p>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="panel-card h-100">
|
||||
<div class="card-label">Yang perlu disiapkan</div>
|
||||
<ul class="mb-0 ps-3 small">
|
||||
<li>Host database, seringnya <code>localhost</code>.</li>
|
||||
<li>Port MySQL, default <code>3306</code>.</li>
|
||||
<li>Nama database dari hosting/cPanel.</li>
|
||||
<li>Username dan password database.</li>
|
||||
</ul>
|
||||
<div class="alert alert-info mt-3 mb-0 small">Jika database yang Anda tulis belum ada, installer akan mencoba membuatnya otomatis jika akun database punya izin.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<section class="panel-card h-100">
|
||||
<div class="card-label">Status saat ini</div>
|
||||
<?php if (!empty($status['ready'])): ?>
|
||||
<div class="alert alert-success mb-3">Aplikasi sudah terhubung ke database.</div>
|
||||
<p class="section-copy mb-2">Sumber konfigurasi aktif: <strong><?= e(installer_source_label((string)$status['source'])) ?></strong>.</p>
|
||||
<p class="section-copy mb-0">Kalau Anda hanya ingin memakai website, tidak perlu mengisi ulang form di kanan.</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong><?= e((string)$status['message']) ?></strong>
|
||||
<?php if (!empty($status['details'])): ?>
|
||||
<div class="small mt-2"><?= e((string)$status['details']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="section-copy mb-0">Begitu data database benar, aplikasi akan membuat tabel blog otomatis saat Anda masuk ke halaman utama.</p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<section class="panel-card">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start gap-3 mb-4">
|
||||
<div>
|
||||
<span class="section-kicker">Form koneksi</span>
|
||||
<h2 class="section-title mb-1">Isi data MySQL / MariaDB</h2>
|
||||
<p class="section-copy mb-0">Data ini biasanya ada di menu Database pada panel hosting atau PHPMyAdmin.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-dark btn-sm" href="https://www.phpmyadmin.net/" target="_blank" rel="noopener noreferrer">Apa itu PHPMyAdmin?</a>
|
||||
</div>
|
||||
|
||||
<?php if ($flash !== null): ?>
|
||||
<div class="alert alert-<?= e((string)$flash['type']) ?>">
|
||||
<strong><?= e((string)$flash['title']) ?></strong>
|
||||
<div class="small mt-1"><?= e((string)$flash['message']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($status['ready'])): ?>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark" href="<?= e($nextPath) ?>">Masuk ke website</a>
|
||||
<a class="btn btn-outline-dark" href="index.php">Ke beranda</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="next" value="<?= e($nextPath) ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="db_host">Host database</label>
|
||||
<input class="form-control <?= isset($errors['db_host']) ? 'is-invalid' : '' ?>" type="text" id="db_host" name="db_host" value="<?= e($formData['db_host']) ?>" placeholder="localhost" autocomplete="off">
|
||||
<?php if (isset($errors['db_host'])): ?><div class="invalid-feedback"><?= e($errors['db_host']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="db_port">Port</label>
|
||||
<input class="form-control <?= isset($errors['db_port']) ? 'is-invalid' : '' ?>" type="text" id="db_port" name="db_port" value="<?= e($formData['db_port']) ?>" placeholder="3306" inputmode="numeric" autocomplete="off">
|
||||
<?php if (isset($errors['db_port'])): ?><div class="invalid-feedback"><?= e($errors['db_port']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="db_name">Nama database</label>
|
||||
<input class="form-control <?= isset($errors['db_name']) ? 'is-invalid' : '' ?>" type="text" id="db_name" name="db_name" value="<?= e($formData['db_name']) ?>" placeholder="nama_database" autocomplete="off">
|
||||
<?php if (isset($errors['db_name'])): ?><div class="invalid-feedback"><?= e($errors['db_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="db_user">Username database</label>
|
||||
<input class="form-control <?= isset($errors['db_user']) ? 'is-invalid' : '' ?>" type="text" id="db_user" name="db_user" value="<?= e($formData['db_user']) ?>" placeholder="username_db" autocomplete="off">
|
||||
<?php if (isset($errors['db_user'])): ?><div class="invalid-feedback"><?= e($errors['db_user']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="db_pass">Password database</label>
|
||||
<input class="form-control" type="password" id="db_pass" name="db_pass" value="<?= e($formData['db_pass']) ?>" placeholder="Kosongkan jika memang tidak ada password" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary mt-4 mb-0 small">Setelah berhasil disimpan, website akan memakai file konfigurasi lokal di folder <code>db/</code>. Jadi kalau script dipindah ke server lain, cukup buka web dan isi form ini lagi bila koneksi lama tidak cocok.</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<button class="btn btn-dark btn-cta" type="submit">Simpan & masuk ke website</button>
|
||||
<a class="btn btn-outline-dark btn-cta" href="/">Kembali</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php render_page_end(); ?>
|
||||
126
post.php
Normal file
126
post.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/site.php';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
boot_site();
|
||||
|
||||
$slug = trim((string)($_GET['slug'] ?? ''));
|
||||
$post = $slug !== '' ? fetch_post_by_slug($slug) : null;
|
||||
|
||||
if ($post === null) {
|
||||
http_response_code(404);
|
||||
render_page_start([
|
||||
'title' => 'Artikel tidak ditemukan',
|
||||
'description' => 'Halaman artikel yang diminta tidak ditemukan.',
|
||||
'canonical' => current_url(),
|
||||
'robots' => 'noindex,follow',
|
||||
]);
|
||||
?>
|
||||
<main id="main-content" class="page-wrap py-5">
|
||||
<div class="container">
|
||||
<section class="empty-state">
|
||||
<h1 class="h3 mb-2">Artikel tidak ditemukan</h1>
|
||||
<p class="mb-3">Slug yang diminta tidak tersedia. Silakan kembali ke blog untuk melihat artikel yang tersedia.</p>
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
||||
<a class="btn btn-dark" href="blog.php">Kembali ke blog</a>
|
||||
<a class="btn btn-outline-dark" href="index.php">Ke beranda</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<?php
|
||||
render_page_end();
|
||||
return;
|
||||
}
|
||||
|
||||
$canonicalUrl = canonical_for(post_url($post));
|
||||
$relatedPosts = fetch_related_posts((int)$post['id'], (string)$post['category'], 3);
|
||||
$description = excerpt_text((string)$post['excerpt'], 155);
|
||||
|
||||
render_page_start([
|
||||
'title' => (string)$post['title'],
|
||||
'description' => $description,
|
||||
'canonical' => $canonicalUrl,
|
||||
'keywords' => 'artikel apknusa, blog android, backlink apknusa, ' . (string)$post['category'],
|
||||
'json_ld' => [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BlogPosting',
|
||||
'headline' => (string)$post['title'],
|
||||
'description' => $description,
|
||||
'datePublished' => date(DATE_ATOM, strtotime((string)$post['published_at'])),
|
||||
'dateModified' => date(DATE_ATOM, strtotime((string)$post['updated_at'])),
|
||||
'mainEntityOfPage' => $canonicalUrl,
|
||||
'author' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => project_name(),
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => project_name(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
?>
|
||||
<main id="main-content" class="page-wrap py-5">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<article class="article-shell">
|
||||
<div class="article-meta-top d-flex flex-wrap gap-2 align-items-center mb-3">
|
||||
<a class="tag-badge text-decoration-none" href="blog.php?category=<?= rawurlencode((string)$post['category']) ?>"><?= e((string)$post['category']) ?></a>
|
||||
<span class="muted-meta"><?= e((string)format_article_date((string)$post['published_at'])) ?></span>
|
||||
<span class="muted-meta"><?= e((string)reading_time_minutes((string)$post['content'])) ?> menit baca</span>
|
||||
</div>
|
||||
<h1 class="article-title mb-3"><?= e((string)$post['title']) ?></h1>
|
||||
<p class="article-lead"><?= e((string)$post['excerpt']) ?></p>
|
||||
<div class="article-body">
|
||||
<?= render_post_content((string)$post['content']) ?>
|
||||
</div>
|
||||
<div class="cta-panel mt-4">
|
||||
<div>
|
||||
<div class="card-label">Langkah berikutnya</div>
|
||||
<h2 class="h4 mb-2">Lanjutkan ke website utama</h2>
|
||||
<p class="mb-0">Setelah membaca ringkasan ini, arahkan pengunjung ke apknusa.com untuk melihat referensi atau konten utama yang lebih lengkap.</p>
|
||||
</div>
|
||||
<a class="btn btn-dark btn-cta" href="<?= e((string)$post['cta_url']) ?>" target="_blank" rel="noopener noreferrer"><?= e((string)$post['cta_text']) ?></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<aside class="sidebar-stack d-flex flex-column gap-3">
|
||||
<div class="panel-card">
|
||||
<div class="card-label">Arah navigasi</div>
|
||||
<h2 class="h5 mb-2">Alur pengunjung</h2>
|
||||
<ol class="flow-list mb-0 ps-3">
|
||||
<li>Baca ringkasan topik</li>
|
||||
<li>Pahami konteks utama</li>
|
||||
<li>Klik menuju apknusa.com</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="card-label">Artikel terkait</div>
|
||||
<div class="related-list d-flex flex-column gap-3 mt-3">
|
||||
<?php foreach ($relatedPosts as $related): ?>
|
||||
<article class="related-item">
|
||||
<span class="muted-meta d-block mb-1"><?= e((string)$related['category']) ?></span>
|
||||
<h3 class="h6 mb-1"><a href="<?= e(post_url($related)) ?>"><?= e((string)$related['title']) ?></a></h3>
|
||||
<p class="small text-secondary mb-0"><?= e((string)excerpt_text((string)$related['excerpt'], 92)) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="card-label">Link cepat</div>
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="blog.php">Kembali ke blog</a>
|
||||
<a class="btn btn-outline-dark btn-sm" href="<?= e(APKNUSA_URL) ?>" target="_blank" rel="noopener noreferrer">Buka apknusa.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php render_page_end(); ?>
|
||||
4
robots.txt
Normal file
4
robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin.php
|
||||
Sitemap: https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/sitemap.xml
|
||||
51
sitemap.xml
Normal file
51
sitemap.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/</loc>
|
||||
<lastmod>2026-05-17T14:40:34+00:00</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/blog.php</loc>
|
||||
<lastmod>2026-05-17T14:40:34+00:00</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=checklist-link-seo-ke-apknusa-com</loc>
|
||||
<lastmod>2026-05-17T13:29:20+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=perbedaan-apk-xapk-dan-file-instalasi-lain-yang-perlu-dipahami</loc>
|
||||
<lastmod>2026-05-17T13:29:11+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=tips-menyimpan-file-apk-supaya-mudah-dicari-lagi</loc>
|
||||
<lastmod>2026-05-17T13:29:11+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=kenapa-review-aplikasi-penting-sebelum-download</loc>
|
||||
<lastmod>2026-05-17T13:29:11+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=panduan-update-aplikasi-android-tanpa-kehilangan-data</loc>
|
||||
<lastmod>2026-05-17T13:29:11+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://baclink-apknusa-microsite-ad42.dev.flatlogic.app/post.php?slug=cara-memilih-sumber-apk-yang-aman-sebelum-instal</loc>
|
||||
<lastmod>2026-05-17T13:29:11+00:00</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
Loading…
x
Reference in New Issue
Block a user