668 lines
23 KiB
PHP
668 lines
23 KiB
PHP
<?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);
|
|
}
|