228 lines
7.4 KiB
PHP
228 lines
7.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace app\home\controller;
|
|
|
|
use api\Httpcurl;
|
|
use app\service\SeoMetaService;
|
|
use app\service\SitemapService;
|
|
use think\facade\Log;
|
|
use Throwable;
|
|
|
|
final class Seo
|
|
{
|
|
public function sitemap()
|
|
{
|
|
$seoConfig = (array) config('seo');
|
|
$sitemapConfig = (array) ($seoConfig['sitemap'] ?? []);
|
|
$baseUrl = (new SeoMetaService($seoConfig))->baseUrl();
|
|
if ($baseUrl === '') {
|
|
return response(
|
|
'SEO_BASE_URL is not configured.',
|
|
503,
|
|
['Content-Type' => 'text/plain; charset=UTF-8']
|
|
);
|
|
}
|
|
|
|
$cacheSeconds = max(60, (int) ($sitemapConfig['cache_seconds'] ?? 3600));
|
|
$cacheDirectory = runtime_path('seo');
|
|
$cacheFile = $cacheDirectory
|
|
. 'sitemap-'
|
|
. substr(hash('sha256', $baseUrl), 0, 16)
|
|
. '.xml';
|
|
$cachedXml = $this->readCache($cacheFile);
|
|
$cacheTime = is_file($cacheFile) ? filemtime($cacheFile) : false;
|
|
if (
|
|
$cachedXml !== ''
|
|
&& $cacheTime !== false
|
|
&& $cacheTime + $cacheSeconds > time()
|
|
) {
|
|
return $this->xmlResponse($cachedXml, $cacheSeconds);
|
|
}
|
|
|
|
$service = new SitemapService($baseUrl);
|
|
$entries = $this->staticEntries($seoConfig);
|
|
[$newsEntries, $newsComplete] = $this->newsEntries($sitemapConfig);
|
|
|
|
if (!$newsComplete && $cachedXml !== '') {
|
|
return $this->xmlResponse($cachedXml, 300);
|
|
}
|
|
|
|
$xml = $service->render(array_merge($entries, $newsEntries));
|
|
if ($newsComplete) {
|
|
$this->writeCache($cacheDirectory, $cacheFile, $xml);
|
|
}
|
|
|
|
return $this->xmlResponse($xml, $newsComplete ? $cacheSeconds : 300);
|
|
}
|
|
|
|
public function robots()
|
|
{
|
|
$seoConfig = (array) config('seo');
|
|
$baseUrl = (new SeoMetaService($seoConfig))->baseUrl();
|
|
$service = new SitemapService($baseUrl);
|
|
$robots = $service->renderRobots($this->disallowPaths($seoConfig));
|
|
|
|
return response($robots, 200, [
|
|
'Content-Type' => 'text/plain; charset=UTF-8',
|
|
'Cache-Control' => 'public, max-age=3600',
|
|
]);
|
|
}
|
|
|
|
private function staticEntries(array $seoConfig): array
|
|
{
|
|
$entries = [];
|
|
foreach ((array) ($seoConfig['pages'] ?? []) as $route => $page) {
|
|
$page = (array) $page;
|
|
if (!str_starts_with((string) $route, 'home/')) {
|
|
continue;
|
|
}
|
|
if (!str_starts_with((string) ($page['robots'] ?? ''), 'index')) {
|
|
continue;
|
|
}
|
|
if ($route === 'home/news/detail') {
|
|
continue;
|
|
}
|
|
|
|
$path = (string) ($page['canonical_path'] ?? '');
|
|
if ($path !== '') {
|
|
$entries[] = ['path' => $path];
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
private function newsEntries(array $sitemapConfig): array
|
|
{
|
|
$url = trim((string) ($sitemapConfig['news_list_url'] ?? ''));
|
|
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
|
|
Log::warning('SEO sitemap news URL is invalid.');
|
|
return [[], false];
|
|
}
|
|
|
|
$pageSize = max(1, min(500, (int) ($sitemapConfig['news_page_size'] ?? 100)));
|
|
$maxPages = max(1, min(500, (int) ($sitemapConfig['max_news_pages'] ?? 100)));
|
|
$entries = [];
|
|
$fetchedItems = 0;
|
|
|
|
for ($page = 1; $page <= $maxPages; $page++) {
|
|
try {
|
|
$response = Httpcurl::request($url, 'post', [
|
|
'model' => 'article',
|
|
'source_code' => 'cn',
|
|
'page' => $page,
|
|
'size' => $pageSize,
|
|
], [], 5);
|
|
} catch (Throwable $exception) {
|
|
Log::warning('SEO sitemap news request failed: ' . $exception->getMessage());
|
|
return [$entries, false];
|
|
}
|
|
|
|
if (!is_array($response) || !isset($response[0]) || !empty($response[3])) {
|
|
Log::warning('SEO sitemap news request returned an invalid response.');
|
|
return [$entries, false];
|
|
}
|
|
|
|
$statusCode = (int) ($response[2]['http_code'] ?? 0);
|
|
if ($statusCode < 200 || $statusCode >= 300) {
|
|
Log::warning('SEO sitemap news request returned an invalid HTTP status.');
|
|
return [$entries, false];
|
|
}
|
|
|
|
$result = json_decode((string) $response[0], true);
|
|
if (
|
|
!is_array($result)
|
|
|| (int) ($result['code'] ?? 0) !== 1
|
|
|| !is_array($result['data'] ?? null)
|
|
) {
|
|
Log::warning('SEO sitemap news response could not be decoded.');
|
|
return [$entries, false];
|
|
}
|
|
|
|
$items = $result['data'];
|
|
$fetchedItems += count($items);
|
|
foreach ($items as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$id = filter_var($item['id'] ?? null, FILTER_VALIDATE_INT, [
|
|
'options' => ['min_range' => 1],
|
|
]);
|
|
if ($id === false) {
|
|
continue;
|
|
}
|
|
|
|
$entries[] = [
|
|
'path' => '/home/news/detail',
|
|
'query' => ['id' => $id],
|
|
'lastmod' => $this->articleDate((string) ($item['create_time'] ?? '')),
|
|
];
|
|
}
|
|
|
|
$total = max(0, (int) ($result['count'] ?? 0));
|
|
if (
|
|
$items === []
|
|
|| ($total > 0 && $fetchedItems >= $total)
|
|
|| ($total === 0 && count($items) < $pageSize)
|
|
) {
|
|
return [$entries, true];
|
|
}
|
|
}
|
|
|
|
Log::warning('SEO sitemap news pagination reached the configured page limit.');
|
|
return [$entries, false];
|
|
}
|
|
|
|
private function disallowPaths(array $seoConfig): array
|
|
{
|
|
$paths = [];
|
|
foreach ((array) ($seoConfig['noindex_controllers'] ?? []) as $controller) {
|
|
$controller = trim((string) $controller, '/');
|
|
if ($controller !== '') {
|
|
$paths[] = '/home/' . $controller . '/';
|
|
}
|
|
}
|
|
|
|
return $paths;
|
|
}
|
|
|
|
private function articleDate(string $date): string
|
|
{
|
|
$timestamp = strtotime($date);
|
|
return $timestamp === false ? '' : date('Y-m-d', $timestamp);
|
|
}
|
|
|
|
private function readCache(string $cacheFile): string
|
|
{
|
|
if (!is_file($cacheFile) || !is_readable($cacheFile)) {
|
|
return '';
|
|
}
|
|
|
|
$contents = file_get_contents($cacheFile);
|
|
return is_string($contents) ? $contents : '';
|
|
}
|
|
|
|
private function writeCache(string $directory, string $cacheFile, string $xml): void
|
|
{
|
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
|
Log::warning('SEO sitemap cache directory could not be created.');
|
|
return;
|
|
}
|
|
|
|
if (file_put_contents($cacheFile, $xml, LOCK_EX) === false) {
|
|
Log::warning('SEO sitemap cache could not be written.');
|
|
}
|
|
}
|
|
|
|
private function xmlResponse(string $xml, int $maxAge)
|
|
{
|
|
return response($xml, 200, [
|
|
'Content-Type' => 'application/xml; charset=UTF-8',
|
|
'Cache-Control' => 'public, max-age=' . max(0, $maxAge),
|
|
'X-Robots-Tag' => 'noindex',
|
|
]);
|
|
}
|
|
}
|