포스트

GitHub 블로그 도메인 변경부터 SEO/GEO 전면 개편까지

GitHub Pages 블로그 도메인 변경 후 Google Search Console 꼬임 해결, JSON-LD 구조화 데이터(TechArticle, HowTo, FAQPage, BreadcrumbList) 구현, IndexNow 자동 제출까지 SEO/GEO 전면 개편 과정.

GitHub 블로그 도메인 변경부터 SEO/GEO 전면 개편까지

TL;DR

  • 예전 GitHub 블로그 도메인(ooyuo.github.io)을 삭제했다가 다시 만들려니 Google Search Console이 꼬여버렸다.
  • 어차피 신입 때 만든 계정명도 마음에 안 들었어서, GitHub 아이디까지 통째로 바꿨다.
  • 이왕 새로 시작하는 거 제대로 해보자 싶어 JSON-LD 구조화 데이터부터 IndexNow 자동 제출까지 SEO/GEO를 전면 개편했다.
  • 핵심은 프론트매터만 채우면 구조화 데이터가 자동으로 붙는 구조를 만드는 것이었다. 새 글 쓸 때 SEO 때문에 따로 뭘 할 일이 없게.

문제 인식

몇 년 전에 ooyuo.github.io로 GitHub Pages 블로그를 운영하다, 어느 순간 티스토리로 갈아탔다. 그러면서 GitHub 레포도 같이 삭제했고, 도메인은 한동안 그냥 방치해뒀다.

그러다 다시 GitHub Pages로 돌아왔다. 같은 레포명으로 블로그를 다시 만들었는데, 여기서 문제가 터졌다. Google Search Console에 이전 도메인이 여전히 남아 있었던 것이다. 이미 삭제한 속성인데 새 속성으로 등록하려니 소유권 인증부터 꼬이기 시작했다.

결국 GitHub 아이디 자체를 ooyuo에서 z9-durun으로 갈아엎었다. 사실 이전 아이디가 신입 때 아무 생각 없이 만든 거라 오래전부터 바꾸고 싶기도 했다. 도메인이 z9-durun.github.io로 바뀌면서 Search Console 문제는 풀렸다.

그런데 나중에 알았다 — Google Search Console에 ‘주소 변경’ 도구가 따로 있다. 기존 속성에서 새 속성으로의 이전을 Google에 직접 알려주는 기능인데, 이미 레포를 삭제하고 아이디까지 다 바꾼 뒤였다. 알았으면 좀 더 깔끔하게 처리했을 일이다.


해결 방향 설계

도메인을 갈아엎고 나니 욕심이 났다. 이왕 새로 시작하는 거 SEO를 제대로 해보자. 이전 회사에서 GA4를 다뤘던 경험이 있어 웹 분석 자체는 익숙했지만, 블로그에 구조화 데이터(GEO)까지 제대로 적용해 본 적은 없었다.

“진짜 SEO/GEO를 제대로 하면 개인 블로그도 검색엔진에 잘 타는 걸까?”

이게 이번 개편의 동기였다. 메타태그 몇 개 넣는 수준이 아니라, Schema.org 구조화 데이터를 제대로 설계해서 검색엔진이 내 콘텐츠를 정확히 이해하도록 만들어보고 싶었다.

목표는 네 가지였다.

  1. 구조화 데이터 레이어 — TechArticle, HowTo, FAQPage 등 콘텐츠 특성에 맞는 Schema.org 타입 적용
  2. 크롤링/인덱싱 최적화 — robots.txt, sitemap, IndexNow로 검색엔진이 빠르게 새 글을 발견하도록
  3. 메타태그 정비 — Open Graph, Twitter Card 이미지 보정, 시맨틱 HTML 수정
  4. 프론트매터 표준화 — 포스트별로 구조화 데이터가 자동 활성화되는 구조

전체 구조를 그려보면 이런 모양이다.

