// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\blog\controller; use addon\blog\model\BlogArticle; use addon\blog\model\BlogCategory; use think\facade\Request; use ywxapp\controller\FrontendBase; /** * 站点地图(前台 SEO 用) * * 收录范围:首页 / 分类 / 已发布且审核通过的文章(草稿、待审、驳回均不收录) * * 文章 ≤ 5000:sitemap.xml 直接输出 urlset * 文章 > 5000:sitemap.xml 输出 sitemap index,文章拆到 sitemap-.xml */ class Sitemap extends FrontendBase { /** 单个 sitemap 文件最多 URL 数 */ const MAX_PER_FILE = 5000; public function index() { $base = Request::domain(); $total = $this->query()->count(); $static = [ ['loc' => $base . (string) url('blog/index/index'), 'priority' => '1.0', 'lastmod' => date('Y-m-d')], ]; foreach (BlogCategory::where('status', 1)->select() as $c) { $static[] = [ 'loc' => $base . (string) url('blog/index/category', ['id' => $c->id]), 'priority' => '0.6', ]; } if ($total > self::MAX_PER_FILE) { $pages = (int) ceil($total / self::MAX_PER_FILE); $maps = $static; for ($i = 1; $i <= $pages; $i++) { // 分片 URL 必须手工拼接:url() 会追加 .html 后缀,不能用于 .xml $maps[] = [ 'loc' => $base . '/blog/sitemap-' . $i . '.xml', 'lastmod' => date('Y-m-d'), 'priority' => '0.8', ]; } return response(self::renderIndex($maps), 200, ['Content-Type' => 'application/xml; charset=utf-8']); } $items = $static; foreach ($this->query()->field('id,update_at')->order('create_at', 'desc')->select() as $a) { $items[] = [ 'loc' => $base . (string) url('blog/article/detail', ['id' => $a->id]), 'lastmod' => date('Y-m-d', (int) $a->update_at ?: time()), 'priority' => '0.8', ]; } return response(self::renderUrlset($items), 200, ['Content-Type' => 'application/xml; charset=utf-8']); } /** * 文章分片:/blog/sitemap-.xml */ public function chunk($page = 1) { $page = max(1, (int) $page); $base = Request::domain(); $total = $this->query()->count(); $pages = max(1, (int) ceil($total / self::MAX_PER_FILE)); if ($page > $pages) { return response(self::renderUrlset([]), 200, ['Content-Type' => 'application/xml; charset=utf-8']); } $articles = $this->query() ->field('id,update_at') ->order('create_at', 'desc') ->page($page, self::MAX_PER_FILE) ->select(); $items = []; foreach ($articles as $a) { $items[] = [ 'loc' => $base . (string) url('blog/article/detail', ['id' => $a->id]), 'lastmod' => date('Y-m-d', (int) $a->update_at ?: time()), 'priority' => '0.8', ]; } return response(self::renderUrlset($items), 200, ['Content-Type' => 'application/xml; charset=utf-8']); } /** * 对外可见文章的查询条件:已发布 + 审核通过 + 未删除 */ private function query() { return BlogArticle::where('status', 1)->where('audit_status', 2); } private static function renderUrlset(array $items): string { $xml = '' . "\n" . '' . "\n"; foreach ($items as $item) { $xml .= " \n"; $xml .= ' ' . self::esc($item['loc'] ?? '') . "\n"; if (!empty($item['lastmod'])) { $xml .= ' ' . $item['lastmod'] . "\n"; } $xml .= ' ' . ($item['priority'] ?? '0.5') . "\n"; $xml .= " \n"; } return $xml . ''; } private static function renderIndex(array $items): string { $xml = '' . "\n" . '' . "\n"; foreach ($items as $item) { $xml .= " \n"; $xml .= ' ' . self::esc($item['loc'] ?? '') . "\n"; if (!empty($item['lastmod'])) { $xml .= ' ' . $item['lastmod'] . "\n"; } $xml .= " \n"; } return $xml . ''; } private static function esc(string $value): string { return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8'); } }