SitemapController.php 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Support\Facades\Cache;
  4. use Illuminate\Support\Facades\DB;
  5. /**
  6. * sitemap.xml 생성
  7. *
  8. * 기존에는 public/sitemap.xml 정적 파일을 손으로 갱신했는데, 목록 페이지 14건만 담겨 있고
  9. * 개별 게시글이 한 건도 없었다(lastmod 도 2026-01-10 에 멈춰 있었다).
  10. * 블로그에서 색인 가치가 가장 높은 것이 게시글 본문이므로 DB 에서 직접 생성한다.
  11. *
  12. * 공개 판정 조건은 Board 모델의 기존 공개 목록 쿼리와 동일하게 맞춘다.
  13. * (BRD.is_display = 1, PST.is_delete = 0, PST.is_secret = 0, PST.is_personal = 0)
  14. */
  15. class SitemapController extends Controller
  16. {
  17. // 게시글이 늘어나도 매 요청마다 전체 조회를 하지 않도록 캐시한다.
  18. private const CACHE_KEY = 'sitemap.xml';
  19. private const CACHE_SECONDS = 3600;
  20. public function index()
  21. {
  22. $xml = Cache::remember(self::CACHE_KEY, self::CACHE_SECONDS, function () {
  23. return $this->build();
  24. });
  25. return response($xml, 200)->header('Content-Type', 'application/xml; charset=utf-8');
  26. }
  27. private function build(): string
  28. {
  29. // 레이아웃의 canonical(url()->current())과 같은 방식으로 요청 기준 주소를 쓴다.
  30. // config('app.url') 은 환경파일 값에 좌우되어(로컬은 local-blog.web.or.kr) 어긋날 수 있다.
  31. $baseUrl = rtrim(url('/'), '/');
  32. $today = date('Y-m-d');
  33. $xml = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
  34. $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . PHP_EOL;
  35. // 고정 페이지
  36. $xml .= $this->url("{$baseUrl}/", $today, 'daily', '1.0');
  37. $xml .= $this->url("{$baseUrl}/recently", $today, 'daily', '0.8');
  38. $xml .= $this->url("{$baseUrl}/tag", $today, 'daily', '0.6');
  39. // 문서 페이지. tb_document 에 노출 여부 컬럼을 확인하지 못해 현재 공개된 두 건만 둔다.
  40. $xml .= $this->url("{$baseUrl}/document/profile", $today, 'monthly', '0.5');
  41. $xml .= $this->url("{$baseUrl}/document/proposal", $today, 'monthly', '0.5');
  42. // 게시판 목록
  43. $boards = DB::table('tb_board')
  44. ->where('is_display', '=', 1)
  45. ->orderBy('sort')
  46. ->pluck('code');
  47. foreach ($boards as $code) {
  48. $xml .= $this->url("{$baseUrl}/board/{$code}", $today, 'daily', '0.8');
  49. }
  50. // 게시글. 공지/전체공지도 실제로 열리는 페이지이므로 함께 넣는다.
  51. DB::table('tb_post')
  52. ->join('tb_board', 'tb_board.id', '=', 'tb_post.board_id')
  53. ->where('tb_board.is_display', '=', 1)
  54. ->where('tb_post.is_delete', '=', 0)
  55. ->where('tb_post.is_secret', '=', 0)
  56. ->where('tb_post.is_personal', '=', 0)
  57. ->select('tb_board.code', 'tb_post.id', 'tb_post.updated_at', 'tb_post.created_at')
  58. ->orderBy('tb_post.id')
  59. ->chunk(1000, function ($posts) use (&$xml, $baseUrl, $today) {
  60. foreach ($posts as $post) {
  61. $modified = $post->updated_at ?: $post->created_at;
  62. $lastModified = $modified ? date('Y-m-d', strtotime($modified)) : $today;
  63. $xml .= $this->url("{$baseUrl}/board/{$post->code}/{$post->id}", $lastModified, 'weekly', '0.6');
  64. }
  65. });
  66. $xml .= '</urlset>' . PHP_EOL;
  67. return $xml;
  68. }
  69. private function url(string $location, string $lastModified, string $changeFrequency, string $priority): string
  70. {
  71. return ' <url>' . PHP_EOL
  72. . ' <loc>' . htmlspecialchars($location, ENT_XML1) . '</loc>' . PHP_EOL
  73. . ' <lastmod>' . $lastModified . '</lastmod>' . PHP_EOL
  74. . ' <changefreq>' . $changeFrequency . '</changefreq>' . PHP_EOL
  75. . ' <priority>' . $priority . '</priority>' . PHP_EOL
  76. . ' </url>' . PHP_EOL;
  77. }
  78. }