고스트 CMS 다국어 블로그: 9. Author 페이지와 포스트페이지에서 언어 전환 가능하도록 업데이트

고스트 CMS 다국어 블로그: 9. Author 페이지와 포스트페이지에서 언어 전환 가능하도록 업데이트

기존 테마에는 두 가지 문제가 남아 있었다.

  1. Tag 페이지에서는 선택한 언어의 포스트만 보이지만 Author 페이지에서는 영어와 한국어 포스트가 모두 보인다.
  2. 번역된 포스트가 있어도 Language selector에서 언어를 바꾸면 해당 포스트가 아니라 선택한 언어의 메인 페이지로 이동한다.

0.0.9에서는 Author 페이지를 기존 Tag 페이지와 같은 방식으로 처리하고, #multi 포스트에서는 번역된 포스트로 바로 이동하도록 수정했다.

0.0.8 Author 페이지에서 영어와 한국어 포스트가 함께 보이는 상태

0.0.8 테마의 /author/sanghun/ 화면이다. selector는 English지만 영어 글과 한국어 글이 한 목록에 함께 나타난다.

0.0.9 · Author filtering and post language switching

Author 페이지 다국어 처리

현재 Author 페이지는 아래 주소 하나를 사용한다.

/author/sanghun/

Ghost가 이 페이지를 렌더링할 때는 작성자의 포스트를 언어 구분 없이 모두 가져온다. 브라우저의 localStorage에 저장된 언어는 서버에서 알 수 없기 때문에 Handlebars 템플릿에서 영어 포스트와 한국어 포스트를 미리 나눠 가져올 수도 없다.

이 문제는 기존 Tag 페이지와 같은 방식으로 해결했다.

  1. 서버에서는 포스트를 모두 렌더링한다.
  2. 각 포스트 카드에 해당 포스트의 모든 태그를 넣는다.
  3. 브라우저에서 현재 선택된 언어를 확인한다.
  4. #en 또는 #ko가 일치하는 카드만 보여준다.

author.hbs

각 포스트 카드의 data-tags 속성에 공개 태그와 internal tag를 모두 넣는다.

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

여기서 중요한 부분은 visibility="all"이다. #en, #ko, #multi는 internal tag이므로 이 옵션이 없으면 {{#foreach tags}} 결과에 포함되지 않는다.

assets/js/util.js

Tag 페이지와 Author 페이지가 같은 필터링 함수를 사용하도록 공통 로직을 filterArchivePostsByLanguage()로 분리했다.

function filterArchivePostsByLanguage() {
  const supportedLanguages = ["en", "ko"];
  const selectedLanguage = getSelectedLanguage();
  const postCards = document.querySelectorAll("div.post-card[data-tags]");
  let visibleCount = 0;

  postCards.forEach((card) => {
    let tagNames = [];

    try {
      tagNames = JSON.parse(card.getAttribute("data-tags") || "[]");
    } catch (error) {
      tagNames = [];
    }

    const cardLanguage = supportedLanguages.find((language) =>
      tagNames.includes(`#${language}`)
    );

    if (cardLanguage !== selectedLanguage) {
      card.style.display = "none";
      return;
    }

    visibleCount += 1;
    setTimeout(() => {
      card.style.opacity = 1;
      card.querySelectorAll(".post-card-excerpt").forEach((excerpt) => {
        excerpt.style.opacity = 1;
      });
    }, 10);
  });

  return visibleCount;
}

author.hbstag.hbs에서는 DOM이 준비된 뒤 이 함수를 호출한다.

document.addEventListener('DOMContentLoaded', function() {
    filterArchivePostsByLanguage();
});

이제 Author 페이지에서도 Language selector가 English라면 #en 포스트만, 한국어라면 #ko 포스트만 보인다.

0.0.9 Author 페이지에서 영어 포스트만 보이는 상태

같은 /author/sanghun/ 주소에서 English를 선택한 결과다. #en 포스트만 남는다.

0.0.9 Author 페이지에서 한국어 포스트만 보이는 상태

URL을 바꾸지 않고 한국어를 선택하면 #ko 포스트만 남는다.

