Skip to content

Instantly share code, notes, and snippets.

@codeflorist
Last active March 28, 2024 13:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codeflorist/506f7cd08cd0d733a616d996c56b17b1 to your computer and use it in GitHub Desktop.
Save codeflorist/506f7cd08cd0d733a616d996c56b17b1 to your computer and use it in GitHub Desktop.
Multilingual route management for using Nuxt3 with Storyblok CMS
export const useStoryRoutes = () => {
type StoryRoute = {
storyPath: string // The backend-storyblok-path of the story.
uuid: string // The uuid of the story.
localeTitles: LocalizedText
localePaths: LocalizedText
}
const pageTypes = 'page,news-article'
const { defaultLocale, locales: availableLocales, locale: currentLocale } = useI18n()
const i18nSwitchLocalePath = useSwitchLocalePath()
const route = useRoute()
const storyRoutes = useState<StoryRoute[]>('storyRoutes', () => [])
const isDev = typeof process !== 'undefined' && process.env.NODE_ENV === 'development'
const isStoryblokEditor = !!route.query?._storyblok
const isStoryblokFetchAllowed = process.server || isStoryblokEditor
const storyVersion: 'draft' | 'published' = isStoryblokEditor || isDev ? 'draft' : 'published'
const doThrowErrors = isDev || process.server
const storyUsesTranslatedSlugs = (story: Story): boolean => 'translated_slugs' in story && Array.isArray(story.translated_slugs)
/* Makes sure, paths have trailing but no ending slash */
const normalizePath = (path: string): string => `/${path.replace(/\/$/, '').replace(/^\//, '')}`
/* Gets the storyblok-backend-path for a story */
const getStoryPathFromStory = (story: Story): string => {
if (storyUsesTranslatedSlugs(story)) {
return normalizePath(story.default_full_slug)
}
return normalizePath(
story.lang === 'default'
? story.full_slug
: story.full_slug.split('/').slice(1).join('/')
)
}
/* Gets the translated title for a story */
const getTranslatedTitle = (story: Story, locale: string): string => {
// First candidate is always the story.content.title field.
if (story.content.title) {
return story.content.title
}
// If the story uses translated slugs, it might also contain a candidate for the translated title.
if (storyUsesTranslatedSlugs(story) && locale !== defaultLocale) {
const translatedSlug = story.translated_slugs.find(
translatedSlug => translatedSlug.lang === locale
)
if (typeof translatedSlug !== 'undefined' && translatedSlug.name) {
return translatedSlug.name
}
}
// By default we just return the story.name (which might be untranslated!)
return story.name
}
/* Generates the translated path for a story */
const generateTranslatedPath = (story: Story, locale: string): string => {
const isDefaultLocale = locale === defaultLocale
const fullStorySlug = normalizePath(story.full_slug)
// Special treatment for home page.
const isHomePage = isDefaultLocale
? fullStorySlug === '/home'
: fullStorySlug === `/${locale}/home`
if (isHomePage) {
return isDefaultLocale ? '/' : `/${locale}`
}
// If the space uses the "Translated slugs" app,
// we get the complete path already in a finished state.
if (storyUsesTranslatedSlugs(story)) {
return normalizePath(story.full_slug)
}
// If the space does NO use the "Translated slugs" app,
// we use the custom story.content.slug field to get the slug
// and have to build the translated path ourselves.
// Get the story-slug (either a language specific one, or the default-storyblok one)
const localeStorySlug = story.content.slug || story.slug
// Check if page has a parent-page or -folder.
// Non default-languages begin with /<locale>/,
// so we have to look for 2 slashes here.
const hasParent = isDefaultLocale
? fullStorySlug.split('/').length > 2
: fullStorySlug.split('/').length > 3
// If there is no parent, we simply return the localeStorySlug,
// (prefixed by the locale, if not default locale).
if (!hasParent) {
return normalizePath(isDefaultLocale ? localeStorySlug : `${locale}/${localeStorySlug}`)
}
// Otherwise we look for the parent inside storyRoutes.value
const parentStoryPath = getStoryPathFromStory(story).split('/').slice(0, -1).join('/')
const parentStoryRoute = storyRoutes.value.find(
storyRoute => storyRoute.storyPath === parentStoryPath
)
if (typeof parentStoryRoute !== 'undefined') {
return `${parentStoryRoute.localePaths[locale]}/${localeStorySlug}`
}
// If no parent is present, this means the story lies in a folder
// without any root-story. So we cannot translate the folder.
return `${fullStorySlug.split('/').slice(0, -1).join('/')}/${localeStorySlug}`
}
const fetch = async () => {
if (isStoryblokFetchAllowed && storyRoutes.value.length === 0) {
for (const localeData of availableLocales.value) {
const stories: Story[] = await useStoryblokApi().getAll('cdn/stories', {
sort_by: 'parent_id:asc,is_startpage:desc',
filter_query: {
component: {
in: pageTypes,
},
},
version: storyVersion,
language: localeData.code,
})
for (const story of stories) {
let routeIndex = null
const storyPath = getStoryPathFromStory(story)
// Check, if story is already present in other language
const alreadyPresentIndex = storyRoutes.value.findIndex(
(route: StoryRoute) => route.storyPath === storyPath
)
// If not, we push new StoryRoute object to fill
if (alreadyPresentIndex === -1) {
routeIndex
= storyRoutes.value.push({
storyPath,
uuid: story.uuid,
localeTitles: {},
localePaths: {},
}) - 1
}
else {
routeIndex = alreadyPresentIndex
}
storyRoutes.value[routeIndex].localeTitles[localeData.code]
= getTranslatedTitle(story, localeData.code)
storyRoutes.value[routeIndex].localePaths[localeData.code]
= generateTranslatedPath(story, localeData.code)
}
}
}
}
/* Gets the translated frontend path for a storyblok-backend-path */
const localePath = (storyPath: string, locale = ''): string | null => {
const targetLocale = locale || currentLocale.value
const storyRoute = storyRoutes.value.find(
storyRoute => normalizePath(storyPath) === storyRoute.storyPath
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.localePaths[targetLocale]
}
const errMsg = `Error: no translated path found for storyPath "${storyPath}" and locale "${targetLocale}"`
console.error(errMsg)
if (doThrowErrors) {
throw createError({
statusCode: 500,
statusMessage: errMsg,
})
}
return null
}
/* Gets the translated frontend path for a story uuid */
const localePathByUuid = (uuid: string, locale = ''): string | null => {
const targetLocale = locale || currentLocale.value
const storyRoute = storyRoutes.value.find(
storyRoute => uuid === storyRoute.uuid
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.localePaths[targetLocale]
}
const errMsg = `Error: no translated path found for story with uuid "${uuid}" and locale "${targetLocale}"`
console.error(errMsg)
if (doThrowErrors) {
throw createError({
statusCode: 500,
statusMessage: errMsg,
})
}
return null
}
/* Gets the translated Page Title for a story */
const localeTitle = (storyPath: string, locale = ''): string | null => {
const targetLocale = locale || currentLocale.value
const storyRoute = storyRoutes.value.find(
storyRoute => normalizePath(storyPath) === storyRoute.storyPath
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.localeTitles[targetLocale]
}
const errMsg = `Error: no translated page title found for storyPath "${storyPath}" and locale "${targetLocale}"`
console.error(errMsg)
if (doThrowErrors) {
throw createError({
statusCode: 500,
statusMessage: errMsg,
})
}
return null
}
/* Gets the translated Page Title for a story */
const localeTitleByUuid = (uuid: string, locale = ''): string | null => {
const targetLocale = locale || currentLocale.value
const storyRoute = storyRoutes.value.find(
storyRoute => uuid === storyRoute.storyPath
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.localeTitles[targetLocale]
}
const errMsg = `Error: no translated page title found for story with uuid "${uuid}" and locale "${targetLocale}"`
console.error(errMsg)
if (doThrowErrors) {
throw createError({
statusCode: 500,
statusMessage: errMsg,
})
}
return null
}
/* Gets the storyblok-backend-path for a translated frontend path */
const storyPath = (localePath: string): string | null => {
const storyRoute = storyRoutes.value.find(storyRoute =>
Object.values(storyRoute.localePaths).find(
storyLocalePath => storyLocalePath === localePath
)
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.storyPath
}
return null
}
/* Gets the storyblok-backend-path for a translated frontend path */
const storyPathByUuid = (uuid: string): string | null => {
const storyRoute = storyRoutes.value.find(storyRoute => storyRoute.uuid === uuid)
if (typeof storyRoute !== 'undefined') {
return storyRoute.storyPath
}
return null
}
/* Gets the path to the current page in another language */
const switchLocalePath = (locale: string): string => {
const storyRoute = storyRoutes.value.find(storyRoute =>
Object.values(storyRoute.localePaths).find(
localePath => localePath === normalizePath(route.path)
)
)
if (typeof storyRoute !== 'undefined') {
return storyRoute.localePaths[locale]
}
// Fall back to i18n-version by default.
return i18nSwitchLocalePath(locale)
}
return {
fetch,
storyRoutes,
localePath,
localePathByUuid,
localeTitleByUuid,
localeTitle,
storyPath,
switchLocalePath,
normalizePath,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment