Explorar el Código

sitemap 동적 생성 및 색인 제어 추가

- SitemapController: /sitemap.xml 을 Request.Categories/Brand enum 에서 생성.
  직전 커밋(48e19ba)에서 정적 wwwroot/sitemap.xml 만 먼저 삭제되었으므로 이 커밋과 함께 배포해야 한다.
  UseStaticFiles 가 라우팅보다 먼저 실행되어 정적 파일이 있으면 컨트롤러에 도달하지 못한다.
  URL 집합은 기존 31개와 완전히 동일함을 확인.
- _Layout: canonical 링크 추가. 쿼리스트링이 붙은 주소의 중복 색인을 막는다.
  ViewData["NoIndex"] 인 페이지는 canonical 대신 noindex 를 내보낸다.
- Search/Error 뷰: NoIndex 표시 (검색은 키워드마다 주소가 달라지고 keyword 없이는 400)
- robots.txt: Disallow: /search 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KIM-JINO5 hace 2 semanas
padre
commit
04c8517784

+ 72 - 0
Controllers/SitemapController.cs

@@ -0,0 +1,72 @@
+using System.Text;
+using Microsoft.AspNetCore.Mvc;
+using CoupangRequest = goods.Models.Coupang.Request;
+
+namespace goods.Controllers
+{
+    // sitemap.xml 생성
+    // 카테고리/PL 목록은 Request.Categories, Request.Brand enum 에서 그대로 읽는다.
+    // HomeController 가 Enum.IsDefined 로 유효성을 검사하므로, enum 에 없는 ID 는 애초에 400 이 되고
+    // enum 에 추가된 ID 는 별도 작업 없이 sitemap 에 함께 노출된다.
+    public class SitemapController : Controller
+    {
+        // 사이트 정식 주소. 리버스 프록시 뒤에서는 Request.Scheme 이 http 로 들어올 수 있어
+        // 설정값(Site:BaseUrl)을 우선 사용하고, 없으면 운영 도메인을 쓴다.
+        private const string DefaultBaseUrl = "https://goods.web.or.kr";
+
+        private readonly IConfiguration _configuration;
+
+        public SitemapController(IConfiguration configuration)
+        {
+            _configuration = configuration;
+        }
+
+        [HttpGet("/sitemap.xml")]
+        [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
+        public IActionResult Index()
+        {
+            var baseUrl = (_configuration["Site:BaseUrl"] ?? DefaultBaseUrl).TrimEnd('/');
+
+            // 목록 페이지는 쿠팡 API 응답에 따라 매일 내용이 바뀌므로 날짜 단위로만 표기한다.
+            var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
+
+            var builder = new StringBuilder();
+            builder.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
+            builder.AppendLine(@"<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">");
+
+            // 홈
+            AppendUrl(builder, $"{baseUrl}/", today, "daily", "1.0");
+
+            // 골드박스, 이벤트
+            AppendUrl(builder, $"{baseUrl}/GoldBox", today, "daily", "0.8");
+            AppendUrl(builder, $"{baseUrl}/Event", today, "weekly", "0.8");
+
+            // 카테고리별 베스트 상품
+            foreach (var id in Enum.GetValues<CoupangRequest.Categories>().Select(value => (int)value).Distinct().OrderBy(id => id))
+            {
+                AppendUrl(builder, $"{baseUrl}/category/{id}", today, "daily", "0.8");
+            }
+
+            // 쿠팡PL 브랜드별 상품
+            foreach (var id in Enum.GetValues<CoupangRequest.Brand>().Select(value => (int)value).Distinct().OrderBy(id => id))
+            {
+                AppendUrl(builder, $"{baseUrl}/pl/{id}", today, "daily", "0.8");
+            }
+
+            builder.AppendLine("</urlset>");
+
+            // 검색(/search)은 keyword 없이는 400 을 반환하는 파라미터 페이지라 제외한다.
+            return Content(builder.ToString(), "application/xml", Encoding.UTF8);
+        }
+
+        private static void AppendUrl(StringBuilder builder, string location, string lastModified, string changeFrequency, string priority)
+        {
+            builder.AppendLine("  <url>");
+            builder.AppendLine($"    <loc>{location}</loc>");
+            builder.AppendLine($"    <lastmod>{lastModified}</lastmod>");
+            builder.AppendLine($"    <changefreq>{changeFrequency}</changefreq>");
+            builder.AppendLine($"    <priority>{priority}</priority>");
+            builder.AppendLine("  </url>");
+        }
+    }
+}

+ 3 - 0
Views/Home/Search.cshtml

@@ -2,6 +2,9 @@
 
 @{
     ViewData["Title"] = "상품 검색";
+
+    // 검색 결과는 키워드마다 주소가 달라지고 keyword 없이는 400 이므로 색인 대상에서 제외한다.
+    ViewData["NoIndex"] = true;
 }
 
 <div id="products" class="container-2xl mx-auto">

+ 3 - 0
Views/Shared/Error.cshtml

@@ -1,6 +1,9 @@
 @model ErrorViewModel
 @{
     ViewData["Title"] = "Error";
+
+    // 오류 페이지는 색인 대상에서 제외한다.
+    ViewData["NoIndex"] = true;
 }
 
 <h1 class="text-danger">Error.</h1>

+ 18 - 1
Views/Shared/_Layout.cshtml

@@ -1,4 +1,5 @@
-<!DOCTYPE html>
+@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
+<!DOCTYPE html>
 <html lang="ko">
 <head>
     <meta charset="utf-8" />
@@ -18,6 +19,22 @@
     <meta name="author" content="김진오" />
     <meta name="naver-site-verification" content="534321e6b7402169fc787ae2f795049aef0232e3" />
 
+    @*
+        정규 주소(canonical). 쿼리스트링이 붙은 주소가 별도 페이지로 색인되는 것을 막는다.
+        리버스 프록시 뒤에서는 Request.Scheme 이 http 로 들어올 수 있어 SitemapController 와 동일하게
+        설정값(Site:BaseUrl)을 우선 사용한다.
+        색인 대상이 아닌 페이지(검색 결과, 오류)는 ViewData["NoIndex"] = true 로 표시해 canonical 대신 noindex 를 내보낸다.
+    *@
+    @if (ViewData["NoIndex"] is true)
+    {
+        <meta name="robots" content="noindex, follow" />
+    }
+    else
+    {
+        var canonicalBase = (Configuration["Site:BaseUrl"] ?? "https://goods.web.or.kr").TrimEnd('/');
+        <link rel="canonical" href="@(canonicalBase + Context.Request.Path)" />
+    }
+
     <link rel="stylesheet" href="~/css/tailwind.output.css" asp-append-version="true" />
     <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
 

+ 4 - 2
wwwroot/robots.txt

@@ -1,3 +1,5 @@
 User-agent: *
-Disallow:
-Sitemap: https://goods.web.or.kr/sitemap.xml
+Allow: /
+Disallow: /search
+
+Sitemap: https://goods.web.or.kr/sitemap.xml