graph TB
    subgraph Layer3["🔍 크롤링/인덱싱"]
        robots["robots.txt"]
        sitemap["sitemap.xml"]
        feed["feed.xml"]
        indexnow["IndexNow API"]
    end

    subgraph Layer2["📊 구조화 데이터 (GEO)"]
        tech["TechArticle"]
        howto["HowTo"]
        faqpage["FAQPage"]
        website["WebSite + SearchAction"]
        bread["BreadcrumbList"]
    end

    subgraph Layer1["🏷️ 메타태그 (SEO)"]
        og["Open Graph"]
        twitter["Twitter Card"]
        canonical["Canonical URL"]
        seotag["jekyll-seo-tag"]
    end

    Layer1 --> Browser["브라우저/SNS"]
    Layer2 --> SearchEngine["검색엔진 리치 결과"]
    Layer3 --> Crawler["검색엔진 크롤러"]

구현

1. _config.yml — 기반 설정

도메인 변경과 함께 SEO 기반 설정을 정리했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
url: "https://z9-durun.github.io"

github:
  username: z9-durun

social:
  name: chole
  email: z9.durun@gmail.com
  links:
    - https://github.com/z9-durun

webmaster_verifications:
  google: YOUR_GOOGLE_VERIFICATION_CODE

analytics:
  google:
    id: "G-XXXXXXXXXX"

plugins:
  - jekyll-sitemap   # /sitemap.xml 자동 생성
  - jekyll-feed      # /feed.xml 자동 생성 (신규 추가)

jekyll-sitemap은 원래 있었고, jekyll-feed를 새로 추가했다. RSS 피드도 검색엔진이 새 콘텐츠를 발견하는 또 다른 경로다.

2. robots.txt — 크롤러 안내

기존에는 robots.txt가 아예 없었다. 새로 만들었다.

1
2
3
4
User-agent: *
Allow: /

Sitemap: https://z9-durun.github.io/sitemap.xml

별거 없어 보이지만, 크롤러에게 sitemap 위치를 알려주는 단일 진입점 역할을 한다.

3. JSON-LD 구조화 데이터 — 가장 공들인 부분

이번 개편에서 제일 시간을 많이 쓴 부분이다. _includes/seo-jsonld.html을 만들고, 5가지 Schema.org 타입을 조건부로 출력하도록 설계했다.

페이지 타입에 따라 어떤 스키마가 붙는지 정리하면 이렇다.

flowchart LR
    Page["페이지 진입"]

    Page --> IsPost{포스트?}
    Page --> IsHome{홈?}
    Page --> Always["BreadcrumbList\n(모든 페이지)"]

    IsPost -->|Yes| TA["TechArticle"]
    IsPost -->|Yes| HasHowto{howto 프론트매터?}
    IsPost -->|Yes| HasFaq{faq 프론트매터?}

    HasHowto -->|Yes| HT["HowTo"]
    HasFaq -->|Yes| FAQ["FAQPage"]

    IsHome -->|Yes| WS["WebSite\n+ SearchAction"]

TechArticle (모든 포스트)

흔히 쓰이는 BlogPosting 대신 TechArticle 타입을 골랐다. 기술 블로그니까 더 정확한 시그널을 검색엔진에 보낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{% if page.layout == 'post' %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "{{ page.title | escape }}",
  "description": "{{ page.description | default: page.excerpt | strip_html | truncate: 160 | escape }}",
  "inLanguage": "{{ site.lang | default: 'ko-KR' }}",
  "wordCount": {{ page.content | number_of_words }},
  "articleSection": "{{ page.categories | join: ', ' }}",
  "author": {
    "@type": "Person",
    "name": "{{ site.social.name }}",
    "jobTitle": "Frontend Developer",
    "knowsAbout": ["React", "React Native", "TypeScript", "Frontend Development"],
    "sameAs": [
      {% for link in site.social.links %}"{{ link }}"{% unless forloop.last %},{% endunless %}{% endfor %}
    ]
  },
  "datePublished": "{{ page.date | date_to_xmlschema }}",
  "dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
  "keywords": "{{ page.tags | join: ', ' }}"
}
</script>
{% endif %}

포인트는 inLanguage, wordCount, articleSection, 저자의 jobTitleknowsAbout까지 채운 부분이다. 검색엔진이 콘텐츠의 언어·분량·분야·저자 전문성을 한 번에 파악한다.

HowTo (프론트매터로 제어)

포스트 프론트매터에 howto: 키를 넣으면 HowTo 구조화 데이터가 알아서 생성된다.

