고스트 CMS 다국어 블로그: 8. #multi 포스트 hreflang 자동화

고스트 CMS 다국어 블로그: 8. #multi 포스트 hreflang 자동화
💡
Managed Ghost hosting 서비스에서는 Ghost Core를 수정하거나 custom Handlebars helper를 등록할 수 없다. 이 제약 안에서 반복적인 Code Injection을 없애기 위해 선택한 방식이다.

포스트마다 Ghost Admin의 Code Injection에 hreflang을 직접 넣을 수는 있다. 그러나 번역 포스트를 발행할 때마다 영어 URL과 한국어 URL을 양쪽 포스트에 반복해서 입력해야 한다. URL을 잘못 복사하거나 한쪽 포스트에만 넣을 가능성도 있다.

0.0.8에서는 이 작업을 테마가 자동으로 처리하도록 변경했다. 함께 드러난 태그 순서 의존 문제도 제거했다.

Ghost Admin에서 확인한 한국어 번역 포스트의 slug와 internal tag

실제 공개 글을 로컬 Ghost로 가져온 화면이다. slug는 orca-codex-threads-chatgpt-ko로 끝나며, 태그에는 #ko#multi가 함께 설정되어 있다.

0.0.8 · Automated bilingual hreflang and tag detection

다국어 포스트 규칙

자동화를 위해 다음 규칙을 사용한다.

  • 영어 포스트 slug는 -en으로 끝난다.
  • 한국어 포스트 slug는 -ko로 끝난다.
  • 번역본이 모두 준비된 포스트에 #multi internal tag를 붙인다.
  • 각 포스트에는 자신의 언어를 나타내는 #en 또는 #ko internal tag가 있어야 한다.

예를 들어 아래 두 포스트는 하나의 번역 쌍이다.

English slug: multilingual-ghost-cms-en
한국어 slug:  multilingual-ghost-cms-ko

실제 URL은 다음과 같이 만들어진다.

https://sanghunkang.com/multilingual-ghost-cms-en/
https://sanghunkang.com/ko/multilingual-ghost-cms-ko/

partials/hreflang-multi.hbs

포스트와 Page의 다국어 URL을 만드는 코드를 별도의 partial로 분리했다.

먼저 지원 언어와 기본 언어를 설정한다.

var LANGS = ["en", "ko"];
var DEFAULT_LANG = "en";

그다음 포스트의 모든 태그 이름을 JavaScript 배열로 만든다.

var tagNames = [
    {{#foreach tags visibility="all"}}
        "{{name}}"{{#unless @last}},{{/unless}}
    {{/foreach}}
];

visibility="all"을 지정하는 이유는 #multi, #en, #ko가 모두 internal tag이기 때문이다. 이 옵션이 없으면 Ghost는 internal tag를 loop 결과에서 제외한다.

#multi가 없으면 아무 작업도 하지 않는다.

if (tagNames.indexOf("#multi") === -1) return;

현재 포스트의 언어도 태그 배열에서 찾는다.

var lang = null;
LANGS.forEach(function(l) {
    if (tagNames.indexOf("#" + l) !== -1) lang = l;
});

if (!lang) return;

slug 마지막의 -en 또는 -ko를 제거하면 번역 포스트가 공유하는 stem을 얻을 수 있다.

var suffixPattern = new RegExp("-(" + LANGS.join("|") + ")$");
var stem = slug.replace(suffixPattern, "");

이 stem에 각 언어 suffix와 URL prefix를 다시 붙여 언어별 URL을 만든다.

function buildUrl(l) {
    var prefix = (l === DEFAULT_LANG) ? "" : "/" + l;
    return siteUrl + prefix + "/" + stem + "-" + l + "/";
}

마지막으로 <head>hreflang 링크를 추가한다.

var html = "";

LANGS.forEach(function(l) {
    html += '<link rel="alternate" hreflang="' + l + '" href="' + buildUrl(l) + '"/>';
});

html += '<link rel="alternate" hreflang="x-default" href="' + buildUrl(DEFAULT_LANG) + '"/>';

document.head.insertAdjacentHTML("beforeend", html);

post와 page context에서 partial 호출

default.hbs<head>에서 현재 화면이 Post 또는 Page일 때 partial을 호출한다.

{{#is "post"}}
    {{#post}}
        {{> "hreflang-multi"}}
    {{/post}}
{{/is}}

{{#is "page"}}
    {{#page}}
        {{> "hreflang-multi"}}
    {{/page}}
{{/is}}

{{#post}}{{#page}} context 안에서 호출해야 partial이 현재 콘텐츠의 {{slug}}{{tags}}를 사용할 수 있다.

태그 순서에 의존하던 문제

기존 Tag 페이지는 두 번째 태그가 언어 태그라는 규칙을 사용했다.

data-language="{{tags.[1].name}}"

이 방식에서는 태그 순서가 바뀌면 언어 필터링이 깨진다.

[Ghost, #ko, #multi]  -> 정상
[#multi, Ghost, #ko]  -> 실패

0.0.8에서는 각 포스트의 모든 태그를 data-tags에 넣는다.

<div class="post-card" data-tags='[{{#foreach tags visibility="all"}}"{{name}}"{{#unless @last}},{{/unless}}{{/foreach}}]'>

JavaScript는 배열 전체에서 언어 태그를 찾는다.

let cardLang = null;

LANGS.forEach(l => {
    if (tagNames.indexOf('#' + l) !== -1) {
        cardLang = l;
    }
});

이제 #en 또는 #ko가 몇 번째에 있는지는 중요하지 않다.

Ghost의 {{#has tag="..."}} helper도 internal tag 판별에는 사용하지 않았다. internal tag의 화면 표시 이름은 #multi이지만 실제 slug는 hash-multi이므로 이름과 slug를 혼동하면 조건이 조용히 실패할 수 있다. 모든 internal tag 이름을 배열로 넘긴 뒤 JavaScript에서 명시적으로 비교하는 쪽이 예측 가능했다.

결과 확인

#multi 포스트에서 개발자 도구를 열고 아래 코드를 실행한다.

[...document.querySelectorAll('link[rel="alternate"][hreflang]')]
  .map(link => [link.hreflang, link.href]);

영어 포스트와 한국어 포스트 양쪽에서 en, ko, x-default 세 링크가 나오고 URL이 각각 올바른 번역 포스트를 가리켜야 한다.

0.0.8 한국어 #multi 포스트의 hreflang 검사 결과

한국어 Orca 포스트에서 DOM을 검사한 결과다. en은 영어 번역 글, ko는 현재 한국어 글, x-default는 영어 번역 글을 가리킨다. 검은 패널은 실제 DOM 조회 결과를 화면에 겹쳐 표시한 것이다.

Tag 페이지에서는 포스트의 태그 순서를 변경해도 선택한 언어의 포스트만 정상적으로 보여야 한다.

주의할 점

#multi는 모든 언어 버전이 실제로 존재할 때만 붙여야 한다. 한국어 포스트만 있는데 #multi를 붙이면 테마는 규칙에 따라 영어 URL도 만들고, 그 URL은 404가 된다.

또한 포스트의 hreflang은 브라우저 JavaScript가 <head>에 추가한다. Google은 JavaScript를 렌더링하므로 이 방식을 처리할 수 있지만 JavaScript 렌더링이 약한 일부 crawler는 링크를 놓칠 수 있다.


0.0.8부터는 번역 포스트에 언어 태그와 #multi만 정확히 설정하면 hreflang이 자동으로 만들어진다.