Implement routing, localization, and multi-sites

This walkthrough builds on the app from Render SitecoreAI content in your app, which hard-codes a single site name and language from environment variables. In this walkthrough, you replace those hard-coded values with request-time resolution, so your app can serve multiple sites and languages, and honor content-managed redirects.

The walkthrough provides code examples for Astro 7 and Go front-end applications, and it describes how to:

  1. Fetch and cache your site list
  2. Resolve the site from the request hostname
  3. Resolve the locale from the request path
  4. Fetch and evaluate redirects
  5. Query the site dictionary
  6. Wire resolution into your routing
  7. Test your app
Before you begin

Fetch and cache your site list

Add a function that retrieves your environment's site list and caches it in memory, so you don't query it on every request.

  1. Add the following to src/services/sitecoreClient.js:

    // src/services/sitecoreClient.js
    
    let siteListCache = null;
    
    export async function fetchSiteList() {
      if (siteListCache) {
        return siteListCache;
      }
    
      const env = import.meta.env;
      const endpoint = env.SITECORE_EDGE_PLATFORM_URL;
      const contextId = env.SITECORE_EDGE_CONTEXT_ID;
    
      const query = `
        query SiteInfoCollectionQuery {
          site {
            siteInfoCollection { name hostname language }
          }
        }
      `;
    
      const response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-sitecore-contextid": contextId,
        },
        body: JSON.stringify({ query }),
      });
    
      const result = await response.json();
    
      if (result.errors) {
        throw new Error(JSON.stringify(result.errors));
      }
    
      siteListCache = result?.data?.site?.siteInfoCollection || [];
      return siteListCache;
    }

Resolve the site from the request hostname

Add a function that matches the request's hostname against your cached site list, falling back to a configured default site.

  1. Add the following to src/services/sitecoreClient.js:

    // src/services/sitecoreClient.js
    
    export function resolveSite(hostname, sites, defaultSiteName) {
      const match = sites.find((site) =>
        site.hostname
          .split(/\||,|;/)
          .map((h) => h.trim().toLowerCase())
          .some((h) => hostnameMatches(hostname.toLowerCase(), h))
      );
    
      return match || sites.find((site) => site.name === defaultSiteName);
    }
    
    function hostnameMatches(hostname, pattern) {
      if (!pattern) return false;
      const regex = new RegExp(
        "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$",
        "i"
      );
      return regex.test(hostname);
    }
  2. Update src/pages/[...slug].astro to resolve the site from the request instead of using a hard-coded environment variable:

    ---
    // src/pages/[...slug].astro
    import { fetchLayoutData, fetchSiteList, resolveSite } from '../services/sitecoreClient.js';
    
    export const prerender = false;
    
    const sites = await fetchSiteList();
    const site = resolveSite(
      Astro.url.hostname,
      sites,
      import.meta.env.SITECORE_SITE_NAME
    );
    ---

Resolve the locale from the request path

Next, extract a locale segment from the request path (for example, /fr-fr/products), falling back to the resolved site's default language when no locale segment is present.

  1. Add the following to src/services/sitecoreClient.js:

    // src/services/sitecoreClient.js
    
    export function resolveLocale(slugSegments, configuredLocales, defaultLocale) {
      const [first, ...rest] = slugSegments;
      const match = configuredLocales.find(
        (locale) => locale.toLowerCase() === (first || "").toLowerCase()
      );
    
      if (match) {
        return { locale: match, remainingSegments: rest };
      }
      return { locale: defaultLocale, remainingSegments: slugSegments };
    }
  2. Update src/pages/[...slug].astro to resolve the locale and strip it from the route path:

    ---
    // src/pages/[...slug].astro
    const configuredLocales = (import.meta.env.SITECORE_SITE_LOCALES || "en").split(",");
    const slugSegments = Astro.params.slug ? Astro.params.slug.split('/') : [];
    const { locale, remainingSegments } = resolveLocale(
      slugSegments,
      configuredLocales,
      site.language
    );
    let routePath = remainingSegments.length ? `/${remainingSegments.join('/')}` : '/';
    ---

Fetch and evaluate redirects

