{"componentChunkName":"component---src-templates-blog-post-js","path":"/javascript/Gatsby-블로그-GEO-AEO-적용기-JSON-LD-FAQPage-llms-txt-사이트맵/","result":{"data":{"site":{"siteMetadata":{"title":"Bottlehs Tech Blog","description":"웹·앱·AI 개발자 전병훈(bottlehs)의 기술 블로그. AI·LLM, JavaScript·Vue, Spring Boot, 알고리즘, 블록체인 실무 가이드를 다룹니다","siteUrl":"https://www.bottlehs.dev","author":{"name":"Jeon Byung Hun","summary":"개발을 즐기는 bottlehs - Engineer, MS, AI, FE, BE, OS, IOT, Blockchain, 설계, 테스트"},"social":{"github":"bottlehs","codepen":"bottlehs"}}},"markdownRemark":{"id":"3a7f2c74-1a9c-5d38-9d1f-54b9570a7cc9","excerpt":"예전에는 검색 결과 첫 페이지의 파란 링크 10개 안에 드는 것이 목표였다. 요즘은 구글의 AI 요약, Perplexity, ChatGPT…","html":"<p>예전에는 검색 결과 첫 페이지의 파란 링크 10개 안에 드는 것이 목표였다. 요즘은 구글의 AI 요약, Perplexity, ChatGPT 검색처럼 <strong>답변을 먼저 보여 주고 출처를 인용하는</strong> 방식이 늘고 있다. 이런 환경에서는 “사람이 클릭하게 만드는 것”만큼 <strong>“기계가 글의 구조와 글쓴이를 정확히 이해하게 만드는 것”</strong>이 중요해진다.</p>\n<p>이 글은 Gatsby 2 기반 기술 블로그(글 186편)에 SEO, GEO, AEO 개선을 적용한 과정을 정리한다. 추상적인 체크리스트보다 <strong>실제로 바꾼 코드와 수치</strong>를 중심으로 쓴다.</p>\n<h2 id=\"seo-geo-aeo는-무엇이-다른가\" style=\"position:relative;\"><a href=\"#seo-geo-aeo%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80\" aria-label=\"seo geo aeo는 무엇이 다른가 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>SEO, GEO, AEO는 무엇이 다른가</h2>\n<p>세 용어는 겹치는 부분이 많지만, 목표로 하는 “읽는 주체”가 다르다.</p>\n<table>\n<thead>\n<tr>\n<th>구분</th>\n<th>풀이</th>\n<th>누가 읽나</th>\n<th>핵심 목표</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>SEO</td>\n<td>Search Engine Optimization</td>\n<td>검색 크롤러</td>\n<td>색인되고 순위에 오르기</td>\n</tr>\n<tr>\n<td>AEO</td>\n<td>Answer Engine Optimization</td>\n<td>답변 엔진(스니펫, 음성 비서, AI 요약)</td>\n<td>질문에 대한 <strong>직접 답</strong>으로 채택되기</td>\n</tr>\n<tr>\n<td>GEO</td>\n<td>Generative Engine Optimization</td>\n<td>생성형 AI 검색</td>\n<td>생성된 답변에 <strong>출처로 인용</strong>되기</td>\n</tr>\n</tbody>\n</table>\n<p>실무에서 할 일은 대부분 같다. <strong>명확한 구조, 기계가 읽을 수 있는 메타데이터, 신뢰할 수 있는 글쓴이 정보, 질문과 답이 분명한 본문</strong>이다. 차이는 어디에 더 힘을 주느냐 정도다.</p>\n<h2 id=\"적용-전-진단\" style=\"position:relative;\"><a href=\"#%EC%A0%81%EC%9A%A9-%EC%A0%84-%EC%A7%84%EB%8B%A8\" aria-label=\"적용 전 진단 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>적용 전 진단</h2>\n<p>작업 전에 빌드 결과물의 <code class=\"language-text\">&lt;head></code>를 직접 열어 보았다. 소스 코드만 보면 놓치는 부분이 많아서, <strong>최종 HTML을 기준으로 진단</strong>하는 것이 정확하다.</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">npx gatsby build\n<span class=\"token comment\"># 홈 &lt;head>에서 메타 태그만 추려 보기</span>\n<span class=\"token function\">grep</span> <span class=\"token parameter variable\">-oE</span> <span class=\"token string\">'&lt;meta[^>]*>'</span> public/index.html <span class=\"token operator\">|</span> <span class=\"token function\">head</span> <span class=\"token parameter variable\">-30</span></code></pre></div>\n<p>발견한 문제는 다음과 같았다.</p>\n<table>\n<thead>\n<tr>\n<th>항목</th>\n<th>상태</th>\n<th>영향</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code class=\"language-text\">og:image</code></td>\n<td>없음</td>\n<td>카카오톡·슬랙 공유 시 미리보기 이미지 없음</td>\n</tr>\n<tr>\n<td><code class=\"language-text\">og:type</code></td>\n<td>모든 페이지가 <code class=\"language-text\">website</code></td>\n<td>글 페이지가 “기사”로 인식되지 않음</td>\n</tr>\n<tr>\n<td>JSON-LD</td>\n<td>없음</td>\n<td>글쓴이·게시일·FAQ를 기계가 구조적으로 알 수 없음</td>\n</tr>\n<tr>\n<td><code class=\"language-text\">keywords</code></td>\n<td>포트폴리오 페이지에서 넘겨도 <strong>무시됨</strong></td>\n<td>SEO 컴포넌트가 해당 prop을 받지 않는 버그</td>\n</tr>\n<tr>\n<td>사이트맵</td>\n<td>URL 927개 중 739개가 태그 페이지</td>\n<td>대부분 글 1~2개짜리 얇은 페이지</td>\n</tr>\n<tr>\n<td><code class=\"language-text\">robots.txt</code></td>\n<td><code class=\"language-text\">Sitemap:</code> 줄 없음</td>\n<td>일부 검색엔진이 사이트맵을 자동으로 찾지 못함</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"1-seo-컴포넌트를-페이지-유형을-아는-컴포넌트로\" style=\"position:relative;\"><a href=\"#1-seo-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9C%A0%ED%98%95%EC%9D%84-%EC%95%84%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C\" aria-label=\"1 seo 컴포넌트를 페이지 유형을 아는 컴포넌트로 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>1. SEO 컴포넌트를 “페이지 유형”을 아는 컴포넌트로</h2>\n<p>Gatsby 스타터의 기본 <code class=\"language-text\">SEO</code> 컴포넌트는 제목과 설명만 받는다. 여기에 <code class=\"language-text\">pathname</code>, <code class=\"language-text\">image</code>, <code class=\"language-text\">type</code>, <code class=\"language-text\">article</code>, <code class=\"language-text\">jsonLd</code>를 추가해서, 페이지가 자기 유형에 맞는 메타데이터를 내보내게 했다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// src/components/seo.js (핵심 부분)</span>\n<span class=\"token keyword\">const</span> <span class=\"token function-variable function\">SEO</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> description<span class=\"token punctuation\">,</span> lang<span class=\"token punctuation\">,</span> meta<span class=\"token punctuation\">,</span> title<span class=\"token punctuation\">,</span> keywords<span class=\"token punctuation\">,</span> image<span class=\"token punctuation\">,</span> pathname<span class=\"token punctuation\">,</span> type<span class=\"token punctuation\">,</span> article<span class=\"token punctuation\">,</span> jsonLd <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// ...siteMetadata 조회</span>\n  <span class=\"token keyword\">const</span> pageUrl <span class=\"token operator\">=</span> <span class=\"token function\">toAbsoluteUrl</span><span class=\"token punctuation\">(</span>siteUrl<span class=\"token punctuation\">,</span> pathname <span class=\"token operator\">||</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">/</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">const</span> imageUrl <span class=\"token operator\">=</span> <span class=\"token function\">toAbsoluteUrl</span><span class=\"token punctuation\">(</span>siteUrl<span class=\"token punctuation\">,</span> image <span class=\"token operator\">||</span> <span class=\"token constant\">DEFAULT_IMAGE</span><span class=\"token punctuation\">)</span>\n\n  <span class=\"token keyword\">const</span> articleMeta <span class=\"token operator\">=</span> article\n    <span class=\"token operator\">?</span> <span class=\"token punctuation\">[</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">article:published_time</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> article<span class=\"token punctuation\">.</span>publishedTime <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">article:modified_time</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> article<span class=\"token punctuation\">.</span>modifiedTime <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">article:section</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> article<span class=\"token punctuation\">.</span>section <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token operator\">...</span><span class=\"token punctuation\">(</span>article<span class=\"token punctuation\">.</span>tags <span class=\"token operator\">||</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">map</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">tag</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">article:tag</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> tag <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      <span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span><span class=\"token function\">filter</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">m</span> <span class=\"token operator\">=></span> m<span class=\"token punctuation\">.</span>content<span class=\"token punctuation\">)</span>\n    <span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token operator\">&lt;</span>Helmet\n      meta<span class=\"token operator\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">[</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">og:url</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> pageUrl <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">og:type</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> type <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">og:image</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> imageUrl <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">property</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">og:locale</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">ko_KR</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">twitter:card</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> image <span class=\"token operator\">?</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">summary_large_image</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">summary</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n        <span class=\"token comment\">// ...</span>\n      <span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span><span class=\"token function\">filter</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">m</span> <span class=\"token operator\">=></span> m<span class=\"token punctuation\">.</span>content<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">concat</span><span class=\"token punctuation\">(</span>articleMeta<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">concat</span><span class=\"token punctuation\">(</span>meta<span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span>\n    <span class=\"token operator\">></span>\n      <span class=\"token punctuation\">{</span>jsonLd <span class=\"token operator\">&amp;&amp;</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token operator\">&lt;</span>script type<span class=\"token operator\">=</span><span class=\"token string\">\"application/ld+json\"</span><span class=\"token operator\">></span>\n          <span class=\"token punctuation\">{</span><span class=\"token constant\">JSON</span><span class=\"token punctuation\">.</span><span class=\"token function\">stringify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> <span class=\"token string-property property\">\"@context\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://schema.org\"</span><span class=\"token punctuation\">,</span> <span class=\"token string-property property\">\"@graph\"</span><span class=\"token operator\">:</span> jsonLd <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span>\n        <span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>script<span class=\"token operator\">></span>\n      <span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span>\n    <span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>Helmet<span class=\"token operator\">></span>\n  <span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><code class=\"language-text\">toAbsoluteUrl</code>에서 <code class=\"language-text\">encodeURI(decodeURI(path))</code>를 쓴 이유가 있다. 이 블로그는 한글 slug를 쓰는데, 경로가 이미 인코딩된 상태로 들어오기도 하고 아닌 채로 들어오기도 한다. 한 번 풀고 다시 인코딩하면 <strong>이중 인코딩(<code class=\"language-text\">%25EC…</code>)을 막을 수 있다</strong>.</p>\n<blockquote>\n<p>canonical 태그는 이미 <code class=\"language-text\">gatsby-plugin-canonical-urls</code>가 넣고 있어서 SEO 컴포넌트에서는 뺐다. canonical이 두 개 있으면 검색엔진이 어느 쪽도 신뢰하지 않을 수 있다.</p>\n</blockquote>\n<h2 id=\"2-json-ld는-code-classlanguage-textgraphcode로-연결한다\" style=\"position:relative;\"><a href=\"#2-json-ld%EB%8A%94-code-classlanguage-textgraphcode%EB%A1%9C-%EC%97%B0%EA%B2%B0%ED%95%9C%EB%8B%A4\" aria-label=\"2 json ld는 code classlanguage textgraphcode로 연결한다 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>2. JSON-LD는 <code class=\"language-text\">@graph</code>로 연결한다</h2>\n<p>구조화 데이터를 페이지마다 따로따로 넣기보다, <code class=\"language-text\">@id</code>로 서로 연결된 <strong>그래프</strong>를 만드는 편이 좋다. “이 글(BlogPosting)의 저자는 이 사람(Person)이고, 이 사이트(WebSite)의 일부다”라는 관계가 명확해지기 때문이다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// src/utils/structuredData.js</span>\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token function-variable function\">personId</span> <span class=\"token operator\">=</span> <span class=\"token parameter\">siteUrl</span> <span class=\"token operator\">=></span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>siteUrl<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">/#person</span><span class=\"token template-punctuation string\">`</span></span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token function-variable function\">personSchema</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> siteUrl<span class=\"token punctuation\">,</span> author<span class=\"token punctuation\">,</span> social <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token string-property property\">\"@type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Person\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token string-property property\">\"@id\"</span><span class=\"token operator\">:</span> <span class=\"token function\">personId</span><span class=\"token punctuation\">(</span>siteUrl<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> author<span class=\"token punctuation\">.</span>name<span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">url</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>siteUrl<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">/portfolio/</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">sameAs</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">https://github.com/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>social<span class=\"token punctuation\">.</span>github<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">https://codepen.io/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>social<span class=\"token punctuation\">.</span>codepen<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token function-variable function\">blogPostingSchema</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">siteMetadata<span class=\"token punctuation\">,</span> post</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token string-property property\">\"@type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"BlogPosting\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">headline</span><span class=\"token operator\">:</span> post<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>title<span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">datePublished</span><span class=\"token operator\">:</span> post<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>dateISO<span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">dateModified</span><span class=\"token operator\">:</span> post<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>updatedISO <span class=\"token operator\">||</span> post<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>dateISO<span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">author</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token string-property property\">\"@id\"</span><span class=\"token operator\">:</span> <span class=\"token function\">personId</span><span class=\"token punctuation\">(</span>siteMetadata<span class=\"token punctuation\">.</span>siteUrl<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span> <span class=\"token comment\">// Person 노드를 참조</span>\n  <span class=\"token comment\">// ...</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p>페이지 유형별로 넣은 스키마는 다음과 같다.</p>\n<table>\n<thead>\n<tr>\n<th>페이지</th>\n<th>스키마</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>홈</td>\n<td><code class=\"language-text\">WebSite</code>, <code class=\"language-text\">Person</code></td>\n</tr>\n<tr>\n<td>글</td>\n<td><code class=\"language-text\">BlogPosting</code>, <code class=\"language-text\">Person</code>, <code class=\"language-text\">BreadcrumbList</code>, (FAQ가 있으면) <code class=\"language-text\">FAQPage</code></td>\n</tr>\n<tr>\n<td>포트폴리오</td>\n<td><code class=\"language-text\">ProfilePage</code> + <code class=\"language-text\">Person</code></td>\n</tr>\n</tbody>\n</table>\n<p>AEO·GEO 관점에서 특히 중요한 것은 <code class=\"language-text\">Person</code>의 <code class=\"language-text\">sameAs</code>다. GitHub 같은 외부 프로필과 연결해 두면, 답변 엔진이 “누가, 어떤 근거로 쓴 글인지” 판단하는 데 도움이 된다.</p>\n<h2 id=\"3-마크다운-faq에서-faqpage를-자동으로-만든다\" style=\"position:relative;\"><a href=\"#3-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4-faq%EC%97%90%EC%84%9C-faqpage%EB%A5%BC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A0%EB%8B%A4\" aria-label=\"3 마크다운 faq에서 faqpage를 자동으로 만든다 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>3. 마크다운 FAQ에서 FAQPage를 자동으로 만든다</h2>\n<p>이 블로그의 글 상당수는 마지막에 다음 형식의 FAQ 섹션을 두고 있었다.</p>\n<div class=\"gatsby-highlight\" data-language=\"markdown\"><pre class=\"language-markdown\"><code class=\"language-markdown\"><span class=\"token title important\"><span class=\"token punctuation\">##</span> FAQ</span>\n\n<span class=\"token bold\"><span class=\"token punctuation\">**</span><span class=\"token content\">Q: 질문 내용?</span><span class=\"token punctuation\">**</span></span>  \nA: 답변 내용.</code></pre></div>\n<p>글마다 JSON-LD를 손으로 쓰는 대신, <code class=\"language-text\">onCreateNode</code>에서 본문을 파싱해 노드 필드로 붙였다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// gatsby-node.js</span>\n<span class=\"token keyword\">const</span> <span class=\"token function-variable function\">extractFaq</span> <span class=\"token operator\">=</span> <span class=\"token parameter\">markdown</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// 코드 블록 안의 \"## FAQ\" 예시를 실제 FAQ로 오인하지 않도록 먼저 걷어낸다</span>\n  <span class=\"token keyword\">const</span> section <span class=\"token operator\">=</span> markdown<span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">^```[\\s\\S]*?^```</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">gm</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">match</span><span class=\"token punctuation\">(</span>\n    <span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">^##\\s+(?:FAQ|자주\\s*묻는[^\\n]*)[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|$(?![\\s\\S]))</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">m</span></span>\n  <span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>section<span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n  <span class=\"token keyword\">const</span> faq <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n  <span class=\"token comment\">// \"**Q: …**\" 또는 \"**Q1. …**\" 다음 줄의 \"A: …\"를 한 쌍으로 본다</span>\n  <span class=\"token keyword\">const</span> pattern <span class=\"token operator\">=</span> <span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">\\*\\*Q\\d*[.:：]\\s*([\\s\\S]+?)\\*\\*\\s*\\n+\\s*A[.:：]\\s*([\\s\\S]+?)(?=\\n\\s*\\n|$(?![\\s\\S]))</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span>\n  <span class=\"token keyword\">let</span> match\n  <span class=\"token keyword\">while</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>match <span class=\"token operator\">=</span> pattern<span class=\"token punctuation\">.</span><span class=\"token function\">exec</span><span class=\"token punctuation\">(</span>section<span class=\"token punctuation\">[</span><span class=\"token number\">1</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">!==</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    faq<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">question</span><span class=\"token operator\">:</span> <span class=\"token function\">toPlainText</span><span class=\"token punctuation\">(</span>match<span class=\"token punctuation\">[</span><span class=\"token number\">1</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">answer</span><span class=\"token operator\">:</span> <span class=\"token function\">toPlainText</span><span class=\"token punctuation\">(</span>match<span class=\"token punctuation\">[</span><span class=\"token number\">2</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span> faq\n<span class=\"token punctuation\">}</span>\n\nexports<span class=\"token punctuation\">.</span><span class=\"token function-variable function\">onCreateNode</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> node<span class=\"token punctuation\">,</span> actions <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>node<span class=\"token punctuation\">.</span>internal<span class=\"token punctuation\">.</span>type <span class=\"token operator\">===</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">MarkdownRemark</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    actions<span class=\"token punctuation\">.</span><span class=\"token function\">createNodeField</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> node<span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">faq</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">value</span><span class=\"token operator\">:</span> <span class=\"token function\">extractFaq</span><span class=\"token punctuation\">(</span>node<span class=\"token punctuation\">.</span>rawMarkdownBody <span class=\"token operator\">||</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>정규식은 처음에 <code class=\"language-text\">**Q:</code> 형식만 잡도록 짰다. 그런데 전체 글에 돌려 보니 FAQ 섹션이 있는데도 추출이 안 되는 글이 2편 나왔다. 확인해 보니 <code class=\"language-text\">**Q1.</code>처럼 번호를 붙인 형식이었다. <code class=\"language-text\">Q\\d*[.:：]</code>로 바꾼 뒤 <strong>54편, Q&#x26;A 316쌍</strong>이 추출되었다. 파서를 만들면 <strong>전체 데이터에 한 번 돌려 보고 “섹션은 있는데 결과가 0인 파일”을 찾는 것</strong>이 가장 빠른 검증법이다.</p>\n<p>또 하나의 함정은 이 글 자체에서 나왔다. 위처럼 FAQ 형식을 설명하는 코드 블록 안에 <code class=\"language-text\">## FAQ</code>가 들어 있어서, 추출기가 글 끝의 진짜 FAQ 대신 코드 블록 속 예시를 먼저 잡았다. 그래서 파싱 전에 펜스 코드 블록을 걷어내는 한 줄을 추가했다. <strong>마크다운을 정규식으로 다룰 때는 코드 블록부터 제외한다</strong>는 원칙을 기억해 두면 좋다.</p>\n<p>필드는 스키마에 명시해 둔다. 그래야 FAQ가 없는 글만 있는 환경에서도 GraphQL 쿼리가 깨지지 않는다.</p>\n<div class=\"gatsby-highlight\" data-language=\"graphql\"><pre class=\"language-graphql\"><code class=\"language-graphql\"><span class=\"token keyword\">type</span> <span class=\"token class-name\">Fields</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token attr-name\">slug</span><span class=\"token punctuation\">:</span> <span class=\"token scalar\">String</span>\n  <span class=\"token attr-name\">faq</span><span class=\"token punctuation\">:</span> <span class=\"token punctuation\">[</span><span class=\"token class-name\">FaqItem</span><span class=\"token punctuation\">]</span>\n  <span class=\"token attr-name\">image</span><span class=\"token punctuation\">:</span> <span class=\"token scalar\">String</span>\n<span class=\"token punctuation\">}</span>\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">FaqItem</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token attr-name\">question</span><span class=\"token punctuation\">:</span> <span class=\"token scalar\">String</span>\n  <span class=\"token attr-name\">answer</span><span class=\"token punctuation\">:</span> <span class=\"token scalar\">String</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<h3 id=\"faqpage에-대한-현실적인-기대치\" style=\"position:relative;\"><a href=\"#faqpage%EC%97%90-%EB%8C%80%ED%95%9C-%ED%98%84%EC%8B%A4%EC%A0%81%EC%9D%B8-%EA%B8%B0%EB%8C%80%EC%B9%98\" aria-label=\"faqpage에 대한 현실적인 기대치 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>FAQPage에 대한 현실적인 기대치</h3>\n<p>구글은 2023년 8월부터 FAQ <strong>리치 결과(검색 결과에 펼쳐지는 Q&#x26;A)</strong>를 잘 알려진 정부·보건 관련 권위 사이트 위주로만 보여 주고 있다. 일반 기술 블로그가 FAQPage를 넣는다고 검색 결과에 Q&#x26;A가 펼쳐질 가능성은 낮다.</p>\n<p>그럼에도 넣은 이유는 <strong>답변 엔진 관점</strong>이다. 질문과 답이 명시적으로 짝지어진 데이터는 기계가 “이 글이 어떤 질문에 답하는지”를 파악하는 데 여전히 유용하다. 단, <strong>화면에 실제로 보이는 FAQ만</strong> 마크업해야 한다. 숨겨진 Q&#x26;A를 구조화 데이터에만 넣는 것은 가이드라인 위반이다.</p>\n<h2 id=\"4-빌드-시-llmstxt-생성\" style=\"position:relative;\"><a href=\"#4-%EB%B9%8C%EB%93%9C-%EC%8B%9C-llmstxt-%EC%83%9D%EC%84%B1\" aria-label=\"4 빌드 시 llmstxt 생성 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>4. 빌드 시 llms.txt 생성</h2>\n<p><a href=\"https://llmstxt.org\">llms.txt</a>는 사이트의 개요와 주요 문서 목록을 LLM이 읽기 좋은 마크다운으로 제공하자는 <strong>제안 형식</strong>이다. 아직 표준이 아니고, 주요 AI 크롤러가 이 파일을 반드시 읽는다는 보장도 없다. 하지만 생성 비용이 거의 없으므로, 빌드할 때 자동으로 만들게 했다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// gatsby-node.js</span>\nexports<span class=\"token punctuation\">.</span><span class=\"token function-variable function\">onPostBuild</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">async</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> graphql <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> data <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">graphql</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">{ /* site 정보 + 전체 글 */ }</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> title<span class=\"token punctuation\">,</span> description<span class=\"token punctuation\">,</span> siteUrl <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> data<span class=\"token punctuation\">.</span>site<span class=\"token punctuation\">.</span>siteMetadata\n  <span class=\"token keyword\">const</span> byCategory <span class=\"token operator\">=</span> _<span class=\"token punctuation\">.</span><span class=\"token function\">groupBy</span><span class=\"token punctuation\">(</span>data<span class=\"token punctuation\">.</span>allMarkdownRemark<span class=\"token punctuation\">.</span>nodes<span class=\"token punctuation\">,</span> <span class=\"token parameter\">n</span> <span class=\"token operator\">=></span> n<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>category<span class=\"token punctuation\">)</span>\n\n  <span class=\"token keyword\">const</span> lines <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\"># </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>title<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">> </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>description<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">]</span>\n  Object<span class=\"token punctuation\">.</span><span class=\"token function\">keys</span><span class=\"token punctuation\">(</span>byCategory<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">sort</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">category</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n    lines<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">## </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>category<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span>\n    byCategory<span class=\"token punctuation\">[</span>category<span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">node</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      lines<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">- [</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>node<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>title<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">](</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>siteUrl<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span><span class=\"token function\">encodeURI</span><span class=\"token punctuation\">(</span>node<span class=\"token punctuation\">.</span>fields<span class=\"token punctuation\">.</span>slug<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">): </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>node<span class=\"token punctuation\">.</span>frontmatter<span class=\"token punctuation\">.</span>description<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n    lines<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  fs<span class=\"token punctuation\">.</span><span class=\"token function\">writeFileSync</span><span class=\"token punctuation\">(</span>path<span class=\"token punctuation\">.</span><span class=\"token function\">join</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">public</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">llms.txt</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> lines<span class=\"token punctuation\">.</span><span class=\"token function\">join</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\\n</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>같은 <code class=\"language-text\">onPostBuild</code>에서 <code class=\"language-text\">robots.txt</code>도 <code class=\"language-text\">siteUrl</code> 기반으로 생성하게 바꿨다. 도메인이 바뀌어도 <code class=\"language-text\">Sitemap:</code> 줄이 자동으로 따라온다.</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">User-agent: *\nAllow: /\n\nSitemap: https://www.bottlehs.dev/sitemap.xml</code></pre></div>\n<h2 id=\"5-사이트맵-정리-927개에서-264개로\" style=\"position:relative;\"><a href=\"#5-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5-%EC%A0%95%EB%A6%AC-927%EA%B0%9C%EC%97%90%EC%84%9C-264%EA%B0%9C%EB%A1%9C\" aria-label=\"5 사이트맵 정리 927개에서 264개로 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>5. 사이트맵 정리: 927개에서 264개로</h2>\n<p>태그가 739개였고, 대부분 글이 한두 개뿐이었다. 이런 페이지는 검색 사용자에게 가치가 거의 없고, 신규 사이트의 제한된 크롤링 예산을 잡아먹는다. 기준을 <strong>“글 3개 이상인 태그만 색인”</strong>으로 정하고, 판단을 한 곳에서만 하도록 태그 페이지 생성 시점의 <code class=\"language-text\">context</code>에 넣었다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// gatsby-node.js — 태그 페이지를 만들 때 색인 여부를 결정</span>\n<span class=\"token keyword\">const</span> <span class=\"token constant\">MIN_POSTS_FOR_INDEXED_TAG</span> <span class=\"token operator\">=</span> <span class=\"token number\">3</span>\n\ntags<span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">tag</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token function\">createPage</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">path</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">/tags/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>_<span class=\"token punctuation\">.</span><span class=\"token function\">kebabCase</span><span class=\"token punctuation\">(</span>tag<span class=\"token punctuation\">.</span>fieldValue<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">/</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">component</span><span class=\"token operator\">:</span> blogTag<span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">context</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">tag</span><span class=\"token operator\">:</span> tag<span class=\"token punctuation\">.</span>fieldValue<span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">noindex</span><span class=\"token operator\">:</span> tag<span class=\"token punctuation\">.</span>totalCount <span class=\"token operator\">&lt;</span> <span class=\"token constant\">MIN_POSTS_FOR_INDEXED_TAG</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p>태그 템플릿은 이 값을 보고 <code class=\"language-text\">noindex, follow</code>를 넣는다. <code class=\"language-text\">follow</code>를 유지하므로 태그 페이지를 통한 글 링크 탐색은 막지 않는다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token operator\">&lt;</span><span class=\"token constant\">SEO</span>\n  title<span class=\"token operator\">=</span><span class=\"token punctuation\">{</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">#</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>tag<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\"> 관련 글</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">}</span>\n  meta<span class=\"token operator\">=</span><span class=\"token punctuation\">{</span>pageContext<span class=\"token punctuation\">.</span>noindex <span class=\"token operator\">?</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">robots</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">content</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">noindex, follow</span><span class=\"token template-punctuation string\">`</span></span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">}</span>\n<span class=\"token operator\">/</span><span class=\"token operator\">></span></code></pre></div>\n<p>사이트맵 플러그인은 <strong>같은 <code class=\"language-text\">context.noindex</code> 값</strong>을 읽어 제외한다. 태그 페이지와 사이트맵이 서로 다른 기준을 쓰면 “사이트맵에는 있는데 noindex인 페이지”라는 모순이 생기므로, 판단 로직은 반드시 하나여야 한다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// gatsby-config.js — gatsby-plugin-sitemap 옵션</span>\n<span class=\"token function-variable function\">serialize</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> site<span class=\"token punctuation\">,</span> allSitePage<span class=\"token punctuation\">,</span> allMarkdownRemark <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> lastmodBySlug <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span>\n  allMarkdownRemark<span class=\"token punctuation\">.</span>nodes<span class=\"token punctuation\">.</span><span class=\"token function\">forEach</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> fields<span class=\"token punctuation\">,</span> frontmatter <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n    lastmodBySlug<span class=\"token punctuation\">[</span>fields<span class=\"token punctuation\">.</span>slug<span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> frontmatter<span class=\"token punctuation\">.</span>updated <span class=\"token operator\">||</span> frontmatter<span class=\"token punctuation\">.</span>date\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n\n  <span class=\"token keyword\">return</span> allSitePage<span class=\"token punctuation\">.</span>nodes\n    <span class=\"token punctuation\">.</span><span class=\"token function\">filter</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">page</span> <span class=\"token operator\">=></span> <span class=\"token operator\">!</span>page<span class=\"token punctuation\">.</span>context<span class=\"token operator\">?.</span>noindex<span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">.</span><span class=\"token function\">map</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">page</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">url</span><span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>site<span class=\"token punctuation\">.</span>siteMetadata<span class=\"token punctuation\">.</span>siteUrl<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span><span class=\"token function\">encodeURI</span><span class=\"token punctuation\">(</span>page<span class=\"token punctuation\">.</span>path<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token comment\">// 한글 경로 인코딩</span>\n      <span class=\"token operator\">...</span><span class=\"token punctuation\">(</span>lastmodBySlug<span class=\"token punctuation\">[</span>page<span class=\"token punctuation\">.</span>path<span class=\"token punctuation\">]</span> <span class=\"token operator\">&amp;&amp;</span> <span class=\"token punctuation\">{</span>\n        <span class=\"token literal-property property\">lastmodISO</span><span class=\"token operator\">:</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Date</span><span class=\"token punctuation\">(</span>lastmodBySlug<span class=\"token punctuation\">[</span>page<span class=\"token punctuation\">.</span>path<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">toISOString</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span></code></pre></div>\n<table>\n<thead>\n<tr>\n<th>지표</th>\n<th>이전</th>\n<th>이후</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>사이트맵 URL</td>\n<td>927</td>\n<td>264</td>\n</tr>\n<tr>\n<td>태그 페이지(사이트맵 내)</td>\n<td>739</td>\n<td>76</td>\n</tr>\n<tr>\n<td><code class=\"language-text\">lastmod</code></td>\n<td>0</td>\n<td>187</td>\n</tr>\n<tr>\n<td>인코딩 안 된 한글 URL</td>\n<td>578</td>\n<td>0</td>\n</tr>\n<tr>\n<td>noindex 처리된 태그 페이지</td>\n<td>0</td>\n<td>663</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"6-배포-후에야-보인-버그-존재하지-않는-대표-이미지\" style=\"position:relative;\"><a href=\"#6-%EB%B0%B0%ED%8F%AC-%ED%9B%84%EC%97%90%EC%95%BC-%EB%B3%B4%EC%9D%B8-%EB%B2%84%EA%B7%B8-%EC%A1%B4%EC%9E%AC%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%8C%80%ED%91%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80\" aria-label=\"6 배포 후에야 보인 버그 존재하지 않는 대표 이미지 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>6. 배포 후에야 보인 버그: 존재하지 않는 대표 이미지</h2>\n<p><code class=\"language-text\">og:image</code>는 본문의 첫 이미지를 쓰도록 했다. 로컬 빌드는 문제없이 통과했는데, 배포 후 실제 URL을 요청해 보니 일부 글의 <code class=\"language-text\">og:image</code>가 <strong>404</strong>였다. 30편의 글이 <code class=\"language-text\">/assets/etc.png</code> 같은 이미지를 참조하고 있었는데, 그 파일이 원래부터 저장소에 없었던 것이다. 본문 이미지도 깨져 있었지만 아무도 눈치채지 못했고, 대표 이미지로 끌어올리면서 공유 미리보기까지 깨질 뻔했다.</p>\n<p><code class=\"language-text\">static/</code> 폴더에 실제로 파일이 있을 때만 대표 이미지로 쓰고, 없으면 기본 이미지로 대체하게 고쳤다.</p>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">const</span> <span class=\"token function-variable function\">extractImage</span> <span class=\"token operator\">=</span> <span class=\"token parameter\">markdown</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> match <span class=\"token operator\">=</span> markdown<span class=\"token punctuation\">.</span><span class=\"token function\">match</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">!\\[[^\\]]*\\]\\(\\s*(\\/[^)\\s]+|https?:\\/\\/[^)\\s]+)</span><span class=\"token regex-delimiter\">/</span></span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>match<span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span>\n  <span class=\"token keyword\">const</span> image <span class=\"token operator\">=</span> match<span class=\"token punctuation\">[</span><span class=\"token number\">1</span><span class=\"token punctuation\">]</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>image<span class=\"token punctuation\">.</span><span class=\"token function\">startsWith</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">/</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span> <span class=\"token operator\">&amp;&amp;</span> <span class=\"token operator\">!</span>fs<span class=\"token punctuation\">.</span><span class=\"token function\">existsSync</span><span class=\"token punctuation\">(</span>path<span class=\"token punctuation\">.</span><span class=\"token function\">join</span><span class=\"token punctuation\">(</span>__dirname<span class=\"token punctuation\">,</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">static</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> <span class=\"token function\">decodeURI</span><span class=\"token punctuation\">(</span>image<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span> <span class=\"token comment\">// 기본 이미지로 대체</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span> image\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>교훈은 단순하다. <strong>빌드 성공이 곧 정상은 아니다.</strong> 메타데이터가 가리키는 리소스까지 실제로 열리는지 확인해야 한다.</p>\n<h2 id=\"검증-방법\" style=\"position:relative;\"><a href=\"#%EA%B2%80%EC%A6%9D-%EB%B0%A9%EB%B2%95\" aria-label=\"검증 방법 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>검증 방법</h2>\n<p>배포 후에는 다음 순서로 확인했다.</p>\n<ol>\n<li><strong>curl로 실제 응답 확인</strong>: 사이트맵에서 URL 몇 개를 뽑아 상태 코드를 확인한다.</li>\n</ol>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">curl</span> <span class=\"token parameter variable\">-s</span> https://www.bottlehs.dev/sitemap.xml <span class=\"token punctuation\">\\</span>\n  <span class=\"token operator\">|</span> <span class=\"token function\">grep</span> <span class=\"token parameter variable\">-o</span> <span class=\"token string\">'&lt;loc>[^&lt;]*'</span> <span class=\"token operator\">|</span> <span class=\"token function\">sed</span> <span class=\"token string\">'s#&lt;loc>##'</span> <span class=\"token operator\">|</span> <span class=\"token function\">awk</span> <span class=\"token string\">'NR%60==1'</span> <span class=\"token punctuation\">\\</span>\n  <span class=\"token operator\">|</span> <span class=\"token keyword\">while</span> <span class=\"token builtin class-name\">read</span> u<span class=\"token punctuation\">;</span> <span class=\"token keyword\">do</span> <span class=\"token builtin class-name\">echo</span> <span class=\"token string\">\"<span class=\"token variable\"><span class=\"token variable\">$(</span><span class=\"token function\">curl</span> <span class=\"token parameter variable\">-s</span> <span class=\"token parameter variable\">-o</span> /dev/null <span class=\"token parameter variable\">-w</span> <span class=\"token string\">'%{http_code}'</span> <span class=\"token string\">\"<span class=\"token variable\">$u</span>\"</span><span class=\"token variable\">)</span></span> <span class=\"token variable\">$u</span>\"</span><span class=\"token punctuation\">;</span> <span class=\"token keyword\">done</span></code></pre></div>\n<ol start=\"2\">\n<li><strong>JSON-LD 파싱 확인</strong>: 글 페이지의 <code class=\"language-text\">ld+json</code>을 JSON으로 파싱해 <code class=\"language-text\">@type</code> 목록을 본다.</li>\n<li><strong>사이트맵과 noindex의 일관성 확인</strong>: 사이트맵 안의 URL 중 <code class=\"language-text\">noindex</code>가 붙은 페이지가 0개인지 확인한다.</li>\n<li><strong>외부 검사 도구</strong>: 구글 <a href=\"https://search.google.com/test/rich-results\">리치 결과 테스트</a>와 <a href=\"https://validator.schema.org/\">Schema Markup Validator</a>로 문법 오류를 확인한다.</li>\n</ol>\n<h2 id=\"적용할-때-주의할-점\" style=\"position:relative;\"><a href=\"#%EC%A0%81%EC%9A%A9%ED%95%A0-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90\" aria-label=\"적용할 때 주의할 점 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>적용할 때 주의할 점</h2>\n<ul>\n<li><strong>구조화 데이터는 화면 내용과 일치해야 한다.</strong> 보이지 않는 정보를 마크업하면 무시되거나 불이익을 받을 수 있다.</li>\n<li><strong>메타 태그는 한 곳에서만 만든다.</strong> canonical처럼 플러그인이 이미 넣는 태그를 컴포넌트에서 또 넣으면 중복된다.</li>\n<li><strong>색인 기준은 하나의 소스에서 파생시킨다.</strong> 태그 페이지의 noindex와 사이트맵 제외가 같은 값을 보도록 설계했다.</li>\n<li><strong>GEO·AEO는 콘텐츠 품질을 대신하지 않는다.</strong> 구조화 데이터는 좋은 글을 기계가 더 잘 이해하게 도울 뿐, 얇은 글을 좋은 글로 바꿔 주지 않는다.</li>\n</ul>\n<h2 id=\"faq\" style=\"position:relative;\"><a href=\"#faq\" aria-label=\"faq permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>FAQ</h2>\n<p><strong>Q: Gatsby 2처럼 오래된 버전에서도 이 방식이 동작하나요?</strong><br>\nA: 동작한다. 이 글의 코드는 Gatsby 2.32와 react-helmet 기반에서 적용하고 빌드까지 확인한 것이다. Gatsby 4.19부터는 react-helmet 대신 내장 Head API를 쓰는 것이 권장되지만, 메타데이터를 구성하는 로직은 그대로 옮길 수 있다.</p>\n<p><strong>Q: FAQ 리치 결과가 거의 안 나온다면 FAQPage는 넣을 필요가 없나요?</strong><br>\nA: 검색 결과 화면만 보면 효과가 작다. 하지만 질문과 답의 짝을 명시적으로 제공한다는 점에서 답변 엔진이 글을 이해하는 데는 여전히 도움이 되고, 본문에 이미 있는 FAQ를 자동 변환하는 것이라 유지 비용도 거의 없다.</p>\n<p><strong>Q: llms.txt를 만들면 AI 검색에 인용되나요?</strong><br>\nA: 보장되지 않는다. llms.txt는 아직 제안 단계의 형식이고, 어떤 AI 서비스가 이를 읽는지는 서비스마다 다르다. 비용이 거의 들지 않는 선택지로 보고 추가하는 것이 적절하다.</p>\n<p><strong>Q: 태그 페이지를 아예 삭제하지 않고 noindex만 한 이유는 무엇인가요?</strong><br>\nA: 태그 페이지는 방문자가 관련 글을 찾는 탐색 수단으로 여전히 유용하다. <code class=\"language-text\">noindex, follow</code>로 두면 검색 결과에서는 빠지지만, 크롤러는 그 페이지의 링크를 따라 글을 발견할 수 있다.</p>\n<p><strong>Q: 글 3개라는 기준은 어떻게 정했나요?</strong><br>\nA: 정답이 있는 값은 아니다. 태그 페이지가 “여러 글을 묶어 보여 주는 가치”를 가지려면 최소 몇 편은 있어야 한다고 보고 3으로 정했다. 상수 하나로 관리하므로 사이트 상황에 맞게 조정하면 된다.</p>","tableOfContents":"<ul>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#seo-geo-aeo%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80\">SEO, GEO, AEO는 무엇이 다른가</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#%EC%A0%81%EC%9A%A9-%EC%A0%84-%EC%A7%84%EB%8B%A8\">적용 전 진단</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#1-seo-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9C%A0%ED%98%95%EC%9D%84-%EC%95%84%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C\">1. SEO 컴포넌트를 “페이지 유형”을 아는 컴포넌트로</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#2-json-ld%EB%8A%94-code-classlanguage-textgraphcode%EB%A1%9C-%EC%97%B0%EA%B2%B0%ED%95%9C%EB%8B%A4\">2. JSON-LD는 <code class=\"language-text\">@graph</code>로 연결한다</a></li>\n<li>\n<p><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#3-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4-faq%EC%97%90%EC%84%9C-faqpage%EB%A5%BC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A0%EB%8B%A4\">3. 마크다운 FAQ에서 FAQPage를 자동으로 만든다</a></p>\n<ul>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#faqpage%EC%97%90-%EB%8C%80%ED%95%9C-%ED%98%84%EC%8B%A4%EC%A0%81%EC%9D%B8-%EA%B8%B0%EB%8C%80%EC%B9%98\">FAQPage에 대한 현실적인 기대치</a></li>\n</ul>\n</li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#4-%EB%B9%8C%EB%93%9C-%EC%8B%9C-llmstxt-%EC%83%9D%EC%84%B1\">4. 빌드 시 llms.txt 생성</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#5-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5-%EC%A0%95%EB%A6%AC-927%EA%B0%9C%EC%97%90%EC%84%9C-264%EA%B0%9C%EB%A1%9C\">5. 사이트맵 정리: 927개에서 264개로</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#6-%EB%B0%B0%ED%8F%AC-%ED%9B%84%EC%97%90%EC%95%BC-%EB%B3%B4%EC%9D%B8-%EB%B2%84%EA%B7%B8-%EC%A1%B4%EC%9E%AC%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%8C%80%ED%91%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80\">6. 배포 후에야 보인 버그: 존재하지 않는 대표 이미지</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#%EA%B2%80%EC%A6%9D-%EB%B0%A9%EB%B2%95\">검증 방법</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#%EC%A0%81%EC%9A%A9%ED%95%A0-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90\">적용할 때 주의할 점</a></li>\n<li><a href=\"/javascript/Gatsby-%EB%B8%94%EB%A1%9C%EA%B7%B8-GEO-AEO-%EC%A0%81%EC%9A%A9%EA%B8%B0-JSON-LD-FAQPage-llms-txt-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5/#faq\">FAQ</a></li>\n</ul>","wordCount":{"words":1022},"fields":{"slug":"/javascript/Gatsby-블로그-GEO-AEO-적용기-JSON-LD-FAQPage-llms-txt-사이트맵/","image":null,"faq":[{"question":"Gatsby 2처럼 오래된 버전에서도 이 방식이 동작하나요?","answer":"동작한다. 이 글의 코드는 Gatsby 2.32와 react-helmet 기반에서 적용하고 빌드까지 확인한 것이다. Gatsby 4.19부터는 react-helmet 대신 내장 Head API를 쓰는 것이 권장되지만, 메타데이터를 구성하는 로직은 그대로 옮길 수 있다."},{"question":"FAQ 리치 결과가 거의 안 나온다면 FAQPage는 넣을 필요가 없나요?","answer":"검색 결과 화면만 보면 효과가 작다. 하지만 질문과 답의 짝을 명시적으로 제공한다는 점에서 답변 엔진이 글을 이해하는 데는 여전히 도움이 되고, 본문에 이미 있는 FAQ를 자동 변환하는 것이라 유지 비용도 거의 없다."},{"question":"llms.txt를 만들면 AI 검색에 인용되나요?","answer":"보장되지 않는다. llms.txt는 아직 제안 단계의 형식이고, 어떤 AI 서비스가 이를 읽는지는 서비스마다 다르다. 비용이 거의 들지 않는 선택지로 보고 추가하는 것이 적절하다."},{"question":"태그 페이지를 아예 삭제하지 않고 noindex만 한 이유는 무엇인가요?","answer":"태그 페이지는 방문자가 관련 글을 찾는 탐색 수단으로 여전히 유용하다. noindex, follow로 두면 검색 결과에서는 빠지지만, 크롤러는 그 페이지의 링크를 따라 글을 발견할 수 있다."},{"question":"글 3개라는 기준은 어떻게 정했나요?","answer":"정답이 있는 값은 아니다. 태그 페이지가 \"여러 글을 묶어 보여 주는 가치\"를 가지려면 최소 몇 편은 있어야 한다고 보고 3으로 정했다. 상수 하나로 관리하므로 사이트 상황에 맞게 조정하면 된다."}]},"frontmatter":{"title":"Gatsby 블로그 GEO·AEO 적용기 — JSON-LD, FAQPage 자동 생성, llms.txt, 사이트맵 정리","date":"2026년 9월 25일","dateISO":"2026-09-25T04:00:00.000Z","updatedISO":null,"category":"javascript","description":"검색이 AI 답변으로 옮겨 가는 흐름에 맞춰 Gatsby 기술 블로그에 적용한 GEO·AEO 작업을 실제 코드와 함께 정리한다. SEO 컴포넌트 확장, schema.org JSON-LD 그래프, 마크다운에서 FAQPage 자동 추출, 빌드 시 llms.txt 생성, 얇은 태그 페이지 noindex와 사이트맵 정리(927→264개)까지 다룬다.","tags":["Gatsby","SEO","AI 검색","구조화 데이터","JavaScript","Vercel","웹 개발"]}}},"pageContext":{"slug":"/javascript/Gatsby-블로그-GEO-AEO-적용기-JSON-LD-FAQPage-llms-txt-사이트맵/","previous":{"fields":{"slug":"/etc/도메인-만료로-블로그가-사라졌다-새-도메인-이전-SEO-애드센스-체크리스트/"},"frontmatter":{"title":"도메인 만료로 블로그가 사라졌다 — 새 도메인 이전 SEO·애드센스 체크리스트","tags":["도메인","SEO","애드센스","Vercel","Gatsby","배포","회고"]}},"next":null}},"staticQueryHashes":["213619243","3262363727"]}