SEO-friendly Localization with Kobweb
- Prerequisites
- Kobweb's Static Site Generation
- Libres' String Resource Files
- String Resource Task
- Kobweb's Configuration File
- Route Prefix
- Export Task
- Add Task Dependencies
- Manually Switching Languages
- Use Browser's Preferred Language
- Specifying Hreflang Tags
- Conclusion
In the previous article, we explored a fairly straightforward way to localize your Kobweb site using Libres. It is a suitable method for most web apps, but web crawlers will only see the default language. To improve SEO and have the site indexed in multiple languages, we need to export it multiple times, once for each language.
Prerequisites
This is a more advanced topic, so a more in-depth understanding of Kobweb as well as web hosting is required. If you haven't already, check out the previous article for a step-by-step guide on how to add localization functionality to your Kobweb app and getting acquainted with Libres.
Kobweb's Static Site Generation
During the export process, Kobweb visits each page of your site and snapshots it's static HTML. This serves two purposes: it speeds up page loading and makes the site SEO-friendly - the static HTML is what web crawlers see. The method we used in the previous article only exports the site once, in the default language, and then uses JavaScript to switch between languages.
So, the idea is to export the site separately for each language, each with its own static HTML. For this we will add two new Gradle tasks, which, in turn, modify the string resource and Kobweb config files before exporting the site.
Libres' String Resource Files
To understand how this will work, let's take a look at the files Libres generates.
public object ResStrings {
private val baseLocale: StringsEn = StringsEn
private val locales: Map<String, Strings> = mapOf("en" to StringsEn, "de" to StringsDe)
public val home: String
get() = locales[getCurrentLanguageCode()]?.home ?: baseLocale.home
public val about: String
get() = locales[getCurrentLanguageCode()]?.about ?: baseLocale.about
}
public object StringsEn : Strings {
override val home: String = "Home"
override val about: String = "About"
}
So, what we will do is remove the locales
property and replace locales[getCurrentLanguageCode()]
with the respective object for each language before exporting the site. Which means get() = locales[getCurrentLanguageCode()]?.home
will become get() = StringsDe.home
for German, for example.
String Resource Task
Our two new Gradle tasks will communicate with each-other via a temporary file containing the current locale. We need this to specify which Strings
object to use.
val resTask by tasks.register("modifyStrings") {
val dir = layout.buildDirectory.dir("generated/libres/common/src/strings")
val file = layout.buildDirectory.file("${dir.get().asFile.path}/ResStrings.kt")
val localeFile = layout.projectDirectory.file("tmp/locale.txt")
outputs.dir(dir)
doLast {
val locale = if (localeFile.asFile.exists()) localeFile.asFile.readText() else "StringsEn"
val asFile = file.get().asFile
val string = asFile.readText().replace("locales[getCurrentLanguageCode()]?", locale)
val output = string.substringBefore("private val locales") + string.substringAfter(")")
asFile.writeText(output)
}
}
Kobweb's Configuration File
Kobweb comes with a conf.yaml
file which we need to modify.
site:
title: "Fluense"
routePrefix: "/"
server:
files:
dev:
contentRoot: "build/processedResources/js/main/public"
script: "build/dist/js/developmentExecutable/web.js"
api: "build/libs/web.jar"
prod:
script: "build/dist/js/productionExecutable/web.js"
siteRoot: ".kobweb/web"
port: 8080
Route Prefix
You may host your site on different subdomains or subdirectories for each language. In the latter case, you must add the route prefix routePrefix: "/"
which we will later change to each locale in the Gradle task.
The second line we will modify in the tasks is the export path, siteRoot
. We will add the locale to the path, so the site is exported to a different directory for each language.
Export Task
For each language, we first create a temporary file with the locale, then modify the configuration file and finally export the site calling the Kobweb CLI.
tasks.register("exportSite") {
group = "kobweb"
val inputDir = layout.projectDirectory.dir("src/commonMain/libres/strings")
val locales =
inputDir.asFile.listFiles()
?.map { file -> file.nameWithoutExtension.split("_").joinToString("") { it.uppercaseFirstChar() } }
?: emptyList()
val configFile = layout.projectDirectory.file(".kobweb/conf.yaml")
val localeFile = layout.projectDirectory.file("tmp/locale.txt")
doLast {
val configContent = configFile.asFile.readLines()
locales.forEach { localeString ->
// localeString = "StringsEn", locale = "en"
val locale = localeString.substringAfter("Strings").lowercase()
println("Exporting $locale...")
localeFile.asFile.apply {
parentFile?.mkdirs()
writeText(localeString)
}
val updatedConfigContent = configContent.map {
when {
it.contains("routePrefix:") -> " routePrefix: \"$locale\""
it.contains("siteRoot:") -> " siteRoot: \".kobweb/$locale\""
else -> it
}
}
configFile.asFile.writeText(updatedConfigContent.joinToString("\n"))
exec {
executable = "cmd"
args = listOf("/C", "kobweb export --layout static --notty")
}
}
configFile.asFile.writeText(configContent.joinToString("\n"))
localeFile.asFile.delete()
}
}
Add Task Dependencies
Finally, we need to make sure our Gradle tasks run in the correct order. The Libres task should be finalized by our own modifyStrings
task, and the Ksp task should depend on it as well.
tasks.withType(LibresResourcesGenerationTask::class.java).configureEach {
finalizedBy(resTask)
}
tasks.withType(KspTaskJS::class.java).configureEach {
dependsOn(resTask)
}
Now, all that's left to do is run the exportSite
task and your site will be exported in multiple languages.
Manually Switching Languages
To allow users to manually switch languages we simply redirect them to the respective locale's site. If using subdirectories, you can replace the current locale in the URL. To keep track of the current locale we could either read it from the URL, or add a locale string to each string resource xml file.
<string name="locale">en</string>
Use Browser's Preferred Language
This depends on how you are hosting your site and will likely require a bespoke solution. An example with nginx would be to use the $http_accept_language
variable to set the locale in the URL and redirect accordingly.
Specifying Hreflang Tags
To help search engines understand the relationship between your site's different language versions, you can add hreflang tags to your HTML. These tags specify the language and optionally the region of the content.
Res.locales.forEach {
document.head?.append(document.createElement("link").apply {
setAttribute("rel", "alternate")
setAttribute("hreflang", it.iso)
setAttribute(
"href",
"https://site.com" + window.location.pathname.replace("/${Res.string.locale}/", "/$it/")
.substringBefore("?")
)
})
}
Conclusion
By exporting your Kobweb site in multiple languages, you can improve your SEO and reach a wider audience. This method is more complex than the one we used in the previous article, but it is necessary if you want your site to be indexed in multiple languages. While the former method is sufficient for our web app, we are using this SEO-friendly way for our Fluense landing page to score higher in search engine results.
Happy coding!