Files
evus.acvisa.com/app/home/controller/Seo.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',
]);
}
}