1
2
3
4
5
6
7
8
howto:
  name: "Storybook 인터랙션 테스트를 함수형으로 설계하는 방법"
  time: "PT45M"
  steps:
    - name: "TestFlow 러너 만들기"
      text: "테스트 컨텍스트를 관리하고 스텝을 순차 실행하는 createTestFlow 함수를 작성합니다."
    - name: "공통 인터랙션 함수 추출"
      text: "자주 사용하는 인터랙션을 재사용 가능한 함수로 분리합니다."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{% if page.howto %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "{{ page.howto.name | default: page.title | escape }}",
  "totalTime": "{{ page.howto.time | default: 'PT30M' }}",
  "step": [
    {% for step in page.howto.steps %}
    {
      "@type": "HowToStep",
      "position": {{ forloop.index }},
      "name": "{{ step.name | escape }}",
      "text": "{{ step.text | escape }}"
    }{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ]
}
</script>
{% endif %}

이 구조 덕분에 새 포스트는 프론트매터만 채우면 끝이다. 템플릿을 건드릴 일이 없다.

FAQPage도 동일한 패턴이다. 프론트매터에 faq: 키를 넣으면 FAQ 리치 결과를 노릴 수 있다.

1
2
3
faq:
  - question: "GitHub Pages 도메인 변경  Search Console은?"
    answer: "주소 변경 도구를 사용하면 됩니다..."

WebSite + SearchAction (홈 페이지)

홈 페이지에는 사이트 내 검색 기능을 구조화 데이터로 노출했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% if page.layout == 'home' or page.url == '/' %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "{{ site.title }}",
  "url": "{{ site.url }}",
  "potentialAction": {
    "@type": "SearchAction",
    "target": {
      "@type": "EntryPoint",
      "urlTemplate": "{{ site.url }}/search/?q={search_term_string}"
    },
    "query-input": "required name=search_term_string"
  }
}
</script>
{% endif %}

BreadcrumbList는 모든 페이지에 출력해서 검색 결과에서 사이트 구조를 보여준다.

4. Open Graph / 메타태그 정비

head.html에서 두 가지를 손봤다.

첫째, viewport 메타태그에서 user-scalable=no를 제거했다. 접근성(A11Y)과 Core Web Vitals 모두에 악영향을 주는 설정이다.

1
2
3
4
5
<!-- Before -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">

<!-- After -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">

둘째, 포스트별 이미지가 없을 때 social_preview_imagesummary_large_image 카드로 자동 업그레이드하도록 보정했다.

5. 시맨틱 HTML 수정

홈 페이지의 포스트 카드 제목이 <h1>으로 되어 있었다. 페이지당 <h1>은 하나여야 한다는 원칙이 있으니 <h2>로 바꿨다.

1
2
3
4
5
<!-- Before -->
<h1 class="card-title my-2 mt-md-0">{{ post.title }}</h1>

<!-- After -->
<h2 class="card-title my-2 mt-md-0">{{ post.title }}</h2>

작은 변경이지만, 검색엔진이 헤딩 계층 구조를 올바르게 해석하는 데 영향을 준다.

6. IndexNow 자동 제출

배포할 때마다 수동으로 색인 요청하는 건 너무 귀찮다. GitHub Actions 워크플로우에 indexnow job을 추가했다.

전체 배포 파이프라인은 이런 흐름이다.

flowchart LR
    A["git push"] --> B["Jekyll Build"]
    B --> C["HTML 검증"]
    C --> D["GitHub Pages\n배포"]
    D --> E["30초 대기"]
    E --> F["sitemap.xml에서\nURL 추출"]
    F --> G["IndexNow API\n제출"]
    G --> H["Bing/Yandex\n색인 요청"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
indexnow:
  runs-on: ubuntu-latest
  needs: deploy
  steps:
    - name: Wait for deployment to propagate
      run: sleep 30

    - name: Submit URLs to IndexNow
      run: |
        URLS=$(curl -s "https://z9-durun.github.io/sitemap.xml" \
          | grep -oP '(?<=<loc>)[^<]+' | head -20)

        URL_JSON=$(echo "$URLS" | jq -R -s -c 'split("\n") | map(select(length > 0))')

        curl -X POST "https://api.indexnow.org/IndexNow" \
          -H "Content-Type: application/json; charset=utf-8" \
          -d "{
            \"host\": \"z9-durun.github.io\",
            \"key\": \"fadc0d58c30a4a3ca2cc3625b7761549\",
            \"urlList\": $URL_JSON
          }"

