1. First, separate pages by language using the routes file to distinguish between ja and en tags.
Since my blog's default is Japanese, my routes.yaml file looks like this:
routes:
collections:
/:
permalink: /{slug}/ # URL for Japanese articles
filter: "tag:[hash-ja]" # Only #ja articles
/en/:
permalink: /en/{slug}/ # URL for English articles
filter: "tag:[hash-en]" # Only #en articles
taxonomies:
tag: /tag/{slug}/ # Can be divided by language if needed
author: /author/{slug}/
This feature requires only downloading the routes file from the labs customization section and modifying it.
2. Add #ja and #en tags to each blog post.
sharp-mean inner tag, not show up user view.
3. Add SEO link tags in Code Injection
<link rel="alternate" hreflang="en" href="https://www.functional-visual.design/en/" />
<link rel="alternate" hreflang="ja" href="https://www.functional-visual.design/" />
<link rel="alternate" hreflang="x-default" href="https://www.functional-visual.design/" />
4. Add JavaScript in Code Injection to dispatch based on language
(I've added userSelectedLanguage to sessionStorage so Japanese users who want to view English pages won't be redirected away)
<script>
(function() {
// Check if user has explicitly selected a language
const userChoseLang = sessionStorage.getItem('userSelectedLanguage');
// Check if current path has a language prefix
const hasLangPrefix = location.pathname.match(/^\/(en|ja)\//);
// Auto-redirect only if no explicit selection
if (!userChoseLang && !hasLangPrefix) {
const lang = navigator.language || navigator.userLanguage;
// Redirect only English users to /en/
// Japanese users aren't redirected (Japanese is default)
if (lang.startsWith('en')) {
// Make sure not to redirect to the same page
const newPath = '/en' + (location.pathname === '/' ? '/' : location.pathname);
if (location.pathname !== newPath) {
location.replace(newPath);
}
}
}
// Add event handling for language link clicks
document.addEventListener('DOMContentLoaded', function() {
const langLinks = document.querySelectorAll('a[hreflang]');
langLinks.forEach(link => {
link.addEventListener('click', function() {
// Record that user has selected a language
sessionStorage.setItem('userSelectedLanguage', true);
});
});
});
})();
</script>
5. Add meta tag switching for SEO
<script>
(function() {
// ① Define descriptions for each language
var metaTexts = {
en: {
title: 'Functional Visual Design',
description: 'As a hobbyist designer living with a disability, I pour my passion into design and coding. Here I showcase my projects and share the insights gained along my creative journey.',
ogLocale: 'en_US'
},
ja: {
title: 'Functional Visual Design',
description: '障がいを抱える趣味のデザイナーとして、デザインとコーディングに情熱を注いでいます。ここではプロジェクトを公開し、創造の過程で得た洞察を共有します。',
ogLocale: 'ja_JP'
}
};
// ② Determine if the current path starts with /en/
var lang = location.pathname.startsWith('/en/') ? 'en' : 'ja';
var cfg = metaTexts[lang];
// ③ Replace <title>
document.title = cfg.title;
// ④ Function to update or add meta tags
function upsertMeta(attr, value, content) {
var selector = 'meta[' + attr + '="' + value + '"]';
var el = document.querySelector(selector);
if (el) {
el.setAttribute('content', content);
} else {
var m = document.createElement('meta');
m.setAttribute(attr, value);
m.setAttribute('content', content);
document.head.appendChild(m);
}
}
// Update meta tags
upsertMeta('name', 'description', cfg.description);
upsertMeta('property', 'og:description', cfg.description);
upsertMeta('property', 'og:locale', cfg.ogLocale);
// (Optional: switch canonical links by language)
// var canonicalUrl = location.origin + location.pathname;
// var canonicalEl = document.querySelector('link[rel="canonical"]');
// if (canonicalEl) {
// canonicalEl.setAttribute('href', canonicalUrl);
// } else {
// var link = document.createElement('link');
// link.setAttribute('rel', 'canonical');
// link.setAttribute('href', canonicalUrl);
// document.head.appendChild(link);
// }
})();
</script>
6. Add Japanese web fonts and custom CSS since the theme doesn't support Japanese
Implement through code injection (this CSS currently doesn't use the theme's variables and will be updated later) to apply to pages that don't contain /en/
<script>
(function() {
// Add font script and styles if path doesn't start with /en/
if (location.pathname.startsWith('/en/')) {
// do nothing
} else {
// ① Dynamically load FontPlus script
var fp = document.createElement('script');
fp.src = 'https://webfont.fontplus.jp/accessor/script/fontplus.js?something-inside-the-api-key';
document.head.appendChild(fp);
// ② CSS variables & global settings for Japanese fonts
var style = document.createElement('style');
style.textContent = `
html[lang="ja"] {
--gh-font-heading: TazuganeGothicStdN-Bold;
--gh-font-body: TazuganeGothicStdN-Regular;
--logo-author-font-weight: 700;
--logo-author-letter-spacing: 0.005em;
--logo-auhtor-line-height: 150%;
--font-weight-titles: 700;
--big-author-name-font-size: 16rem;
--big-author-name-font-weight: 700;
--big-author-name-line-height: 150%;
--big-author-name-letter-spacing: 0.005em;
--full-width-post-font-size: 4.8rem;
--full-width-post-line-height: 125%;
--full-width-post-letter-spacing: 0.005em;
--h1-font-size: 4.8rem;
--h1-line-height: 150%;
--h1-letter-spacing: 0.005em;
--h2-font-size: 3.2rem;
--h2-line-height: 150%;
--h2-letter-spacing: 0.005em;
--h3-font-size: 2.4rem;
--h3-line-height: 150%;
--h3-letter-spacing: 0.005em;
--h4-font-size: 2rem;
--h4-line-height: 150%;
--h4-letter-spacing: 0.005em;
--text-L-semibold-font-size: 1.8rem;
--text-L-semibold-font-weight: 600;
--text-L-semibold-line-height: 150%;
--text-L-semibold-letter-spacing: 0.005em;
--text-L-medium-font-size: 1.8rem;
--text-L-medium-font-weight: 500;
--text-L-medium-line-height: 150%;
--text-M-semibold-font-size: 1.6rem;
--text-M-semibold-font-weight: 600;
--text-M-semibold-line-height: 175%;
--text-M-semibold-letter-spacing: 0.005em;
--text-M-medium-font-size: 1.4rem;
--text-M-medium-font-weight: 500;
--text-M-medium-line-height: 175%;
--text-S-bold-font-size: 1.4rem;
--text-S-bold-font-weight: 700;
--text-S-bold-line-height: 150%;
--text-S-bold-letter-spacing: 0.005em;
--text-S-medium-font-size: 1.4rem;
--text-S-medium-font-weight: 500;
--text-S-medium-line-height: 150%;
--text-XS-bold-font-size: 1.2rem;
--text-XS-bold-font-weight: 700;
--text-XS-bold-line-height: 175%;
--text-XS-bold-letter-spacing: 0.005em;
}
/* Global styles along with font variables */
html[lang="ja"], html[lang="ja"] body {
font-size: 17px;
letter-spacing: 0.5%;
}
`;
document.head.appendChild(style);
}
})();
</script>
7. Add language options to Navigation
Create a Japanese item with the home URL, and an English item with the home URL + \en\ (since my default is Japanese).
Remaining Issues
My theme includes features like show-favorite-posts and other theme-specific section display options, but these can't be used effectively. To change the display with multiple queries for the theme would require modifying the queries issued by the theme itself. Using show-favorite-posts with the theme's default query would mix Japanese favorite posts into English pages regardless of #ja or #en tags. After much consideration, I decided against customizing the theme internals, as it would require rework with each theme update. The favorite-posts feature wasn't essential for me at this time.
Finally, an introduction to my Ghost hosting service
Being extremely lazy, I didn't want to install Ghost on a Japanese server and write backup scripts myself. Instead of renting a server in Japan, I contracted with a European service that handles Ghost installation, daily backups, and Ghost maintenance. It costs 21,355 yen annually, which isn't cheap, and since I don't use social media, few people find my site through search engines. To make my content accessible to those valuable visitors who do arrive, I've moved my English articles from Medium to Ghost. Like many Ghost hosting services, this host uses CDN, so static page data is stored near Japan and shouldn't load too slowly. The issue is that the backend is dynamically generated, so it's a bit slow from a European server. But the reality is that many easily-contracted Ghost hosting services have servers in Europe or America if convenience is your priority.
