Tư duy đúng khi sync từ theme khác
Đừng cố ép site cũ theo cấu trúc của Init Manga.
Việc đúng cần làm là: đọc dữ liệu theo cách site cũ đang lưu, sau đó chuyển đổi (mapping) lại sang định dạng mà Init Manga hiểu. API sync đóng vai trò như một lớp phiên dịch, giúp hai hệ thống khác cấu trúc vẫn nói chuyện được với nhau.
Mô hình dữ liệu minh họa
Trong ví dụ code bên dưới (bạn chỉ cần paste vào là dùng được), cấu trúc dữ liệu được giả định như sau:
- Truyện: mỗi truyện là một
category - Thông tin truyện: lưu bằng
term metavàACF - Chương: mỗi chương là một
post - Quan hệ truyện – chương: lấy từ category đầu tiên của post
Quan trọng: nếu site của bạn dùng custom taxonomy, custom post type, hoặc meta key khác, bạn chỉ cần chỉnh lại các phần mapping trong code, không cần thay đổi logic tổng thể.
Nguyên tắc của API đồng bộ
- Tách riêng endpoint truyện và endpoint chương
- Có phân trang để sync số lượng lớn dữ liệu
- Xác thực bằng header để tránh lộ key
- Dữ liệu trả về rõ ràng, tối giản, dễ debug
Code API đồng bộ
Đoạn code dưới đây có thể đặt vào mu-plugin hoặc plugin thường. Sau khi paste, API sẽ sẵn sàng để Init Manga Sync gọi tới.
<?php
/**
* Old Manga Sync API (mu-plugin)
*
* Cung cấp 2 endpoint REST API để đồng bộ dữ liệu sang Init Manga:
* - GET /wp-json/oldmanga/v1/stories
* - GET /wp-json/oldmanga/v1/chapters
*
* Phân trang: 10 truyện/trang, 100 chương/trang
* Bảo mật: API_PASSWORD (thay đổi trong code)
*/
if ( ! defined('ABSPATH') ) exit;
class OldManga_Sync_API {
// ===== QUAN TRỌNG: Thay đổi password này =====
// Có thể dùng Init Password Generator (https://inithtml.com/init-password-generator/)
const API_PASSWORD = 'STRONG_API_KEY';
const STORIES_PER_PAGE = 10;
const CHAPTERS_PER_PAGE = 100;
const CHAP_SLUG_PREFIX = 'chap';
public static function init() {
add_action('rest_api_init', [__CLASS__, 'register_routes']);
}
public static function register_routes() {
register_rest_route('oldmanga/v1', '/stories', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_stories'],
'permission_callback' => [__CLASS__, 'check_password'],
]);
register_rest_route('oldmanga/v1', '/chapters', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_chapters'],
'permission_callback' => [__CLASS__, 'check_password'],
]);
}
/**
* Kiểm tra password
*/
public static function check_password($request) {
$key = $request->get_header('X-Init-Manga-Key');
$password = '';
if (isset($key)) {
$password = $key;
}
if (empty($password) || !hash_equals(self::API_PASSWORD, $password)) {
return new WP_Error(
'invalid_api_key',
'Invalid or missing API key',
['status' => 401]
);
}
return true;
}
/**
* GET /wp-json/oldmanga/v1/stories
*/
public static function get_stories($request) {
$page = max(1, (int) $request->get_param('page'));
$offset = ($page - 1) * self::STORIES_PER_PAGE;
// Lấy categories, sắp xếp term_id DESC
$cats = get_terms([
'taxonomy' => 'category',
'hide_empty' => true,
'orderby' => 'term_id',
'order' => 'DESC',
'number' => self::STORIES_PER_PAGE,
'offset' => $offset,
]);
// Đếm tổng
$total = wp_count_terms('category', ['hide_empty' => true]);
$total_pages = ceil($total / self::STORIES_PER_PAGE);
// Build dữ liệu
$items = [];
foreach ($cats as $cat) {
$items[] = self::build_story($cat);
}
return new WP_REST_Response([
'success' => true,
'page' => $page,
'per_page' => self::STORIES_PER_PAGE,
'total' => $total,
'total_pages' => $total_pages,
'items' => $items,
], 200);
}
/**
* GET /wp-json/oldmanga/v1/chapters
*/
public static function get_chapters($request) {
$page = max(1, (int) $request->get_param('page'));
$offset = ($page - 1) * self::CHAPTERS_PER_PAGE;
// Lấy posts, sắp xếp ID DESC
$query = new WP_Query([
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => self::CHAPTERS_PER_PAGE,
'offset' => $offset,
]);
$total = $query->found_posts;
$total_pages = ceil($total / self::CHAPTERS_PER_PAGE);
// Build chapters
$chapters = [];
foreach ($query->posts as $post) {
// Lấy category đầu tiên
$cats = wp_get_post_categories($post->ID, ['fields' => 'all']);
$story_slug = '';
$story_id = 0;
$story_name = '';
if (!empty($cats)) {
$cat = $cats[0];
$story_slug = $cat->slug;
$story_id = $cat->term_id;
$story_name = html_entity_decode($cat->name, ENT_QUOTES, 'UTF-8');
}
// Extract số chapter từ title, ưu tiên sau từ "Chap"
$number = self::extract_chapter_number($post->post_title);
$chapters[] = [
'id' => $post->ID,
'story_id' => $story_id,
'story_slug' => $story_slug,
'story_name' => $story_name,
'title' => '',
'number' => (int) $number,
'slug' => self::CHAP_SLUG_PREFIX . '-' . (int) $number,
'content' => $post->post_content,
'date' => $post->post_date,
'modified' => $post->post_modified,
];
}
return new WP_REST_Response([
'success' => true,
'page' => $page,
'per_page' => self::CHAPTERS_PER_PAGE,
'total' => $total,
'total_pages' => $total_pages,
'items' => $chapters,
], 200);
}
/**
* Build dữ liệu truyện từ category
*/
protected static function build_story(WP_Term $cat) {
$alt_title = (string) get_term_meta($cat->term_id, 'comic_othername', true);
$status_vi = (string) get_term_meta($cat->term_id, 'comic_status', true);
// Ảnh bìa
$cover_id = (int) get_term_meta($cat->term_id, 'comic_image', true);
$featured = ['url' => '', 'alt' => '', 'caption' => ''];
if ( $cover_id ) {
$url = wp_get_attachment_url($cover_id);
if ($url) {
$featured['url'] = $url;
$featured['alt'] = get_post_meta($cover_id, '_wp_attachment_image_alt', true) ?: '';
$featured['caption'] = wp_get_attachment_caption($cover_id) ?: '';
}
}
// Taxonomies
$genres = self::resolve_genres($cat->term_id);
$teams = self::resolve_teams($cat->term_id);
$authors = self::resolve_authors($cat->term_id);
$content = term_description($cat->term_id, 'category') ?: '';
// Meta
$meta = [
'type' => 'comic',
'webtoon_support' => '1',
'status' => self::map_status($status_vi),
'alt_title' => $alt_title,
];
$taxonomies = ['genre' => $genres];
if ($authors) $taxonomies['author_tax'] = $authors;
if ($teams) $taxonomies['team'] = $teams;
return [
'id' => $cat->term_id,
'title' => html_entity_decode($cat->name, ENT_QUOTES, 'UTF-8'),
'slug' => $cat->slug,
'meta' => $meta,
'taxonomies' => $taxonomies,
'content' => $content,
'featured' => $featured,
];
}
protected static function map_status($status_vi) {
$s = preg_replace('/\s+/u', ' ', trim(wp_strip_all_tags($status_vi)));
$map = [
'Đang tiến hành' => 'ongoing',
'Trọn bộ' => 'completed',
'Tạm ngưng' => 'dropped',
];
return $map[$s] ?? 'ongoing';
}
protected static function resolve_authors($term_id) {
$raw = (string) get_term_meta($term_id, 'comic_author', true);
if (!$raw) return [];
$parts = array_map('trim', explode(',', str_replace(';', ',', $raw)));
$out = [];
$seen = [];
foreach ($parts as $name) {
if (!$name) continue;
$slug = sanitize_title($name);
if (!$slug || isset($seen[$slug])) continue;
$seen[$slug] = 1;
$out[] = ['name' => $name, 'slug' => $slug, 'taxonomy' => 'author_tax'];
}
return $out;
}
protected static function resolve_genres($term_id) {
$raw = get_term_meta($term_id, 'comic_tags', true);
if (is_string($raw)) {
$maybe = @maybe_unserialize($raw);
if ($maybe !== false || $raw === 'b:0;') $raw = $maybe;
}
$ids = [];
if (is_array($raw)) {
foreach ($raw as $v) if (is_numeric($v)) $ids[] = (int)$v;
} elseif (is_numeric($raw)) {
$ids[] = (int)$raw;
}
$genres = [];
$seen = [];
foreach ($ids as $id) {
$t = get_term($id, 'post_tag');
if ($t && !is_wp_error($t)) {
if (!isset($seen[$t->slug])) {
$genres[] = ['name' => $t->name, 'slug' => $t->slug, 'taxonomy' => 'genre'];
$seen[$t->slug] = 1;
}
}
}
return $genres;
}
protected static function resolve_teams($term_id) {
// ACF
if (function_exists('get_field')) {
$acf_val = get_field('comic_team', 'category_' . $term_id);
$teams = self::normalize_teams($acf_val);
if ($teams) return $teams;
}
// Fallback meta
$raw = get_term_meta($term_id, 'comic_team', true);
if (is_string($raw)) {
$maybe = @maybe_unserialize($raw);
if ($maybe !== false || $raw === 'b:0;') $raw = $maybe;
}
return self::normalize_teams($raw);
}
protected static function normalize_teams($input) {
$ids = [];
if (is_array($input)) {
foreach ($input as $v) {
if (is_object($v) && isset($v->ID)) $ids[] = (int)$v->ID;
elseif (is_array($v) && isset($v['ID'])) $ids[] = (int)$v['ID'];
elseif (is_numeric($v)) $ids[] = (int)$v;
}
} elseif (is_object($input) && isset($input->ID)) {
$ids[] = (int)$input->ID;
} elseif (is_numeric($input)) {
$ids[] = (int)$input;
}
$ids = array_unique(array_filter($ids));
$out = [];
$seen = [];
foreach ($ids as $id) {
$p = get_post($id);
if ($p && !isset($seen[$p->post_name])) {
$out[] = ['name' => get_the_title($p), 'slug' => $p->post_name, 'taxonomy' => 'team'];
$seen[$p->post_name] = 1;
}
}
return $out;
}
/**
* Extract số chapter từ tiêu đề, ưu tiên sau từ "Chap"
* VD: "One Piece Chap 1234" -> 1234
* "Chap 56.5" -> 56.5
* "Chapter 789" -> 789
*/
protected static function extract_chapter_number($title) {
// Chuẩn hóa dấu gạch ngang Unicode
$title = str_replace(["–", "—", "−"], "-", $title);
// Chap | Chương | Chapter
if (preg_match('/\b(chap|chương|chapter)\b[^0-9]*?(\d+(?:[.,]\d+)?)/iu', $title, $m)) {
return (float) str_replace(',', '.', $m[2]);
}
return 0;
}
}
OldManga_Sync_API::init();
Sau khi paste, bạn có thể điều chỉnh nhanh các phần sau cho phù hợp với site của mình:
- Truyện không phải category → đổi
get_terms()sang taxonomy bạn đang dùng - Chương không phải post → đổi
post_typetrongWP_Query - Meta khác key → đổi các
get_term_meta()hoặcget_post_meta() - Không dùng ACF → bỏ hoặc thay thế các đoạn
get_field()
Cách Init Manga Sync gọi API
Init Manga Sync sẽ gọi API bằng header xác thực, không truyền key qua URL:
X-Init-Manga-Key: your-secret-key
Việc này giúp API an toàn hơn, không bị lộ key qua log server, CDN hay cache.
Phần bắt buộc phải tự chỉnh: mapping dữ liệu
Không có hai website nào lưu dữ liệu giống nhau hoàn toàn.
Bạn cần đảm bảo rằng API trả về đúng các thông tin mà Init Manga cần, bao gồm:
- ID và slug truyện
- Tên truyện
- Danh sách taxonomy (genre, author, team…)
- Nội dung truyện
- Danh sách chương kèm số chapter
Miễn là các dữ liệu này đúng, Init Manga không quan tâm bạn lấy chúng từ đâu.
Những lỗi phổ biến khi sync
- Query quá nhiều dữ liệu trong một request
- Không phân trang dẫn tới timeout
- Slug và taxonomy không được chuẩn hóa
- Không xử lý dữ liệu thiếu hoặc sai định dạng
Hãy coi API sync là lớp trung gian thuần dữ liệu, càng đơn giản thì càng dễ bảo trì.
Kết luận
Việc đồng bộ dữ liệu từ trang dùng theme khác vào Init Manga không hề phức tạp nếu bạn tiếp cận đúng hướng. Đoạn code API chỉ là khung dùng chung, còn phần quan trọng nhất là bạn hiểu dữ liệu site cũ và map lại cho chính xác.
Làm đúng ngay từ đầu, bạn có thể gom nhiều site khác nhau về Init Manga một cách gọn gàng, an toàn và bền vững lâu dài.
Bình luận