max(1, $maxPages)) { break; } $response = Http::timeout(20) ->retry(2, 300) ->acceptJson() ->get($url, [ 'per_page' => min(max($perPage, 1), 100), 'page' => $page, '_embed' => 1, 'status' => 'publish', ]); $response->throw(); /** @var array> $items */ $items = $response->json() ?? []; if (empty($items)) { break; } foreach ($items as $item) { $total++; $result = $this->syncItem($item); match ($result) { 'created' => $created++, 'updated' => $updated++, default => $skipped++, }; } if (count($items) < $perPage) { break; } $page++; } return compact('created', 'updated', 'skipped', 'total'); } private function syncItem(array $item): string { $title = trim((string) data_get($item, 'title.rendered', '')); $content = (string) data_get($item, 'content.rendered', ''); $slug = trim((string) data_get($item, 'slug', '')); if ($title === '' || $content === '') { return 'skipped'; } $category = $this->resolveCategory($item); $existing = Post::withTrashed()->where('slug', $slug)->first(); $thumbnail = $this->resolveFeaturedImage($item); $excerpt = trim(strip_tags((string) data_get($item, 'excerpt.rendered', ''))); $publishedAt = data_get($item, 'date_gmt') ?: data_get($item, 'date'); $payload = [ 'post_category_id' => $category->id, 'title' => html_entity_decode(strip_tags($title), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'excerpt' => $excerpt !== '' ? html_entity_decode($excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8') : null, 'content' => $content, 'thumbnail' => $thumbnail, 'status' => 'published', 'is_featured' => false, 'reading_time' => $this->estimateReadingTime($content), 'meta_title' => html_entity_decode(strip_tags($title), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'meta_description' => $excerpt !== '' ? html_entity_decode($excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8') : null, 'published_at' => $publishedAt ? Carbon::parse($publishedAt) : now(), ]; if ($existing) { $existing->restore(); $existing->update($payload); return 'updated'; } Post::create($payload + [ 'slug' => GeneratesUniqueSlug::forModel(Post::class, $slug !== '' ? $slug : $title), 'view_count' => 0, ]); return 'created'; } private function resolveCategory(array $item): PostCategory { $termGroups = (array) data_get($item, '_embedded.wp:term', []); $terms = is_array($termGroups[0] ?? null) ? $termGroups[0] : []; $firstTerm = Arr::first($terms); $categoryName = trim((string) data_get($firstTerm, 'name', 'Imported Docs')); $categorySlug = trim((string) data_get($firstTerm, 'slug', 'imported-docs')); return PostCategory::firstOrCreate( ['slug' => $categorySlug !== '' ? $categorySlug : 'imported-docs'], [ 'name' => $categoryName !== '' ? $categoryName : 'Imported Docs', 'description' => 'Danh mục đồng bộ từ WordPress Docs.', 'status' => 'active', 'sort_order' => 999, ] ); } private function resolveFeaturedImage(array $item): ?string { return data_get($item, '_embedded.wp:featuredmedia.0.source_url') ?: data_get($item, '_embedded.wp:featuredmedia.0.media_details.sizes.full.source_url') ?: null; } private function estimateReadingTime(string $html): int { $wordCount = str_word_count(Str::of(strip_tags($html))->squish()->value()); return max(1, (int) ceil($wordCount / 200)); } }