Add a function that retrieves a site's redirect rules and checks them against the current request before you fetch layout data.

  1. Add the following to src/services/sitecoreClient.js:

    // src/services/sitecoreClient.js
    
    const redirectsCache = new Map();
    
    export async function fetchRedirects(site) {
      if (redirectsCache.has(site)) {
        return redirectsCache.get(site);
      }
    
      const env = import.meta.env;
      const query = `
        query RedirectsQuery($siteName: String!) {
          site {
            siteInfo(site: $siteName) {
              redirects {
                pattern target redirectType isQueryStringPreserved isLanguagePreserved locale
              }
            }
          }
        }
      `;
    
      const response = await fetch(env.SITECORE_EDGE_PLATFORM_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-sitecore-contextid": env.SITECORE_EDGE_CONTEXT_ID,
        },
        body: JSON.stringify({ query, variables: { siteName: site } }),
      });
    
      const result = await response.json();
      const redirects = result?.data?.site?.siteInfo?.redirects || [];
      redirectsCache.set(site, redirects);
      return redirects;
    }
    
    export function matchRedirect(redirects, routePath, locale) {
      return redirects.find((redirect) => {
        if (redirect.locale && redirect.locale !== locale) {
          return false;
        }
        if (isRegex(redirect.pattern)) {
          return new RegExp(redirect.pattern, "i").test(routePath);
        }
        return redirect.pattern.replace(/\/$/, "") === routePath.replace(/\/$/, "");
      });
    }
    
    function isRegex(pattern) {
      // Simple heuristic: treat patterns with regex metacharacters as regular expressions.
      return /[()^$|[\]]/.test(pattern);
    }
  2. Apply a matched redirect in src/pages/[...slug].astro, before fetching layout data. Treat SERVER_TRANSFER as an internal rewrite rather than an HTTP redirect, so the visible URL doesn't change:

    ---
    const redirects = await fetchRedirects(site.name);
    const matched = matchRedirect(redirects, routePath, locale);
    
    if (matched) {
      if (matched.redirectType === "SERVER_TRANSFER") {
        // Keep the visible URL the same; fetch layout data for the target instead.
        routePath = matched.target;
      } else {
        const status = matched.redirectType === "REDIRECT_301" ? 301 : 302;
        return Astro.redirect(matched.target, status);
      }
    }
    ---
Note

These matching functions are simplified for the walkthrough. Refer to redirects for the full evaluation logic, including capture-group substitution in regex targets, the $siteLang token, and query string/locale preservation.

Query the site dictionary

Fetch the site dictionary for the resolved site and locale, so components can look up translated UI strings alongside page content.

  1. Add the following to src/services/sitecoreClient.js:

    // src/services/sitecoreClient.js
    const dictionaryCache = new Map();
    
    export async function fetchDictionary(site, language) {
      const cacheKey = `${site}:${language}`;
      if (dictionaryCache.has(cacheKey)) {
        return dictionaryCache.get(cacheKey);
      }
    
      const env = import.meta.env;
      const dictionary = {};
      let after = null;
      let hasNext = true;
    
      while (hasNext) {
        const response = await fetch(env.SITECORE_EDGE_PLATFORM_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "x-sitecore-contextid": env.SITECORE_EDGE_CONTEXT_ID,
          },
          body: JSON.stringify({
            query: `query($siteName: String!, $language: String!, $after: String) {
              site {
                siteInfo(site: $siteName) {
                  dictionary(language: $language, first: 100, after: $after) {
                    results { key value }
                    pageInfo { endCursor hasNext }
                  }
                }
              }
            }`,
            variables: { siteName: site, language, after },
          }),
        });
    
        const { data } = await response.json();
        const page = data.site.siteInfo.dictionary;
        for (const entry of page.results) {
          dictionary[entry.key] = entry.value;
        }
        hasNext = page.pageInfo.hasNext;
        after = page.pageInfo.endCursor;
      }
    
      dictionaryCache.set(cacheKey, dictionary);
      return dictionary;
    }
  2. Fetch it in src/pages/[...slug].astro, alongside your layout data:

    ---
    const dictionary = await fetchDictionary(site.name, locale);
    ---

    The sample components built in the previous walkthrough don't read any dictionary keys, so this walkthrough doesn't wire dictionary further. In your own app, pass it to your layout/component rendering so components can look up translated strings by key.

Note

If your framework supports static site generation, you can enumerate every page for a site and language up front using the routes query, instead of resolving one path at a time as requests come in.

Wire resolution into your routing

With site, locale, and redirect resolution in place, update the call to fetchLayoutData to use the resolved site name and locale instead of the hard-coded environment variables from the previous walkthrough:

---
// src/pages/[...slug].astro
let layoutData;
try {
  layoutData = await fetchLayoutData(site.name, locale, routePath);
} catch (error) {
  return Astro.redirect('/404');
}

if (!layoutData) {
  return Astro.redirect('/404'); // Sitecore returned null; the route doesn't exist.
}
---

Test your app

  1. This walkthrough's resolveLocale validates the URL's locale segment against your app's configured locale list, so add the new locale to your .env file's list (for example, SITECORE_SITE_LOCALES=en,fr-fr) as a one-time setup step, matching a language your SitecoreAI site is available in.
  2. Restart your development server.
  3. Request the root path with a locale segment, such as /fr-fr, and confirm the layout data returned reflects that language.
  4. If your environment has more than one site configured, test with a hostname that matches a non-default site (for example, by editing your local hosts file), and confirm the correct site's content is returned.
  5. Ask a content author to add a test redirect rule to your site's redirect map (or add one yourself, using the Content Editor), then request the redirect's source path and confirm your app redirects as expected.
  6. Confirm the site dictionary is populated by temporarily logging the resolved dictionary object, and check that a known key returns its expected translated value.

Next steps

If you run into issues, see Troubleshooting.

If you have suggestions for improving this article, let us know!