배포 완료 30초 후에 sitemap에서 URL을 추출해 IndexNow API에 자동 제출하는 구조다. Bing, Yandex 등 IndexNow를 지원하는 검색엔진에 바로 색인 요청이 들어간다.

7. 포스트 프론트매터 표준화

기존 포스트 전체에 다음 필드들을 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
title: "포스트 제목"
description: "검색 결과 스니펫에 노출될 150자 내외 설명"
date: 2024-12-05
categories:
  - Development
  - Git
tags:
  - Git
  - Migration
image:
  path: /assets/img/posts/2024-12-05/hero.webp
  show: false
last_modified_at: 2024-12-05
howto:                    # HowTo 구조화 데이터 (해당하는 글만)
  name: "방법 이름"
  time: "PT20M"
  steps:
    - name: "단계"
      text: "설명"
---

description은 SEO 메타 설명으로 쓰이고, last_modified_at은 JSON-LD의 dateModified에 들어간다. howto가 있으면 HowTo 리치 결과를, faq가 있으면 FAQ 리치 결과를 자동으로 노리는 구조다.


적용 결과

개편하고 나서 달라진 점들.

  • 5가지 Schema.org 타입이 페이지 특성에 맞게 자동으로 출력된다. Google 리치 결과 테스트에서 확인 가능.
  • 프론트매터만 채우면 구조화 데이터가 활성화된다. 템플릿 건드릴 필요 없이 howto:, faq: 키만 추가하면 끝.
  • 배포하면 자동으로 IndexNow에 URL이 제출된다. 수동 색인 요청은 이제 안 한다.
  • Open Graph 이미지가 모든 페이지에서 정상 동작한다. SNS 공유 시 의도한 이미지가 노출된다.
  • 홈 페이지의 헤딩 계층 구조가 정리돼서 크롤러가 페이지 구조를 제대로 해석한다.

한계와 트레이드오프

Google Search Console의 sitemap.xml 가져오기 실패 — 새 속성을 등록하고 sitemap을 제출했는데 “가져올 수 없음” 상태가 떴다. 브라우저에서 직접 sitemap.xml을 열면 정상이고, robots.txt에도 위치가 명시되어 있는데도 그랬다. 새 도메인에 대한 Google의 초기 크롤링 지연으로 보인다 — 보통 3~7일이 걸린다고 한다. 일단 기다려보는 수밖에 없다.

도메인 변경 후 이전 URL의 크롤링 히스토리는 사라진다. Search Console의 ‘주소 변경’ 도구를 썼으면 이전 도메인의 검색 순위를 일부 승계할 수도 있었는데, 이미 레포를 삭제한 뒤에 알았다. 새 도메인에서 처음부터 쌓아가야 한다.

구조화 데이터가 검색 순위에 영향을 주는지는 솔직히 모른다. Google은 “구조화 데이터는 랭킹 시그널이 아니다”라고 못 박는다. 다만 리치 결과로 CTR이 올라가면 결국 도움은 된다. 얼마나 효과가 있는지는 데이터가 쌓여봐야 알 일이다.

IndexNow는 Google을 지원하지 않는다. Bing, Yandex 등만 지원하고 Google은 자체 크롤링 정책을 고수한다. Google 색인 속도에는 도움이 안 되지만, Bing 쪽에서는 의미가 있다.


마무리

도메인 변경이라는 삽질에서 시작했지만, 덕분에 블로그 SEO/GEO 기반을 제대로 잡았다. 결국 만들고 싶었던 건 프론트매터만 채우면 구조화 데이터가 알아서 붙는 구조다. 새 글 쓸 때 SEO 때문에 따로 손댈 게 없다.

Search Console 데이터가 쌓이면 실제 검색 유입 변화를 추적해볼 생각이다. 이 정도 수준의 SEO/GEO가 개인 블로그에 진짜 차이를 만드는지, 그 결과도 나중에 글로 남길 예정이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.