#multi 포스트에서 번역된 포스트로 이동

이 블로그에서 서로 번역 관계인 포스트는 다음 규칙을 사용한다.

  • 영어 포스트의 slug는 -en으로 끝난다.
  • 한국어 포스트의 slug는 -ko로 끝난다.
  • 두 포스트 모두 #multi internal tag를 가진다.

예를 들면 다음 두 포스트는 서로 번역 관계이다.

/example-post-en/
/ko/example-post-ko/

0.0.8에서는 이 규칙을 이용해 hreflang URL을 자동으로 만들었다. 0.0.9에서는 이미 계산한 URL을 Language selector에서도 사용할 수 있도록 확장했다.

partials/hreflang-multi.hbs

현재 화면의 콘텐츠 종류, #multi 여부, 현재 언어, 언어별 URL을 전역 객체에 저장한다.

window.casperMultiLanguageContext = {
    contentType: "{{contentType}}",
    isMulti: isMulti,
    currentLanguage: lang,
    urls: urls
};

#multi 포스트라면 urls는 아래와 같은 형태가 된다.

{
  en: "https://example.com/example-post-en/",
  ko: "https://example.com/ko/example-post-ko/"
}

default.hbs

Language selector의 값이 바뀌었을 때 현재 화면이 #multi 포스트인지 확인한다. 조건에 맞으면 선택한 언어의 포스트 URL로 바로 이동한다.

if (
    contentContext &&
    contentContext.contentType === 'post' &&
    contentContext.isMulti &&
    contentContext.urls[selectedLanguage]
) {
    window.location.href = contentContext.urls[selectedLanguage];
} else {
    window.location.href = selectedLanguage === 'ko' ? '/ko/' : '/';
}

최종 동작은 다음과 같다.

  • #multi 포스트: 해당 포스트의 다른 언어 버전으로 이동
  • #multi가 없는 포스트: 선택한 언어의 메인 페이지로 이동
  • Page: 선택한 언어의 메인 페이지로 이동
  • Tag 또는 Author 페이지: 현재 URL을 새로고침하고 선택한 언어의 포스트만 표시
  • 그 외 화면: 선택한 언어의 메인 페이지로 이동
전환 전 한국어 #multi 포스트

한국어 번역 글에서 selector는 한국어를 가리킨다.

English 선택 후 대응하는 영어 포스트로 이동한 결과

selector를 English로 바꾸면 영어 메인 페이지가 아니라 대응하는 /orca-codex-threads-chatgpt-en/ 포스트로 바로 이동한다.

Author 페이지 필터링의 한계

Author 페이지의 언어 필터링은 브라우저에서 이미 렌더링된 카드를 숨기는 방식이다. 따라서 Ghost가 계산한 페이지네이션은 언어 필터링 전의 전체 포스트를 기준으로 한다.

포스트가 많아지면 다음과 같은 문제가 생길 수 있다.

  • 한 페이지에 실제로 보이는 포스트 수가 줄어들 수 있다.
  • 특정 페이지에는 선택한 언어의 포스트가 거의 없을 수 있다.
  • 전체 페이지 수가 선택한 언어의 포스트 수와 일치하지 않을 수 있다.

현재 블로그에서는 실사용에 문제가 없어서 기존 Tag 페이지와 같은 방식을 선택했다. 나중에 정확한 언어별 페이지네이션이 필요해지면 Ghost Core를 수정하지 않고도 language-specific Channel을 추가하는 방식으로 바꿀 수 있다.

Language-Specific Author Channels 제안

결과

0.0.9에서 Language selector의 동작이 화면의 맥락에 맞게 바뀌었다.

Author 페이지에서는 기존 Tag 페이지처럼 선택한 언어의 포스트만 볼 수 있다. 번역된 #multi 포스트에서는 메인 페이지를 거치지 않고 대응하는 포스트로 바로 이동한다.

Ghost Core나 데이터베이스는 수정하지 않았다. 이번 변경도 테마 안에서만 처리했기 때문에 ghost.io같은 managed hosting 환경에 그대로 적용할 수 있다.