Execution skill for search optimization of GitHub Pages-based sites.
Server configuration not possible → HTML <head>, static files, and client-side JS only.
When to Apply
- When publishing a new page/post
- When requesting an SEO audit
- When requesting improved search visibility/traffic
- When working on structured data/meta tags
Target Sites
| Site | Stack | Content |
|---|---|---|
| eastsea.monster | GitHub Pages + Jekyll/Hugo | 210 tools, 137 blog posts |
| games.eastsea.xyz | GitHub Pages (static) | 101 games |
1. Technical SEO
1.1 sitemap.xml
Jekyll은 jekyll-sitemap auto-generated by plugin. If customization is needed:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in site.pages %}
{% unless page.exclude_from_sitemap %}
<url>
<loc>{{ site.url }}{{ page.url | remove: "index.html" }}</loc>
<lastmod>{{ page.last_modified_at | date: "%Y-%m-%d" }}</lastmod>
<changefreq>{{ page.changefreq | default: "monthly" }}</changefreq>
<priority>{{ page.priority | default: "0.5" }}</priority>
</url>
{% endunless %}
{% endfor %}
</urlset>
Large sites (>500 URLs): split sitemap index
<!-- sitemap-index.xml -->
<sitemapindex xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>https://eastsea.monster/sitemap-tools.xml</loc></sitemap>
<sitemap><loc>https://eastsea.monster/sitemap-blog.xml</loc></sitemap>
</sitemapindex>
1.2 robots.txt
User-agent: *
Allow: /
Sitemap: https://eastsea.monster/sitemap.xml
# 불필요 크롤링 차단
Disallow: /assets/js/
Disallow: /assets/css/
Disallow: /404.html
1.3 Canonical URL
Required for all pages <head>:
<link rel="canonical" href="{{ page.url | absolute_url }}" />
1.4 Required Meta Tags Template
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title }} | {{ site.title }}</title>
<meta name="description" content="{{ page.description | default: site.description | truncate: 160 }}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="{{ page.url | absolute_url }}">
<!-- Open Graph -->
<meta property="og:type" content="{{ page.og_type | default: 'website' }}">
<meta property="og:title" content="{{ page.title }}">
<meta property="og:description" content="{{ page.description | default: site.description | truncate: 200 }}">
<meta property="og:url" content="{{ page.url | absolute_url }}">
<meta property="og:image" content="{{ page.image | default: site.default_image | absolute_url }}">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:locale" content="ko_KR">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ page.title }}">
<meta name="twitter:description" content="{{ page.description | truncate: 200 }}">
<meta name="twitter:image" content="{{ page.image | default: site.default_image | absolute_url }}">
</head>
1.5 JSON-LD Structured Data — WebSite
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "{{ site.title }}",
"url": "{{ site.url }}",
"potentialAction": {
"@type": "SearchAction",
"target": "{{ site.url }}/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
2. Content SEO
2.1 Title Optimization Formula
| Pattern | Example |
|---|---|
| `[Tool Name] — [Core Feature] | Free Online Tool` |
[게임명]: [장르] 게임 — 브라우저에서 바로 플레이 |
Tower Defense: 전략 게임 — 브라우저에서 바로 플레이 |
[주제] — [구체적 내용] (2026) |
Python 비동기 처리 — asyncio 완전 가이드 (2026) |
Rules:
- Within 60 characters (30 characters for Korean)
- Place core keyword first
- Pipe (
|) or dash (—) as separator - Unique title for each page
2.2 Description Optimization
- Within 155 characters (80 characters for Korean)
- Include call-to-action phrases ("Right now", "for free")
- Include core keyword naturally 1-2 times
2.3 Keyword Research Patterns
# Google Autocomplete 크롤링 (Python)
import requests, json
def get_suggestions(keyword):
url = f"https://suggestqueries.google.com/complete/search?client=firefox&q={keyword}"
return json.loads(requests.get(url).text)[1]
# 도구 키워드 확장
for tool in ["json formatter", "base64 encoder", "color picker"]:
print(f"=== {tool} ===")
print(get_suggestions(tool))
print(get_suggestions(f"{tool} online"))
print(get_suggestions(f"{tool} free"))
2.4 Internal Link Strategy
- Tool → Related tools: "If you were looking for JSON Formatter, also check JSON Validator"
- Blog → Tool: Naturally link related tools within the post
- Category hub: Category landing pages link to all sub-tools
- Breadcrumb:
홈 > 카테고리 > 도구명structure
3. Tool Page SEO
3.1 Tool Front Matter Standard
---
layout: tool
title: "JSON Formatter"
description: "JSON 데이터를 보기 좋게 정리하고 유효성을 검증하는 무료 온라인 도구"
category: developer
tags: [json, formatter, validator, developer-tools]
image: /assets/tools/json-formatter-og.png
priority: 0.8
changefreq: monthly
---
3.2 Tool JSON-LD (SoftwareApplication)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "{{ page.title }}",
"description": "{{ page.description }}",
"url": "{{ page.url | absolute_url }}",
"applicationCategory": "{{ page.category | capitalize }}Tool",
"operatingSystem": "Web Browser",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Organization",
"name": "{{ site.title }}",
"url": "{{ site.url }}"
}
}
</script>
3.3 Category Landing Page Pattern
---
layout: category
title: "개발자 도구 모음 — 무료 온라인 개발 유틸리티"
description: "JSON, Base64, URL 인코딩 등 개발에 필요한 도구를 브라우저에서 바로 사용하세요."
category: developer
---
<!-- BreadcrumbList JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "홈", "item": "{{ site.url }}"},
{"@type": "ListItem", "position": 2, "name": "개발자 도구", "item": "{{ page.url | absolute_url }}"}
]
}
</script>
<!-- ItemList for category tools -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": "{{ page.title }}",
"itemListElement": [
{% for tool in site.tools | where: "category", page.category %}
{
"@type": "ListItem",
"position": {{ forloop.index }},
"url": "{{ tool.url | absolute_url }}",
"name": "{{ tool.title }}"
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
</script>
3.4 Tool Meta Auto-Generation Script
#!/usr/bin/env python3
"""도구 페이지 메타태그 일괄 생성/검증"""
import os, yaml, re
from pathlib import Path
TOOLS_DIR = Path("_tools") # or tools/
REQUIRED = ["title", "description", "category", "tags"]
def audit_tools():
issues = []
for f in TOOLS_DIR.glob("*.md"):
content = f.read_text()
fm = yaml.safe_load(content.split("---")[1])
for field in REQUIRED:
if field not in fm or not fm[field]:
issues.append(f"❌ {f.name}: missing '{field}'")
if fm.get("description") and len(fm["description"]) > 160:
issues.append(f"⚠️ {f.name}: description > 160 chars ({len(fm['description'])})")
if fm.get("title") and len(fm["title"]) > 60:
issues.append(f"⚠️ {f.name}: title > 60 chars")
return issues
if __name__ == "__main__":
for issue in audit_tools():
print(issue)
4. Game Page SEO
4.1 Game JSON-LD (Game + VideoGame Schema)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoGame",
"name": "{{ page.title }}",
"description": "{{ page.description }}",
"url": "{{ page.url | absolute_url }}",
"image": "{{ page.screenshot | absolute_url }}",
"genre": "{{ page.genre }}",
"gamePlatform": ["Web Browser", "Mobile Browser"],
"applicationCategory": "Game",
"operatingSystem": "Any",
"playMode": "{{ page.play_mode | default: 'SinglePlayer' }}",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"author": {
"@type": "Organization",
"name": "EastSea Games",
"url": "https://games.eastsea.xyz"
}
{% if page.rating %},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{ page.rating }}",
"ratingCount": "{{ page.rating_count }}",
"bestRating": "5",
"worstRating": "1"
}
{% endif %}
}
</script>
4.2 Game Screenshot Optimization
<!-- 모든 스크린샷에 의미있는 alt 텍스트 -->
<img src="/games/tower-defense/screenshot-1.webp"
alt="Tower Defense 게임 — 3라운드 보스전 화면, 타워 배치 전략"
width="800" height="450" loading="lazy">
alt text rules:
게임명 — 구체적 장면 설명format- Include keywords naturally
- Within 125 characters
4.3 Game Front Matter
---
layout: game
title: "Tower Defense — 무료 전략 타워 디펜스 게임"
description: "브라우저에서 바로 플레이하는 무료 타워 디펜스. 50개 스테이지, 다양한 타워 조합 전략."
genre: Strategy
play_mode: SinglePlayer
screenshot: /games/tower-defense/og-image.png
tags: [tower-defense, strategy, free-game, browser-game]
priority: 0.7
---
5. Blog SEO
5.1 Post JSON-LD (Article)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "{{ page.title }}",
"description": "{{ page.description }}",
"image": "{{ page.image | absolute_url }}",
"datePublished": "{{ page.date | date_to_xmlschema }}",
"dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
"author": {
"@type": "Person",
"name": "{{ page.author | default: site.author }}"
},
"publisher": {
"@type": "Organization",
"name": "{{ site.title }}",
"logo": {
"@type": "ImageObject",
"url": "{{ site.logo | absolute_url }}"
}
},
"mainEntityOfPage": "{{ page.url | absolute_url }}"
}
</script>
5.2 Series Links
{% if page.series %}
<nav class="series-nav" aria-label="시리즈 네비게이션">
<h3>📚 {{ page.series }} 시리즈</h3>
<ol>
{% assign series_posts = site.posts | where: "series", page.series | sort: "series_order" %}
{% for post in series_posts %}
<li {% if post.url == page.url %}class="current"{% endif %}>
<a href="{{ post.url }}">{{ post.title }}</a>
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
5.3 Breadcrumb
<nav aria-label="breadcrumb">
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/"><span itemprop="name">홈</span></a>
<meta itemprop="position" content="1">
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/blog/"><span itemprop="name">블로그</span></a>
<meta itemprop="position" content="2">
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<span itemprop="name">{{ page.title }}</span>
<meta itemprop="position" content="3">
</li>
</ol>
</nav>
6. Performance Measurement
6.1 Google Search Console Integration
<meta name="google-site-verification" content="YOUR_CODE">add- Submit sitemap.xml: Search Console → Sitemaps → Enter URL
- Monitor indexing status: Check the coverage report weekly
6.2 Core Web Vitals Optimization (GitHub Pages)
<!-- 이미지 최적화: WebP + lazy loading -->
<img src="image.webp" alt="설명" width="800" height="450"
loading="lazy" decoding="async">
<!-- CSS 인라인 (Critical Path) -->
<style>/* 첫 화면에 필요한 최소 CSS */</style>
<link rel="preload" href="/assets/css/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- JS defer -->
<script src="/assets/js/app.js" defer></script>
<!-- 폰트 최적화 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
6.3 Tracking Checklist
- [ ] Google Search Console 등록 및 sitemap 제출
- [ ] 색인된 페이지 수 vs 실제 페이지 수 비교
- [ ] 404 에러 페이지 목록 → 리다이렉트 또는 수정
- [ ] Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
- [ ] 모바일 사용성 오류 0건
- [ ] 주간: 상위 검색어, 클릭률(CTR), 평균 게재순위 기록
7. SEO Audit Automation
7.1 Detecting Missing Meta Tags
#!/bin/bash
# seo-audit.sh — 빌드된 HTML에서 SEO 필수 요소 검증
SITE_DIR="${1:-./_site}"
echo "=== SEO Audit: $SITE_DIR ==="
find "$SITE_DIR" -name "*.html" | while read f; do
rel="${f#$SITE_DIR}"
# title 태그 체크
if ! grep -q '<title>' "$f"; then
echo "❌ MISSING <title>: $rel"
fi
# meta description 체크
if ! grep -q 'name="description"' "$f"; then
echo "❌ MISSING meta description: $rel"
fi
# canonical 체크
if ! grep -q 'rel="canonical"' "$f"; then
echo "⚠️ MISSING canonical: $rel"
fi
# og:title 체크
if ! grep -q 'og:title' "$f"; then
echo "⚠️ MISSING og:title: $rel"
fi
# og:image 체크
if ! grep -q 'og:image' "$f"; then
echo "⚠️ MISSING og:image: $rel"
fi
# JSON-LD 체크
if ! grep -q 'application/ld+json' "$f"; then
echo "⚠️ MISSING JSON-LD: $rel"
fi
# img alt 체크
if grep -qP '<img[^>]+(?!alt=)' "$f" 2>/dev/null; then
count=$(grep -cP '<img(?![^>]*alt=)' "$f" 2>/dev/null || echo 0)
[ "$count" -gt 0 ] && echo "⚠️ $count img(s) without alt: $rel"
fi
done
echo "=== Audit Complete ==="
7.2 Broken Link Check
#!/bin/bash
# broken-links.sh — 내부 링크 검증
SITE_DIR="${1:-./_site}"
ERRORS=0
grep -roh 'href="/[^"]*"' "$SITE_DIR" | sort -u | while read link; do
path=$(echo "$link" | sed 's/href="//;s/"//')
# 앵커 제거
path="${path%%#*}"
# 쿼리스트링 제거
path="${path%%\?*}"
target="$SITE_DIR$path"
if [ ! -f "$target" ] && [ ! -f "${target}index.html" ] && [ ! -d "$target" ]; then
echo "🔗 BROKEN: $path"
ERRORS=$((ERRORS + 1))
fi
done
echo "Total broken links: $ERRORS"
7.3 Python Integrated Audit Script
#!/usr/bin/env python3
"""seo_audit.py — 종합 SEO 감사"""
from pathlib import Path
from bs4 import BeautifulSoup
import sys
def audit_html(filepath: Path) -> list[str]:
issues = []
soup = BeautifulSoup(filepath.read_text(), "html.parser")
rel = str(filepath)
# Title
title = soup.find("title")
if not title or not title.string:
issues.append(f"❌ No <title>: {rel}")
elif len(title.string) > 60:
issues.append(f"⚠️ Title > 60 chars: {rel}")
# Meta description
desc = soup.find("meta", attrs={"name": "description"})
if not desc or not desc.get("content"):
issues.append(f"❌ No meta description: {rel}")
elif len(desc["content"]) > 160:
issues.append(f"⚠️ Description > 160 chars: {rel}")
# Canonical
if not soup.find("link", attrs={"rel": "canonical"}):
issues.append(f"⚠️ No canonical: {rel}")
# Open Graph
for og in ["og:title", "og:description", "og:image"]:
if not soup.find("meta", attrs={"property": og}):
issues.append(f"⚠️ No {og}: {rel}")
# Images without alt
for img in soup.find_all("img"):
if not img.get("alt"):
issues.append(f"⚠️ img without alt: {rel} → {img.get('src', '?')[:50]}")
# JSON-LD
if not soup.find("script", attrs={"type": "application/ld+json"}):
issues.append(f"⚠️ No JSON-LD: {rel}")
return issues
if __name__ == "__main__":
site_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("_site")
all_issues = []
for html in site_dir.rglob("*.html"):
all_issues.extend(audit_html(html))
for issue in sorted(all_issues):
print(issue)
print(f"\n📊 Total issues: {len(all_issues)}")
Quick Reference: GitHub Pages SEO Constraints
| Possible | Not possible |
|---|---|
HTML <head> meta tags |
.htaccess / server redirects |
| Static sitemap.xml, robots.txt | Dynamic sitemap generation |
| JSON-LD structured data | Server-side rendering |
| Client-side JS redirects | Custom HTTP headers |
<link rel="canonical"> |
X-Robots-Tag headers |
| Serving WebP images | Image CDN transformation |
| Optimize at build time with GitHub Actions | Edge server configuration |
Redirect alternatives: <meta http-equiv="refresh" content="0; url=/new-path"> + JS window.location