The og:type Bug Three of My Astro Sites Quietly Shipped
요약
이 글은 Astro 사이트에서 블로그 포스트의 Open Graph(OG) 메타 태그가 잘못 설정되는 흔한 SEO 버그를 다룹니다. 일반적인 Astro 프로젝트 구조에서는 `BaseLayout`과 개별 `BlogLayout`이 각각 `og:type`을 정의하면서, 최종적으로 두 개의 충돌하는 OG 타입(`website`와 `article`)이 HTML에 포함됩니다. 소셜 미디어 플랫폼은 이 중 첫 번째 태그만 읽기 때문에, 블로그 포스트가 웹사이트 페이지로 오인되어 SEO 최적화에 문제가 발생합니다. 작성자는 이 문제를 발견하기 위해 빌드 과정에서 추가적인 유효성 검사 스크립트를 실행했으며, 해결책으로 `og:type`을 모든 레이아웃이 아닌 가장 상위의 `BaseLayout` 컴포넌트가 전담하고, 필요한 경우 props를 통해 오버라이딩하도록 구조를 개선해야 한다고 제안합니다.
핵심 포인트
- Open Graph(OG) 메타 태그는 소셜 미디어 플랫폼과 검색 엔진에 페이지 콘텐츠 유형을 알려주는 핵심 요소이다. (예: `website`, `article`)
- Astro와 같은 컴포넌트 기반 프레임워크에서 여러 레이아웃이 독립적으로 `og:type`을 정의할 경우, 충돌하는 메타 태그가 발생하여 SEO 오류를 유발한다.
- 소셜 플랫폼은 중복된 메타 태그가 있을 때 첫 번째로 발견되는 값만 읽기 때문에, 블로그 포스트가 웹사이트 페이지로 잘못 인식될 수 있다.
- 이 버그는 일반적인 렌더링 과정에서는 눈에 띄지 않으며, 별도의 SEO 검증 스크립트나 테스트를 통해서만 발견 가능하다.
- 해결책은 `og:type`의 책임을 최상위 레이아웃(`BaseLayout`)으로 통합하고, 필요한 경우 props를 통해 명시적으로 오버라이드하는 것이다.
나는 네 개의 Astro 사이트를 운영합니다. 그중 세 개는 몇 달간 동일한 SEO 버그를 가지고 있었습니다. 해당 사이트의 모든 블로그 포스트는 Twitter, Facebook, LinkedIn 에 이 페이지가 웹사이트 (website) 라고—not article 라고—알려보았습니다. 여기에는 무엇이 발생했는지, 왜 더 일찍 발견하지 못했는지, 그리고 첫 날부터 이를 감지할 수 있었을 한 줄의 빌드 체크를 설명합니다.
What "og:type" actually does
URL 을 Twitter 나 LinkedIn 에 붙여넣으면, 플랫폼은 해당 페이지를 가져오고 Open Graph meta 태그를 읽어서 어떤 카드를 보여줄지 결정합니다. 그중 가장 중요한 태그는 og:type 입니다. 이 태그는 URL 이 웹사이트 (website), article, 책 (book), 비디오 (video), 프로필 (profile) 인지 플랫폼에 알려줍니다. Twitter 는 article 과 website 에 대해 서로 다른 리치 카드를 표시합니다. Facebook 은 article 을 위해 게시 날짜와 저자를 표출합니다. LinkedIn 은 스니펫을 다르게 형식화합니다. 검색 엔진 또한 og:type 를 콘텐츠 분류에 대한 힌트로 소비합니다.
계약은 간단합니다: 페이지당 한 번만 출력하고, 해당 페이지에 맞는 올바른 값을 사용하세요.
The bug
일반적인 Astro 프로젝트에서 meta 태그는 모든 페이지를 감싸는 BaseLayout.astro 에 위치합니다. 내 BaseLayout 이 다음 줄을 가지고 있었습니다:
<meta property="og:type" content="website" />
이것은 홈 페이지, about 페이지, 블로그 인덱스에 대해 맞습니다. 좋습니다.
블로그 포스트에 대해서는 BlogLayout.astro 가 BaseLayout 을 감싸고 Astro 의 named slot 을 통해 article-specific 태그를 추가했습니다:
<BaseLayout {title} {description} {ogUrl}>
<Fragment slot="head">
<meta property="og:type" content="article" />
<meta property="article:published_time" content={date.toISOString()} />
</Fragment>
<slot />
</BaseLayout>
각 부분에서 각각은 올바른 것처럼 보입니다. 블로그 레이아웃은 블로그 포스트에 article 태그를 추가합니다.
빌드를 실행하고 렌더링된 HTML 을 확인하세요:
<meta property="og:type" content="website" />
Why this is invisible without checking
일반적인 사용에서는 이 버그를 결코 보지 못합니다:
- 페이지가 정상적으로 렌더링됩니다.
- 방문자는 이를 알지 못합니다.
- 빌드가 성공합니다.
- 경고가 없습니다.
- Astro 는 중복 meta 태그를 표시하지 않습니다.
- 그들은 유효한 HTML 입니다.
- Open Graph 파서들은 중복에 대해 에러를 던지지 않습니다—they just take the first match.
Twitter 에서 URL 을 공유할 때, 카드는 일종의 방식으로 작동합니다. 제목, 설명, 이미지는 여전히 올바른 것이기 때문입니다.
단, 깨지는 것은 typ
e signal. Your articles look like landing pages to every machine that consumes them, including Google's structured-data understanding. I caught this on the third site only because I started running a small validation script during my SEO audit. The first two sites had been running for weeks. How three sites all got it
The mechanism is identical across the three repos. Two cooperating layouts each emit one og:type , neither one knows about the other, and the result is two emissions. Once you build a site this way, every variant you start later from the same template inherits the bug.
I copied the layout structure from kenimoto.dev to a PC selection site, then to a whisky media site, then to the LLMO Framework documentation site. The bug rode along every time.
The fix: lift og:type into a prop
The right shape is for BaseLayout to own og:type exclusively, with a default of website and a prop override for pages that need a different value.
BaseLayout.astro :
interface Props {
title: "string;"
description: "string;"
ogUrl: string;
ogType?: 'website' | 'article' | 'book' | 'profile' | 'video.other';
}
const { title, description, ogUrl, ogType = 'website' } = Astro.props;
<head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:url" content={ogUrl} />
<meta property="og:type" content={ogType} />
</head>
BlogLayout.astro then passes ogType="article" and removes its own emission:
import BaseLayout from './BaseLayout.astro';
const { title, description, canonicalUrl, date, tags } = Astro.props;
<BaseLayout title={title} description={description} ogUrl={canonicalUrl} ogType="article" >
<Fragment slot="head">
<meta property="article:published_time" content={date.toISOString()} />
{tags.map(tag => <meta property="article:tag" content={tag} />)}
</Fragment>
<slot />
</BaseLayout>
A BookLayout.astro does the same with ogType="book" . Now og:type is emitted exactly once, and the value matches the page subject.
The build-time check that would have caught it
After the fix I added a small script to the build pipeline that walks every generated HTML file in dist/ and counts how many og:type tags each has.
// scripts/verify-meta.mjs
import { readdir , readFile } from ' node:fs/promises ' ;
import { join } from ' node:path ' ;
async function * walk ( dir ) {
for ( const entry of await readdir ( dir , { withFileTypes : true })) {
const path = join ( dir , entry . name );
if ( e
const entries = await readdir ( path ); for ( const entry of entries ) { if ( entry . isDirectory ()) yield * walk ( path ); else if ( entry . name . endsWith ( ' .html ' )) yield path ; } } const failures = []; for await ( const file of walk ( ' dist ' )) { const html = await readFile ( file , ' utf8 ' ); const count = ( html . match ( /property=
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기