Skip to content

Commit 3b6b01f

Browse files
committed
Add favicon assets and safe meta URLs
1 parent 342d4b5 commit 3b6b01f

9 files changed

Lines changed: 234 additions & 20 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.hihat.blog.controller;
2+
3+
import com.hihat.blog.domain.Article;
4+
import com.hihat.blog.repository.BlogRepository;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.MediaType;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.ResponseBody;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
import java.net.URLEncoder;
13+
import java.nio.charset.StandardCharsets;
14+
import java.time.LocalDateTime;
15+
import java.time.format.DateTimeFormatter;
16+
import java.util.List;
17+
18+
@RestController
19+
@RequiredArgsConstructor
20+
public class SeoController {
21+
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE;
22+
private final BlogRepository blogRepository;
23+
24+
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
25+
@ResponseBody
26+
public String robots(HttpServletRequest request) {
27+
String baseUrl = resolveBaseUrl(request);
28+
return "User-agent: *\n" +
29+
"Allow: /\n" +
30+
"Sitemap: " + baseUrl + "/sitemap.xml\n";
31+
}
32+
33+
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
34+
@ResponseBody
35+
public String sitemap(HttpServletRequest request) {
36+
String baseUrl = resolveBaseUrl(request);
37+
StringBuilder xml = new StringBuilder();
38+
xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
39+
xml.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
40+
41+
appendUrl(xml, baseUrl + "/", null);
42+
appendUrl(xml, baseUrl + "/articles?type=" + encodeParam("이론정리"), null);
43+
appendUrl(xml, baseUrl + "/articles?type=" + encodeParam("문제풀이"), null);
44+
45+
List<Article> articles = blogRepository.findAll();
46+
for (Article article : articles) {
47+
LocalDateTime lastmod = article.getUpdatedAt() != null ? article.getUpdatedAt() : article.getCreatedAt();
48+
appendUrl(xml, baseUrl + "/articles/" + article.getId(), lastmod);
49+
}
50+
51+
xml.append("</urlset>");
52+
return xml.toString();
53+
}
54+
55+
private void appendUrl(StringBuilder xml, String loc, LocalDateTime lastmod) {
56+
xml.append("<url>");
57+
xml.append("<loc>").append(escapeXml(loc)).append("</loc>");
58+
if (lastmod != null) {
59+
xml.append("<lastmod>").append(lastmod.format(DATE_FORMAT)).append("</lastmod>");
60+
}
61+
xml.append("</url>");
62+
}
63+
64+
private String resolveBaseUrl(HttpServletRequest request) {
65+
String scheme = headerValue(request, "X-Forwarded-Proto");
66+
String host = headerValue(request, "X-Forwarded-Host");
67+
68+
if (scheme == null || scheme.isBlank()) {
69+
scheme = request.getScheme();
70+
}
71+
72+
if (host == null || host.isBlank()) {
73+
host = request.getHeader("Host");
74+
}
75+
76+
if (host == null || host.isBlank()) {
77+
host = request.getServerName();
78+
int port = request.getServerPort();
79+
if (("http".equalsIgnoreCase(scheme) && port != 80) || ("https".equalsIgnoreCase(scheme) && port != 443)) {
80+
host = host + ":" + port;
81+
}
82+
}
83+
84+
return scheme + "://" + host;
85+
}
86+
87+
private String encodeParam(String value) {
88+
return URLEncoder.encode(value, StandardCharsets.UTF_8);
89+
}
90+
91+
private String escapeXml(String value) {
92+
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
93+
}
94+
95+
private String headerValue(HttpServletRequest request, String name) {
96+
String value = request.getHeader(name);
97+
return value == null ? null : value.split(",")[0].trim();
98+
}
99+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.hihat.blog.controller;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import org.springframework.ui.Model;
5+
import org.springframework.web.bind.annotation.ControllerAdvice;
6+
import org.springframework.web.bind.annotation.ModelAttribute;
7+
8+
@ControllerAdvice
9+
public class ViewModelAdvice {
10+
11+
@ModelAttribute
12+
public void addBaseUrl(Model model, HttpServletRequest request) {
13+
if (request == null) {
14+
model.addAttribute("baseUrl", "");
15+
model.addAttribute("requestPath", "");
16+
return;
17+
}
18+
19+
String scheme = firstForwardedValue(request, "X-Forwarded-Proto");
20+
if (scheme == null || scheme.isBlank()) {
21+
scheme = request.getScheme();
22+
}
23+
24+
String host = firstForwardedValue(request, "X-Forwarded-Host");
25+
if (host == null || host.isBlank()) {
26+
host = request.getServerName();
27+
}
28+
boolean hostHasPort = host.contains(":");
29+
30+
int port = request.getServerPort();
31+
String forwardedPort = firstForwardedValue(request, "X-Forwarded-Port");
32+
if (forwardedPort != null && !forwardedPort.isBlank()) {
33+
try {
34+
port = Integer.parseInt(forwardedPort);
35+
} catch (NumberFormatException ignored) {
36+
// Fall back to server port.
37+
}
38+
}
39+
40+
String portSuffix = (!hostHasPort && port != 80 && port != 443) ? ":" + port : "";
41+
model.addAttribute("baseUrl", scheme + "://" + host + portSuffix);
42+
model.addAttribute("requestPath", request.getRequestURI());
43+
}
44+
45+
private String firstForwardedValue(HttpServletRequest request, String headerName) {
46+
String value = request.getHeader(headerName);
47+
if (value == null) {
48+
return null;
49+
}
50+
int commaIndex = value.indexOf(',');
51+
return (commaIndex >= 0) ? value.substring(0, commaIndex).trim() : value.trim();
52+
}
53+
}
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<browserconfig>
3+
<msapplication>
4+
<tile>
5+
<square150x150logo src="/assets/img/app-icon.svg"/>
6+
<TileColor>#f6f3ee</TileColor>
7+
</tile>
8+
</msapplication>
9+
</browserconfig>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "Algorithm Note",
3+
"short_name": "AlgoNote",
4+
"start_url": "/",
5+
"display": "standalone",
6+
"background_color": "#f6f3ee",
7+
"theme_color": "#f6f3ee",
8+
"icons": [
9+
{
10+
"src": "/assets/img/app-icon.svg",
11+
"sizes": "any",
12+
"type": "image/svg+xml"
13+
}
14+
]
15+
}
Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
1+
22
<th:block th:fragment="favicon">
33
<!-- favicon -->
4-
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
5-
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
6-
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
7-
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
8-
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
9-
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
10-
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
11-
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
12-
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
13-
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
14-
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
15-
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
16-
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
17-
<link rel="manifest" href="/favicon/manifest.json">
18-
<meta name="msapplication-TileColor" content="#ffffff">
19-
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
20-
<meta name="theme-color" content="#ffffff">
21-
</th:block>
4+
<link rel="icon" type="image/svg+xml" href="/assets/img/app-icon.svg">
5+
<link rel="shortcut icon" type="image/svg+xml" href="/assets/img/app-icon.svg">
6+
<link rel="apple-touch-icon" href="/assets/img/app-icon.svg">
7+
<link rel="manifest" href="/site.webmanifest">
8+
<meta name="msapplication-TileColor" content="#f6f3ee">
9+
<meta name="msapplication-config" content="/browserconfig.xml">
10+
</th:block>

