This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
@model TaxBaik.Web.Pages.Blog.BlogIndexModel
|
||||
@{
|
||||
ViewData["Title"] = "블로그 | 백원숙 세무회계";
|
||||
ViewData["Description"] = "세금 신고, 부동산 세금, 증여·상속 관련 최신 세무 정보와 절세 팁을 확인하세요.";
|
||||
var canonicalQuery = Model.SelectedCategoryId.HasValue
|
||||
? $"?categoryId={Model.SelectedCategoryId.Value}" + (Model.CurrentPage > 1 ? $"&page={Model.CurrentPage}" : string.Empty)
|
||||
: Model.CurrentPage > 1 ? $"?page={Model.CurrentPage}" : string.Empty;
|
||||
ViewData["CanonicalUrl"] = $"https://www.taxbaik.com/blog{canonicalQuery}";
|
||||
ViewData["OgUrl"] = ViewData["CanonicalUrl"];
|
||||
}
|
||||
|
||||
<div class="container py-5">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ViewData["Title"] = Model.Post?.SeoTitle ?? Model.Post?.Title;
|
||||
ViewData["Description"] = Model.Post?.SeoDescription ?? "";
|
||||
ViewData["OgImage"] = Model.Post?.ThumbnailUrl ?? "";
|
||||
var canonicalUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}/blog/{Model.Post?.Slug}";
|
||||
var canonicalUrl = $"https://www.taxbaik.com/blog/{Uri.EscapeDataString(Model.Post?.Slug ?? string.Empty)}";
|
||||
ViewData["CanonicalUrl"] = canonicalUrl;
|
||||
ViewData["OgUrl"] = canonicalUrl;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,6 @@
|
||||
@page "/feed.xml"
|
||||
@model TaxBaik.Web.Pages.RssModel
|
||||
@{
|
||||
Response.ContentType = "application/rss+xml; charset=utf-8";
|
||||
}@{
|
||||
var rssContent = new System.Text.StringBuilder();
|
||||
rssContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
rssContent.AppendLine("<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">");
|
||||
rssContent.AppendLine(" <channel>");
|
||||
rssContent.AppendLine(" <title>백원숙 세무회계 - 블로그</title>");
|
||||
rssContent.AppendLine(" <link>https://www.taxbaik.com</link>");
|
||||
rssContent.AppendLine(" <description>세무사 백원숙의 세금, 부동산, 가족자산 전문 블로그</description>");
|
||||
rssContent.AppendLine(" <language>ko-kr</language>");
|
||||
rssContent.AppendLine($" <lastBuildDate>{Model.LastBuildDate}</lastBuildDate>");
|
||||
rssContent.AppendLine(" <ttl>60</ttl>");
|
||||
|
||||
foreach (var post in Model.Posts)
|
||||
{
|
||||
rssContent.AppendLine(" <item>");
|
||||
rssContent.AppendLine($" <title>{System.Net.WebUtility.HtmlEncode(post.Title)}</title>");
|
||||
rssContent.AppendLine($" <link>https://www.taxbaik.com/blog/{post.Slug}</link>");
|
||||
rssContent.AppendLine($" <guid isPermaLink=\"true\">https://www.taxbaik.com/blog/{post.Slug}</guid>");
|
||||
rssContent.AppendLine($" <pubDate>{post.PublishedAt?.ToString("R")}</pubDate>");
|
||||
var desc = post.Content?.Substring(0, Math.Min(200, post.Content?.Length ?? 0)) ?? "";
|
||||
rssContent.AppendLine($" <description>{System.Net.WebUtility.HtmlEncode(desc)}</description>");
|
||||
if (!string.IsNullOrEmpty(post.Content))
|
||||
{
|
||||
rssContent.AppendLine($" <content:encoded><![CDATA[{post.Content}]]></content:encoded>");
|
||||
}
|
||||
rssContent.AppendLine(" </item>");
|
||||
}
|
||||
|
||||
rssContent.AppendLine(" </channel>");
|
||||
rssContent.AppendLine("</rss>");
|
||||
}@rssContent.ToString()
|
||||
Layout = null;
|
||||
Response.StatusCode = StatusCodes.Status308PermanentRedirect;
|
||||
Response.Headers.Location = "/rss.xml";
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
@page "/rss.xml"
|
||||
@model TaxBaik.Web.Pages.RssModel
|
||||
@{
|
||||
Layout = null;
|
||||
Response.ContentType = "application/rss+xml; charset=utf-8";
|
||||
}@{
|
||||
var rssContent = new System.Text.StringBuilder();
|
||||
rssContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
rssContent.AppendLine("<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">");
|
||||
rssContent.AppendLine("<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">");
|
||||
rssContent.AppendLine(" <channel>");
|
||||
rssContent.AppendLine(" <title>백원숙 세무회계 - 블로그</title>");
|
||||
rssContent.AppendLine(" <link>https://www.taxbaik.com</link>");
|
||||
rssContent.AppendLine(" <description>세무사 백원숙의 세금, 부동산, 가족자산 전문 블로그</description>");
|
||||
rssContent.AppendLine(" <language>ko-kr</language>");
|
||||
rssContent.AppendLine($" <lastBuildDate>{Model.LastBuildDate}</lastBuildDate>");
|
||||
rssContent.AppendLine(" <atom:link href=\"https://www.taxbaik.com/rss.xml\" rel=\"self\" type=\"application/rss+xml\" />");
|
||||
rssContent.AppendLine(" <ttl>60</ttl>");
|
||||
|
||||
foreach (var post in Model.Posts)
|
||||
@@ -25,11 +27,11 @@
|
||||
rssContent.AppendLine($" <description>{System.Net.WebUtility.HtmlEncode(desc)}</description>");
|
||||
if (!string.IsNullOrEmpty(post.Content))
|
||||
{
|
||||
rssContent.AppendLine($" <content:encoded><![CDATA[{post.Content}]]></content:encoded>");
|
||||
rssContent.AppendLine($" <content:encoded><![CDATA[{post.Content.Replace("]]>", "]]]]><![CDATA[>")}]]></content:encoded>");
|
||||
}
|
||||
rssContent.AppendLine(" </item>");
|
||||
}
|
||||
|
||||
rssContent.AppendLine(" </channel>");
|
||||
rssContent.AppendLine("</rss>");
|
||||
}@rssContent.ToString()
|
||||
}@Html.Raw(rssContent.ToString())
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
@page "/sitemap.xml"
|
||||
@model TaxBaik.Web.Pages.SitemapModel
|
||||
@{
|
||||
Layout = null;
|
||||
Response.ContentType = "application/xml; charset=utf-8";
|
||||
}<?xml version="1.0" encoding="utf-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@foreach (var url in Model.Urls)
|
||||
{
|
||||
<url>
|
||||
<loc>@System.Net.WebUtility.HtmlEncode(url)</loc>
|
||||
<lastmod>@DateTime.UtcNow:yyyy-MM-dd</lastmod>
|
||||
<loc>@url.Location</loc>
|
||||
@if (url.LastModified.HasValue)
|
||||
{
|
||||
<lastmod>@url.LastModified.Value.ToUniversalTime().ToString("yyyy-MM-dd")</lastmod>
|
||||
}
|
||||
</url>
|
||||
}
|
||||
</urlset>
|
||||
|
||||
@@ -5,48 +5,91 @@ namespace TaxBaik.Web.Pages;
|
||||
|
||||
public class SitemapModel : PageModel
|
||||
{
|
||||
private const string BaseUrl = "https://www.taxbaik.com";
|
||||
private const int BlogPageSize = 100;
|
||||
|
||||
private readonly BlogService _blogService;
|
||||
private readonly FaqService _faqService;
|
||||
private readonly AnnouncementService _announcementService;
|
||||
private readonly ILogger<SitemapModel> _logger;
|
||||
|
||||
public List<string> Urls { get; set; } = [];
|
||||
public List<SitemapUrl> Urls { get; } = [];
|
||||
|
||||
public SitemapModel(BlogService blogService)
|
||||
public SitemapModel(
|
||||
BlogService blogService,
|
||||
FaqService faqService,
|
||||
AnnouncementService announcementService,
|
||||
ILogger<SitemapModel> logger)
|
||||
{
|
||||
_blogService = blogService;
|
||||
_faqService = faqService;
|
||||
_announcementService = announcementService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var baseUrl = "https://www.taxbaik.com";
|
||||
// FAQ와 공지사항은 홈에만 표시되고 각각의 URL은 리다이렉트된다.
|
||||
// 따라서 홈의 변경일에 합산하고 리다이렉트 URL은 sitemap에서 제외한다.
|
||||
var homeLastModified = default(DateTime);
|
||||
var posts = new List<TaxBaik.Domain.Entities.BlogPost>();
|
||||
|
||||
// 정적 페이지 (항상 포함)
|
||||
Urls.AddRange(new[]
|
||||
{
|
||||
$"{baseUrl}",
|
||||
$"{baseUrl}/about",
|
||||
$"{baseUrl}/services",
|
||||
$"{baseUrl}/contact",
|
||||
$"{baseUrl}/privacy",
|
||||
$"{baseUrl}/terms",
|
||||
$"{baseUrl}/blog",
|
||||
$"{baseUrl}/faq",
|
||||
$"{baseUrl}/announcement",
|
||||
$"{baseUrl}/inquiry"
|
||||
});
|
||||
|
||||
// 동적 블로그 포스트 (DB 오류 처리)
|
||||
try
|
||||
{
|
||||
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 1000, categoryId: null, ct: default);
|
||||
foreach (var post in posts)
|
||||
{
|
||||
Urls.Add($"{baseUrl}/blog/{post.Slug}");
|
||||
}
|
||||
var faqTask = _faqService.GetActiveAsync(HttpContext.RequestAborted);
|
||||
var announcementTask = _announcementService.GetActiveAsync(HttpContext.RequestAborted);
|
||||
var postsTask = GetAllPublishedPostsAsync(HttpContext.RequestAborted);
|
||||
|
||||
await Task.WhenAll(faqTask, announcementTask, postsTask);
|
||||
posts = await postsTask;
|
||||
homeLastModified = (await faqTask)
|
||||
.Select(faq => faq.UpdatedAt)
|
||||
.Concat((await announcementTask).Select(announcement => announcement.UpdatedAt))
|
||||
.Concat(posts.Take(3).Select(post => post.UpdatedAt))
|
||||
.DefaultIfEmpty()
|
||||
.Max();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// DB 연결 실패 등의 경우, 정적 페이지만으로도 sitemap 제공
|
||||
// 프로덕션에서는 DB가 있으므로 이 경로는 사용되지 않음
|
||||
System.Diagnostics.Debug.WriteLine($"Sitemap: Blog posts load failed: {ex.Message}");
|
||||
// DB 문제 중에도 검색 엔진이 기존 핵심 페이지를 계속 발견할 수 있게 한다.
|
||||
_logger.LogError(ex, "Failed to load dynamic sitemap content");
|
||||
}
|
||||
|
||||
Urls.AddRange(new[]
|
||||
{
|
||||
new SitemapUrl($"{BaseUrl}/", homeLastModified == default ? null : homeLastModified),
|
||||
new SitemapUrl($"{BaseUrl}/about"),
|
||||
new SitemapUrl($"{BaseUrl}/services"),
|
||||
new SitemapUrl($"{BaseUrl}/contact"),
|
||||
new SitemapUrl($"{BaseUrl}/privacy"),
|
||||
new SitemapUrl($"{BaseUrl}/terms"),
|
||||
new SitemapUrl($"{BaseUrl}/blog")
|
||||
});
|
||||
|
||||
foreach (var post in posts)
|
||||
{
|
||||
Urls.Add(new SitemapUrl(
|
||||
$"{BaseUrl}/blog/{Uri.EscapeDataString(post.Slug)}",
|
||||
post.UpdatedAt == default ? post.PublishedAt : post.UpdatedAt));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<TaxBaik.Domain.Entities.BlogPost>> GetAllPublishedPostsAsync(CancellationToken ct)
|
||||
{
|
||||
var posts = new List<TaxBaik.Domain.Entities.BlogPost>();
|
||||
var page = 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var (items, total) = await _blogService.GetPublishedPagedAsync(page, BlogPageSize, ct: ct);
|
||||
posts.AddRange(items);
|
||||
|
||||
if (posts.Count >= total || !items.Any())
|
||||
return posts;
|
||||
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SitemapUrl(string Location, DateTime? LastModified = null);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
@{
|
||||
const string SiteUrl = "https://www.taxbaik.com";
|
||||
var canonicalUrl = ViewData["CanonicalUrl"] as string ?? $"{SiteUrl}{Context.Request.PathBase}{Context.Request.Path}";
|
||||
var ogUrl = ViewData["OgUrl"] as string ?? canonicalUrl;
|
||||
var robots = ViewData["Robots"] as string ?? "index, follow";
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
@@ -12,30 +18,29 @@
|
||||
<meta property="og:title" content="@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")" />
|
||||
<meta property="og:description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||
<meta property="og:image" content="@(ViewData["OgImage"] ?? "https://www.taxbaik.com/images/og-image.jpg")" />
|
||||
<meta property="og:url" content="@(ViewData["OgUrl"] ?? "https://www.taxbaik.com/")" />
|
||||
<meta property="og:url" content="@ogUrl" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:title" content="@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")" />
|
||||
<meta property="twitter:description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||
<meta property="twitter:image" content="@(ViewData["OgImage"] ?? "https://www.taxbaik.com/images/og-image.jpg")" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")" />
|
||||
<meta name="twitter:description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||
<meta name="twitter:image" content="@(ViewData["OgImage"] ?? "https://www.taxbaik.com/images/og-image.jpg")" />
|
||||
|
||||
<!-- 검색엔진 등록용 소유권 인증 메타 태그 (발급받으신 토큰이 있으면 아래 content에 넣어 주시면 됩니다) -->
|
||||
<!-- <meta name="naver-site-verification" content="네이버_서치어드바이저_토큰_입력" /> -->
|
||||
<!-- <meta name="google-site-verification" content="구글_서치콘솔_토큰_입력" /> -->
|
||||
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="robots" content="@robots" />
|
||||
<meta name="theme-color" content="#C89D6E" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
<!-- RSS Feed -->
|
||||
<link rel="alternate" type="application/rss+xml" title="백원숙 세무회계 블로그" href="/rss.xml" />
|
||||
<link rel="alternate" type="application/rss+xml" title="TaxBaik Blog Feed" href="/feed.xml" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+KR:wght@400;500;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="canonical" href="@(ViewData["CanonicalUrl"] ?? "https://www.taxbaik.com/")" />
|
||||
<link rel="canonical" href="@canonicalUrl" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ public class SitemapValidationService
|
||||
result.TotalUrls = staticUrls.Count;
|
||||
|
||||
// 2. 동적 블로그 포스트 검증
|
||||
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 1000, categoryId: null, ct: default);
|
||||
var blogUrls = posts.Select(p => $"https://www.taxbaik.com/blog/{p.Slug}").ToList();
|
||||
var posts = await GetAllPublishedPostsAsync();
|
||||
var blogUrls = posts.Select(p => $"https://www.taxbaik.com/blog/{Uri.EscapeDataString(p.Slug)}").ToList();
|
||||
|
||||
ValidateUrls(blogUrls, result);
|
||||
|
||||
@@ -251,10 +251,25 @@ public class SitemapValidationService
|
||||
$"{baseUrl}/contact",
|
||||
$"{baseUrl}/privacy",
|
||||
$"{baseUrl}/terms",
|
||||
$"{baseUrl}/blog",
|
||||
$"{baseUrl}/faq",
|
||||
$"{baseUrl}/announcement",
|
||||
$"{baseUrl}/inquiry"
|
||||
$"{baseUrl}/blog"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<TaxBaik.Domain.Entities.BlogPost>> GetAllPublishedPostsAsync()
|
||||
{
|
||||
const int pageSize = 100;
|
||||
var posts = new List<TaxBaik.Domain.Entities.BlogPost>();
|
||||
var page = 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var (items, total) = await _blogService.GetPublishedPagedAsync(page, pageSize);
|
||||
posts.AddRange(items);
|
||||
|
||||
if (posts.Count >= total || !items.Any())
|
||||
return posts;
|
||||
|
||||
page++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,30 +7,6 @@ Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /portal/
|
||||
Disallow: /manage/
|
||||
Disallow: *.json$
|
||||
Disallow: *.xml$ (sitemap 제외)
|
||||
|
||||
# 검색 엔진별 크롤링 속도 제한
|
||||
User-agent: Googlebot
|
||||
Crawl-delay: 1
|
||||
|
||||
User-agent: Bingbot
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: Yeti
|
||||
Crawl-delay: 1
|
||||
|
||||
# Naver, Kakao 검색 엔진
|
||||
User-agent: Naver
|
||||
Allow: /
|
||||
|
||||
User-agent: Kakaobot
|
||||
Allow: /
|
||||
|
||||
# Sitemap 위치
|
||||
Sitemap: https://www.taxbaik.com/sitemap.xml
|
||||
Sitemap: https://www.taxbaik.com/sitemap.xml
|
||||
|
||||
# RSS 피드
|
||||
Sitemap: https://www.taxbaik.com/rss.xml
|
||||
Sitemap: https://www.taxbaik.com/feed.xml
|
||||
|
||||
Reference in New Issue
Block a user