src/main/resources/templates/layout/mainLayout.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,30 @@
88
<meta http-equiv="X-UA-Compatible" content="IE=edge">
99
<meta name="viewport" content="width=device-width, initial-scale=1">
1010

11+
<meta name="description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
12+
<meta name="theme-color" content="#f6f3ee">
13+
<meta name="apple-mobile-web-app-capable" content="yes">
14+
<meta name="apple-mobile-web-app-title" content="알고리즘 노트">
15+
<meta name="apple-mobile-web-app-status-bar-style" content="default">
16+
17+
<th:block>
18+
<link rel="canonical" th:if="${baseUrl != null and baseUrl != ''}" th:href="${baseUrl + requestPath}">
19+
<meta property="og:type" content="website">
20+
<meta property="og:site_name" content="알고리즘 노트">
21+
<meta property="og:title" content="Yu's Algorithm Note">
22+
<meta property="og:description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
23+
<meta property="og:url" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + requestPath}">
24+
<meta property="og:image" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + '/assets/img/main.png'}">
25+
<meta name="twitter:card" content="summary_large_image">
26+
<meta name="twitter:title" content="Yu's Algorithm Note">
27+
<meta name="twitter:description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
28+
<meta name="twitter:image" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + '/assets/img/main.png'}">
29+
</th:block>
30+
1131
<title>Yu's Algolithm Note</title>
1232

33+
<th:block th:replace="layout/favicon :: favicon"></th:block>
34+
1335
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
1436
<link rel="stylesheet" href="/assets/css/sidebar.css">
1537
<link rel="stylesheet" href="/assets/css/ckcontent.css">

src/main/resources/templates/layout/navbar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<nav class="site-nav" aria-label="Main">
55
<div class="container nav-inner">
66
<a href="/" class="brand" aria-label="알고리즘 노트 홈">
7-
<span class="brand-mark">Y</span>
7+
<span class="brand-mark">A</span>
88
<span class="brand-text">알고리즘 노트</span>
99
</a>
1010
<div class="nav-links" role="navigation">

src/main/resources/templates/layout/subLayout.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,30 @@
55
<meta http-equiv="X-UA-Compatible" content="IE=edge">
66
<meta name="viewport" content="width=device-width, initial-scale=1">
77

8+
<meta name="description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
9+
<meta name="theme-color" content="#f6f3ee">
10+
<meta name="apple-mobile-web-app-capable" content="yes">
11+
<meta name="apple-mobile-web-app-title" content="알고리즘 노트">
12+
<meta name="apple-mobile-web-app-status-bar-style" content="default">
13+
14+
<th:block>
15+
<link rel="canonical" th:if="${baseUrl != null and baseUrl != ''}" th:href="${baseUrl + requestPath}">
16+
<meta property="og:type" content="website">
17+
<meta property="og:site_name" content="알고리즘 노트">
18+
<meta property="og:title" content="Yu's Algorithm Note">
19+
<meta property="og:description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
20+
<meta property="og:url" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + requestPath}">
21+
<meta property="og:image" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + '/assets/img/main.png'}">
22+
<meta name="twitter:card" content="summary_large_image">
23+
<meta name="twitter:title" content="Yu's Algorithm Note">
24+
<meta name="twitter:description" content="알고리즘 이론과 문제 풀이를 간결하게 정리한 개인 노트입니다.">
25+
<meta name="twitter:image" th:if="${baseUrl != null and baseUrl != ''}" th:content="${baseUrl + '/assets/img/main.png'}">
26+
</th:block>
27+
828
<title>Yu's Algolithm Note</title>
929

30+
<th:block th:replace="layout/favicon :: favicon"></th:block>
31+
1032
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
1133
<link rel="stylesheet" href="/assets/css/sidebar.css">
1234
<link rel="stylesheet" href="/assets/css/common.css">

0 commit comments

Comments
 (0)