# File: astro-adapter/index.mdx --- title: Astro Adapter description: Deploy your Astro site to Altitude with the official adapter --- # Astro Adapter The Altitude Astro Adapter allows you to deploy your Astro site to Altitude's edge network with minimal configuration. ## Installation ```bash npm install @altitude/astro-adapter@5.0.1 ``` ## Usage Add the adapter to your `astro.config.mjs` file: ```js import { defineConfig } from 'astro/config'; import altitude from '@altitude/astro-adapter'; export default defineConfig({ output: 'server', adapter: altitude(), }); ``` ## Configuration Options The adapter accepts the following options: ```js altitude({ // Your Altitude project name (optional) // Default: inferred from package.json projectName: 'my-astro-site', }) ``` ## Need Help? If you encounter any issues or have questions, please [open an issue](https://github.com/altitude/astro-adapter/issues) on our GitHub repository. ------------------------------------------- # File: astro-integration/changelog.mdx --- title: Astro Integration Changelog description: Summary of the changelog for the Astro Integration --- # Astro Integration Changelog This page contains a summary of the changelog for astro integration. You can also see a detailed changelog on [GitHub](https://github.com/THG-AltitudeSiteBuilds/astro-integration/releases/) ## v3.0.0 (2025-12-01) ### New Features - **Path-based routing support** - Multiple locales can now be served from the same domain using URL path prefixes (e.g., `/en/`, `/fr/`). This enables more flexible multi-locale deployments without requiring separate domains. - **Custom locale prefixes (`customPrefix`)** - Configure custom URL path prefixes for locales instead of using standard locale codes. For example, use `/english/` instead of `/en-us/` in URLs. - **TypeScript rewrite** - Complete conversion from JavaScript to TypeScript with comprehensive type definitions exported for enhanced developer experience. - **Cloudflare Workers runtime support** - Full compatibility with Cloudflare Workers environments, including proper TypeScript types via `@cloudflare/workers-types`. - **Enhanced configuration validation** - Improved JSON schema validation (v3) with better error messages and more precise constraints. - **Domain-based i18n configuration restructure** - New streamlined configuration structure for better organization and multi-tenancy support. ### Breaking Changes - **Configuration structure changes** - The i18n configuration structure has been reorganized for better clarity and extensibility: - `i18n.domains` now uses domain keys with nested locale configurations - `pathBasedRouting` is now configured per domain rather than globally - `fallbackLocale` is required for each domain configuration - **Deprecated context properties removed** - Several previously deprecated properties are no longer available: - `locals.altitude.preferredLocale` - Use locale detection methods instead - `locals.altitude.localeDomains` - Access domain configuration directly - `locals.altitude.localeCookie` - No longer exposed in context - **Single tenancy mode simplification** - i18n configuration is now optional for single tenancy setups - **Cookie routing support removed** - Path-based and domain-based routing are the only supported methods - **Schema validation updates** - Configuration must now validate against JSON Schema v3 with stricter constraints ### Improvements - **Better locale detection and routing logic** - Enhanced algorithm for determining user locale preferences with improved fallback handling - **Enhanced multi-tenancy support** - Cleaner separation between single and multi-tenant configurations with better validation - **Performance optimizations** - Reduced runtime overhead for locale resolution and configuration processing - **Improved error messages** - More descriptive validation errors and runtime exceptions with actionable guidance - **Better TypeScript integration** - Full type safety for configuration objects and runtime context ### Migration Required Existing v2.x configurations require updates to work with v3.0.0. See the [Migration Guide](/astro-integration/v3.0.0/guides/migration-v3/) for detailed instructions and examples. ## v2.0.0 (2025-07-10) ### Changed - Simplified requirements for single tenancy mode - i18n key no longer required. - Rewrites only occur for i18n-enabled tenants, as opposed to all of them. - `domains` required property in buildConfig is now an array - `locales.domain` is now a required property - Top-level 'KV' key is now optional ### Fixed - Fixed single tenancy mode ### Deprecated - removed support for cookie routing - `locals.altitude.preferredLocale`, `locals.altitude.localeDomains` are now deprecated - We are no longer exposing `locals.altitude.localeCookie` - Several keys previously available on the global context in v1.x (such as `localeCookie`) are no longer exposed in v2. If you are migrating, review your usage of global context keys and update your code accordingly. ## v1.7.7 see https://github.com/THG-AltitudeSiteBuilds/astro-integration/releases/tag/v1.7.7 ------------------------------------------- # File: captcha/index.mdx --- title: --- ------------------------------------------- # File: captcha/introduction.mdx --- title: Getting Started order: 1 description: Framework agnostic captcha abstractions for simplified integration. --- # CAPTCHA Abstraction A framework-agnostic TypeScript library that provides a unified abstraction layer for multiple CAPTCHA providers. This package allows you to easily switch between Google reCAPTCHA, hCaptcha, Cloudflare Turnstile, and Friendly Captcha without changing your application code. ## Features - ๐Ÿ”„ **Framework Agnostic**: Works with any JavaScript framework or vanilla JavaScript - ๐ŸŽฏ **Multiple Providers**: Support for reCAPTCHA (v2), hCaptcha, Turnstile, and Friendly Captcha - ๐Ÿ”ง **Unified API**: Single interface for all providers - ๐Ÿš€ **TypeScript Support**: Full type safety and IntelliSense - ๐Ÿ“ฆ **Lightweight**: Minimal dependencies, loads provider scripts on demand - ๐ŸŽจ **Flexible Configuration**: Support for visible, invisible, and various sizing options - ๐Ÿ”’ **Multiple Instances**: Create multiple CAPTCHA instances with different configurations ## Installation Install using npm: ```bash npm install @thg-altitude/captcha ``` ## Quick Start ### Constructor Options ```typescript interface CaptchaConfig { provider: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'friendlyCaptcha'; siteKey: string; } ``` ### Usage ```typescript import { Captcha } from '@thg-altitude/captcha'; // Create a CAPTCHA instance const captcha = new Captcha({ provider: 'recaptcha', // 'recaptcha' | 'hcaptcha' | 'turnstile' | 'friendlyCaptcha' siteKey: 'your-site-key' }); // Render and handle token await captcha.render('captcha-container', 'login-form', { size: 'invisible', onToken: (token) => { // Submit form with token submitForm(token); } }); ``` ## Provider Support ### Google reCAPTCHA v2 - โœ… Checkbox (normal/compact) - โœ… Invisible - โœ… Programmatic execution - ๐Ÿ“– [View the reCAPTCHA Implementation Guide](/docs/captcha/implementation-guides/recaptcha) ### hCaptcha - โœ… Checkbox (normal/compact) - โœ… Invisible - โœ… Programmatic execution - ๐Ÿ“– [View the hCaptcha Implementation Guide](/docs/captcha/implementation-guides/hcaptcha) ### Cloudflare Turnstile - โœ… Managed (normal/compact/flexible) - โœ… Non-interactive - โœ… Invisible (dashboard configured) - ๐Ÿ“– [View the Turnstile Implementation Guide](/docs/captcha/implementation-guides/turnstile) ### Friendly Captcha - โœ… Automatic puzzle solving - โœ… Privacy-focused (GDPR compliant) - โœ… Configurable start modes (auto/focus/none) - ๐Ÿ“– [View the Friendly Captcha Implementation Guide](/docs/captcha/implementation-guides/friendly-captcha) ## Provider-Specific Notes ### reCAPTCHA - Requires different site keys for visible vs invisible modes - Set `size: invisible` in render options for invisible widgets - Supports `normal` and `compact` sizes for visible widgets ### hCaptcha - Same site key works for both visible and invisible modes - Set `size: 'invisible'` in render options for invisible widgets - Supports `normal` and `compact` sizes ### Turnstile - Widget behavior configured in Cloudflare dashboard - Supports `normal`, `compact`, and `flexible` sizes - No separate invisible mode configuration needed (this is handled in your turnstile dashboard) - Supports appearance modes `always`, `interaction-only` or `execute` - Support for execution modes (see [Turnstile implementation guide](/docs/captcha/implementation-guides/turnstile) for more details) ### Friendly Captcha - Privacy-focused CAPTCHA solution that complies with GDPR - Automatically solves puzzles without user interaction in most cases - Supports three start modes: - `auto`: Puzzle starts automatically when widget loads (default) - `focus`: Puzzle starts when user focuses on any form input within the same form - `none`: Puzzle must be started programmatically using `execute()` - Uses SDK version 0.1.31 from CDN (`https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.1.31/site.min.js`) - Widget events include `frc:widget.complete`, `frc:widget.error`, and `frc:widget.expire` - Reset functionality resets both the widget state and token storage ## Error Handling The library includes custom error classes for better error handling: ```typescript import { CaptchaError, ScriptLoadError, TokenError, RenderError, ConfigurationError } from '@thg-altitude/captcha'; try { await captcha.render('container', 'id'); } catch (error) { if (error instanceof ScriptLoadError) { // Handle script loading failure } else if (error instanceof RenderError) { // Handle rendering failure } } ``` ## Browser Support - Modern browsers with ES2015+ support - Automatic script loading and cleanup - Works in both browser and server-side rendering environments (renders client-side only) ------------------------------------------- # File: captcha/methods.mdx --- title: Methods order: 2 description: Methods provided by CAPTCHA package. --- ### Methods #### `render(container, id, options?)` Renders a CAPTCHA widget in the specified container. This includes three callback functions: 1. **onToken**: Called when the CAPTCHA is successfully solved and a token is generated 2. **onError**: Called when the CAPTCHA encounters an error during loading or execution 3. **onExpired**: Called when a previously generated token has expired and needs to be refreshed ```typescript await captcha.render( 'container-id', // or HTMLElement 'unique-widget-id', { size?: 'normal' | 'compact' | 'invisible' | 'flexible'; // flexible only for Turnstile startMode?: 'auto' | 'focus' | 'none'; // Friendly Captcha only onToken?: (token: string) => void; // Called on frc:widget.complete for Friendly Captcha onError?: () => void; // Called on frc:widget.error for Friendly Captcha onExpired?: () => void; // Called on frc:widget.expire for Friendly Captcha } ); ``` #### `execute(id)` Programmatically executes an invisible CAPTCHA (triggers the challenge). For Friendly Captcha, this calls the `start()` method on the widget when `startMode` is set to `'none'`. ```typescript await captcha.execute('widget-id'); ``` #### `getToken(id)` Retrieves the token for a solved CAPTCHA. ```typescript const token = await captcha.getToken('widget-id'); ``` #### `reset(id)` Resets a CAPTCHA widget to its initial state. For Friendly Captcha, this calls the `reset()` method on the widget and clears the stored token. ```typescript captcha.reset('widget-id'); ``` #### `load()` Manually loads the provider's script. This is automatically called by `render()`, but can be called separately for preloading. For Friendly Captcha, this loads the SDK from CDN if not already loaded. ```typescript await captcha.load(); ``` ------------------------------------------- # File: cli/index.mdx --- title: Altitude CLI description: Reference guide for the Altitude CLI commands and usage. --- # Altitude CLI Commands This reference guide covers the commands available in the Altitude CLI and how to use them. ## Installation To install the CLI you must first have [Node.js](https://nodejs.org/en) installed on your machine. You can then install our CLI package by entering the following into the command-line: ```bash npm install -g @thg-altitude/cli ``` ## Setup In order to use the Altitude CLI, you will first need to register an API client to authorize your requests. To do this, navigate to your organization's settings page where you can find a list of API Clients. Please note, only organization Owners can create API clients. Once you have created an API Client, store its credentials as the following environment variables within the environment that you intend to use the CLI: ``` ALTITUDE_CLIENT_ID ALTITUDE_SECRET_KEY ``` ## Deployment Commands ### Deploy a Site To start a deployment with the CLI, enter the following in the command-line: ```bash altitude deploy ``` You will then be asked to select the site and branch that you wish to deploy. Then you will be asked to select the environment you wish to deploy to. The options are to name the environment after the branch name, to create a custom environment, or to select an existing environment. ### Headless Deployment In order to use this CLI headlessly (for example, within a CI pipeline) you will need to pass arguments for the site and the branch that you wish to deploy as well as the name of the environment you want to deploy to. This can be an existing environment or a new one. ```bash altitude deploy --site "My Site Name" --branch main --env environmentName ``` You can also pass the git reference of the specific commit that you would like to deploy: ```bash altitude deploy --site "My Site Name" --ref {GIT_REF} --env environmentName ``` ## Environment Commands ### Get Environment Information The CLI allows you to get environment information in a JSON format. You will need to pass arguments for the environment name and the site which the environment belongs to. This will return an object containing: the environment name, the deployment URL, and the latest deployment information. ```bash altitude environment info --site "My Site Name" --env environmentName ``` ### Delete Environment To delete an environment, use the following command: ```bash altitude environment delete --site "My Site Name" --env environmentName ``` ## Additional Commands The Altitude CLI provides several other commands for managing your sites and environments. You can see a full list of available commands by running: ```bash altitude --help ``` For help with a specific command, you can run: ```bash altitude [command] --help ------------------------------------------- # File: elements/index.mdx --- title: --- ------------------------------------------- # File: elements/introduction.mdx --- title: Introduction order: 1 --- ### What are Elements Altitude's suite of modular UI of ready made commerce functionality with pre-optimised user experience and accessibility. We built Elements to pair with Ingenuity's Commerce APIs to quickly get started with complex components without the headaches. Elements features include: - โ€ข Flexible Configuration and UI. - โ€ข Performance, user experience and accessibility optimised. - โ€ข Framework-less Reactivity and Framework Variants. ------------------------------------------- # File: insights/index.mdx --- title: --- ------------------------------------------- # File: insights/installation.mdx --- title: Installation order: 2 description: Installing Insights for your web application with a single script tag --- # Installation Get started with Insights by adding a single script tag to your application. No build tools or package managers required. ## Script Tag Installation Add the following script tag to your HTML `` section: ```html ``` Replace the attribute values with your specific configuration: - `channel` - Your brand or channel identifier (e.g., `myprotein`, `lookfantastic`) - `subsite` - Your locale or subsite identifier (e.g., `en`, `de`, `fr`) - `storefront` - Your storefront category (e.g., `nutrition`, `beauty`) The script should be placed within the main template used across your application (e.g., `Layout.astro`, `_app.tsx`, or your base HTML template). Loading it in the `` ensures tracking begins as early as possible. ### Complete Example ```html My Application ``` ## Configuration Configuration is handled through data attributes on the script tag. | Attribute | Required | Description | | --------------------------- | -------- | --------------------------------------------------- | | `data-insights-channel` | Yes | Your brand or channel identifier | | `data-insights-subsite` | Yes | Your locale or subsite identifier | | `data-insights-storefront` | Yes | Your storefront category | ### Example Configuration ```html ``` ## Verification After adding the script, verify the installation by checking for the presence of the global `insights` object via the browser console: ```javascript // Check if insights is available if (typeof window.insights !== "undefined") { console.log("Insights loaded successfully"); } else { console.error("Insights failed to load"); } ``` You should also see network requests firing on page load. > **Troubleshooting**: Events are sent using the Beacon API, which uses a fire-and-forget approach. In some browsers (particularly Chrome DevTools), these requests may appear as "failed" or "cancelled" in the Network tab. This is expected behaviour and does not indicate an actual failure. ## Next Steps 1. **[Page Types](./page-types)** - Configure page type tracking for richer analytics 2. **[Event Tracking](./tracking)** - Track user interactions with HTML attributes 3. **[JavaScript API](./advanced/javascript-api)** - Programmatic tracking for advanced scenarios ------------------------------------------- # File: insights/overview.mdx --- title: Overview order: 1 description: Introduction to Insights analytics library for comprehensive user behavior monitoring --- # Insights A lightweight analytics library designed for modern web applications. Insights provides automatic tracking with minimal setup - add a single script tag with your channel configuration to start collecting comprehensive analytics data. Collected data flows into THG's analytics infrastructure, where it drives reporting and business intelligence. ## Key Features - **Simple Setup**: Single script tag with channel, subsite, and storefront configuration - **Automatic Page Views**: Tracks navigation and page context with zero configuration - **Core Web Vitals**: Automatic tracking of FCP, LCP, CLS, INP, and TTFB - **Session Tracking**: Automatic tracking of viewport changes, scroll depth, and tab visibility - **Attribution Tracking**: UTM parameters and referrer data captured automatically - **A/B Testing Support**: Automatically captures experiment assignments when configured - **Dynamic Content Support**: Automatically detects new elements added to the page for impression tracking - **HTML Attribute Tracking**: Track clicks and impressions with simple `data-insights-*` attributes - **Framework Agnostic**: Works with any JavaScript front-end web application framework - **Zero Configuration**: Authentication, batching, and transmission handled automatically ## When to Use the JavaScript API For most tracking needs, HTML attributes are sufficient. Use the JavaScript API when you need to: - Track events that don't correspond to a DOM element (e.g., hover, scroll depth) - Track events on dynamically generated content where attributes can't be applied - Integrate with existing event handlers or analytics logic ## Next Steps 1. **[Installation](./installation)** - Add the script tag to your application 2. **[Page Types](./page-types)** - Configure page type tracking for richer analytics 3. **[Event Tracking](./tracking)** - Track user interactions with HTML attributes 4. **[JavaScript API](./advanced/javascript-api)** - Programmatic tracking for advanced scenarios ------------------------------------------- # File: insights/page-types.mdx --- title: Page Types order: 4 description: Configure page type tracking for richer analytics context --- # Page Types Page type tracking provides richer analytics context by categorising each page in your application. This enables more meaningful reporting and segmentation of your analytics data. ## Setting the Page Type Add a `data-insights-page-type` attribute to any element on the page (typically the `` or a top-level container): ```html ``` The SDK scans the page for this attribute on load and includes the page type in all analytics data. ## Valid Page Types | Value | Description | |-------|-------------| | `Home` | Homepage | | `List` | Product listing or category page | | `Product` | Product detail page | | `Search` | Search results page | | `Basket` | Shopping basket/cart | | `Checkout` | Checkout flow | | `Checkout Complete` | Order confirmation | | `Account` | User account pages | | `Login` | Login/authentication page | | `PW Reset` | Password reset page | | `Blog` | Blog or editorial content | | `Info` | Informational pages (FAQ, About, etc.) | | `Trade` | Trade/B2B pages | ## Examples ### Product Detail Page ```html

Whey Protein Powder

High-quality protein for muscle recovery

``` ### Category Listing Page ```html

Protein Supplements

``` ### Checkout Flow ```html

Complete Your Order

``` ## Dynamic Page Types For single-page applications where the page type changes without a full page reload, you can update the attribute dynamically. The SDK will detect the change for subsequent events. ```javascript // Update page type when navigating to a product page document.body.setAttribute('data-insights-page-type', 'Product'); ``` ## Next Steps - **[Event Tracking](./tracking)** - Track clicks and impressions with HTML attributes - **[JavaScript API](./advanced/javascript-api)** - Programmatic tracking for advanced scenarios ------------------------------------------- # File: insights/tracking.mdx --- title: Event Tracking order: 3 description: Track user interactions with HTML attributes --- # Event Tracking Page views are tracked automatically once the Insights script is added to your site. For tracking user interactions like clicks and impressions, add `data-insights-*` attributes to your HTML elements. This declarative approach keeps tracking logic out of your JavaScript and makes it easy to add tracking to any component. ## Required Attributes To track an element, add these three attributes: | Attribute | Description | Example | |-----------|-------------|---------| | `data-insights-track` | Type of tracking: `click` or `impression` | `"click"` | | `data-insights-category` | Category of the element | `"button"`, `"product"` | | `data-insights-id` | Unique identifier for the element | `"signup-cta"` | ## Click Tracking Track button clicks, link clicks, or any clickable element: ```html ``` This produces an event with the following structure: ```json { "event_category": "click", "event_data": { "category": "button", "id": "signup-cta" }, "timestamp": 1704067260000 } ``` ## Impression Tracking Track when elements become visible in the viewport. Impressions fire when at least 10% of the element is visible: ```html

Protein Shake

High-performance protein shake for athletes

``` ## Custom Attributes Add any additional data using custom `data-insights-*` attributes: ```html ``` Custom attributes are included in the `event_data` object: ```json { "event_category": "click", "event_data": { "category": "button", "id": "add-to-cart", "productId": "SKU-12345", "price": "29.99" }, "timestamp": 1704067260000 } ``` > **Note**: Attribute names are converted from kebab-case to camelCase (e.g., `data-insights-product-id` becomes `productId`). ## Dynamic Content Elements added to the page after initial load are automatically detected. No additional configuration is needed for single-page applications or dynamically loaded content. ## Next Steps - **[Page Types](./page-types)** - Configure page type tracking for richer analytics - **[JavaScript API](./advanced/javascript-api)** - Programmatic tracking for advanced scenarios ------------------------------------------- # File: platform/changelog.mdx --- title: Altitude Platform Changelog description: Complete changelog for all Altitude Platform versions --- # Altitude Platform Changelog This page contains the complete changelog for all Altitude Platform versions. ## v2.3.0 (2025-02-15) ### Added - Enhanced Edge Functions with improved performance and capabilities - Advanced KV Store with TTL support and atomic operations - Real-time Monitoring dashboard with live metrics - Multi-team Collaboration with role-based access control - Advanced CI/CD Integration with detailed deployment reports ### Changed - Updated KV Store API to support TTL and atomic operations - Enhanced Monitoring API to support real-time metrics - Improved deployment process with better error handling - Updated UI with improved usability and accessibility ### Fixed - Fixed issue with KV Store cache invalidation - Resolved edge function timeout issues - Fixed several UI bugs in the dashboard - Improved error messages for better debugging ## v2.2.0 (2025-01-10) ### Added - Performance optimizations for edge functions - Improved KV Store with better caching - Enhanced logging capabilities with better filtering - Redesigned dashboard with better usability ### Changed - Updated edge function runtime for better performance - Improved error handling and reporting - Enhanced documentation with more examples ### Fixed - Fixed several issues reported in v2.1.0 - Resolved performance bottlenecks - Fixed UI inconsistencies ## v2.1.0 (2024-12-05) ### Added - Enhanced API with new endpoints - New features based on user feedback - Expanded documentation with more examples - Improved security features ### Changed - Updated API for better performance - Enhanced error messages for better debugging - Improved user interface for better usability ### Fixed - Fixed several bugs reported by users - Resolved security vulnerabilities - Fixed documentation errors ## v2.0.0 (2024-11-01) ### Added - Advanced Edge Functions - Key-Value Store - Enhanced Monitoring - Team Collaboration - CI/CD Integration ### Changed - Complete redesign of the platform - Updated API with breaking changes - Improved documentation ### Fixed - Numerous bugs from the previous version - Performance issues - Security vulnerabilities ## v1.0.0 (2024-09-15) ### Added - Initial release of Altitude Platform - Basic edge function deployment - Environment variables - Custom domains - Basic monitoring ------------------------------------------- # File: astro-adapter/v4.0.1/index.mdx --- title: Astro Adapter description: Deploy your Astro site to Altitude with the official adapter --- # Astro Adapter The Altitude Astro Adapter allows you to deploy your Astro site to Altitude's edge network with minimal configuration. ## Installation ```bash npm install @altitude/astro-adapter@4.0.1 ``` ## Usage Add the adapter to your `astro.config.mjs` file: ```js import { defineConfig } from 'astro/config'; import altitude from '@altitude/astro-adapter'; export default defineConfig({ output: 'server', adapter: altitude(), }); ``` ## Configuration Options The adapter accepts the following options: ```js altitude({ // Your Altitude project name (optional) // Default: inferred from package.json projectName: 'my-astro-site', }) ``` ## Need Help? If you encounter any issues or have questions, please [open an issue](https://github.com/altitude/astro-adapter/issues) on our GitHub repository. ------------------------------------------- # File: astro-adapter/v5.0.1/index.mdx --- title: Astro Adapter v5.0.1 description: Deploy your Astro site to Altitude with the official adapter --- # Astro Adapter The Altitude Astro Adapter allows you to deploy your Astro site to Altitude's edge network with minimal configuration. ## Installation ```bash npm install @altitude/astro-adapter ``` > **Note:** Only npm is officially supported in Altitude. ## Usage Add the adapter to your `astro.config.mjs` file: ```js import { defineConfig } from 'astro/config'; import altitude from '@altitude/astro-adapter'; export default defineConfig({ output: 'server', adapter: altitude(), }); ``` ## Configuration Options The adapter accepts the following options: ```js altitude({ // Your Altitude project name (optional) // Default: inferred from package.json projectName: 'my-astro-site', // Additional build options (optional) buildOptions: { // Any additional build configuration // specific to your project needs } }) ``` ## Deployment After building your site with the adapter, you can deploy it to Altitude using the Altitude CLI: ```bash # Install the Altitude CLI if you haven't already npm install -g @altitude/cli # Deploy your site altitude deploy ``` ## Compatibility This version of the adapter is compatible with Astro 3.x and 4.x. ## Need Help? If you encounter any issues or have questions, please [open an issue](https://github.com/altitude/astro-adapter/issues) on our GitHub repository. ------------------------------------------- # File: browser-components/v0.2.0/index.mdx --- title: Browser Components description: Framework agnostic, browser native UI components for use in any project. --- import Preview from "@components/docs/preview.astro"; # Browser Components Welcome to the Altitude Browser Components Documentation! Use the links below to get started.

Get Started

Follow the guide to install the package into your project.

Explore The Altitude Design System

Follow the guide to install the package into your project.

Browse The Components

Follow the guide to install the package into your project.

------------------------------------------- # File: captcha/implementation-guides/friendly-captcha.mdx --- title: Friendly Captcha order: 4 description: Complete implementation examples for Friendly Captcha v2 integration. --- # Friendly Captcha Examples Friendly Captcha provides privacy-focused, GDPR-compliant bot protection that automatically solves puzzles without user interaction in most cases. It uses the Friendly Captcha (v2) SDK v0.1.31 and supports three different start modes. ## Key Points for Friendly Captcha ### Implementation Details - **SDK Version**: Uses v0.1.31 from CDN (`https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.1.31/site.min.js`) - **Widget Events**: Listens for `frc:widget.complete`, `frc:widget.error`, and `frc:widget.expire` - **Privacy-focused**: GDPR compliant, no third-party cookies - **Automatic solving**: Puzzles typically solve without user interaction ### Start Modes - **`auto`** (default): Puzzle starts automatically when widget loads - **`focus`**: Puzzle starts when user focuses on any form input within the same form - **`none`**: Puzzle must be started programmatically using `execute()` ### Best Practices - When using start mode `none`, you must call `execute()` manually to start the puzzle. For `auto` and `focus` modes, the puzzle starts automatically and `execute()` is not required. - Always handle all three event callbacks (`onToken`, `onError`, `onExpired`) - Reset widgets after successful form submission to clear stored tokens - The `reset()` method calls the widget's `reset()` function and clears token storage ## Auto Mode (Default) Auto mode starts the puzzle automatically when the widget loads, providing the smoothest user experience. However, in cases where a form on the page may not alwayds be submitted by users the `focus` mode may be more appropriate. ### Basic Implementation ```typescript import { Captcha } from '@thg-altitude/captcha'; const friendlyCaptcha = new Captcha({ provider: 'friendlyCaptcha', siteKey: 'your-friendly-captcha-site-key' }); await friendlyCaptcha.render('captcha-container', 'contact-form', { startMode: 'auto', // Default - widget starts solving immediately onToken: (captchaToken) => { // Called when frc:widget.complete event fires console.log('Friendly Captcha solved:', captchaToken); setToken(captchaToken); }, onError: () => { // Called when frc:widget.error event fires showErrorMessage('Security verification failed. Please try again.'); }, onExpired: () => { // Called when frc:widget.expire event fires showMessage('Security verification expired. Please complete it again.'); } }); ``` ### React Implementation ```typescript import { useEffect, useRef, useState } from 'react'; import { Captcha } from '@thg-altitude/captcha'; function ContactForm() { const captchaRef = useRef(null); const captcha = useRef(); const [captchaToken, setCaptchaToken] = useState(null); const [error, setError] = useState(''); useEffect(() => { captcha.current = new Captcha({ provider: 'friendlyCaptcha', siteKey: 'your-friendly-captcha-site-key' }); if (captchaRef.current) { captcha.current.render(captchaRef.current, 'contact-form', { startMode: 'auto', onToken: handleCaptchaToken, onError: handleCaptchaError, onExpired: handleCaptchaExpired }); } return () => { captcha.current?.reset('contact-form'); }; }, []); const handleCaptchaToken = (token: string) => { setCaptchaToken(token); setError(''); }; const handleCaptchaError = () => { setCaptchaToken(null); setError('Security verification failed. Please try again.'); }; const handleCaptchaExpired = () => { setCaptchaToken(null); setError('Security verification expired. Please complete it again.'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!captchaToken) { setError('Please wait for security verification to complete.'); return; } try { await submitForm(captchaToken); captcha.current?.reset('contact-form'); setCaptchaToken(null); } catch (error) { setError('Form submission failed. Please try again.'); } }; return (
); } ``` ### Vue Implementation ```vue ``` ------------------------------------------- # File: captcha/implementation-guides/hcaptcha.mdx --- title: hCaptcha order: 2 description: Complete implementation examples for hCaptcha integration. --- # hCaptcha Examples hCaptcha provides privacy-focused bot protection with both visible and invisible modes. Unlike reCAPTCHA, the same site key works for both visible and invisible implementations. ## Visible hCaptcha Visible hCaptcha displays a checkbox that users must interact with. The token is generated automatically when the user completes the challenge. ## Key Points for hCaptcha ### Configuration - **Single site key**: Same key works for both visible and invisible modes - **Visible mode**: Use `size: 'normal'` or `size: 'compact'` - **Invisible mode**: Use `size: 'invisible'` and manual execution ### Best Practices - Always handle all three event callbacks (`onToken`, `onError`, `onExpired`) - Reset widgets after successful form submission - For invisible mode, trigger execution on form submission, not page load - hCaptcha provides better privacy compliance compared to reCAPTCHA ### Basic Implementation ```typescript import { Captcha } from '@thg-altitude/captcha'; const hcaptcha = new Captcha({ provider: 'hcaptcha', siteKey: 'your-hcaptcha-site-key' // Same key works for both visible and invisible }); await hcaptcha.render('captcha-container', 'contact-form', { size: 'normal', // 'normal' | 'compact' onToken: (captchaToken) => { console.log('hCaptcha solved:', captchaToken); setToken(captchaToken); }, onError: () => { showErrorMessage('Security verification failed. Please try again.'); }, onExpired: () => { console.log('hCaptcha token expired'); showMessage('Security verification expired. Please complete it again.'); } }); ``` ### React Implementation ```typescript import { useEffect, useRef, useState } from 'react'; import { Captcha } from '@thg-altitude/captcha'; function ContactForm() { const captchaRef = useRef(null); const captcha = useRef(); const [captchaToken, setCaptchaToken] = useState(null); const [error, setError] = useState(''); useEffect(() => { captcha.current = new Captcha({ provider: 'hcaptcha', siteKey: 'your-hcaptcha-site-key' }); if (captchaRef.current) { captcha.current.render(captchaRef.current, 'contact-form', { size: 'normal', onToken: handleCaptchaToken, onError: handleCaptchaError, onExpired: handleCaptchaExpired }); } return () => { captcha.current?.reset('contact-form'); }; }, []); const handleCaptchaToken = (token: string) => { setCaptchaToken(token); setError(''); }; const handleCaptchaError = () => { setCaptchaToken(null); setError('Security verification failed. Please try again.'); }; const handleCaptchaExpired = () => { setCaptchaToken(null); setError('Security verification expired. Please complete it again.'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!captchaToken) { setError('Please complete the security verification.'); return; } try { await submitForm(captchaToken); captcha.current?.reset('contact-form'); setCaptchaToken(null); } catch (error) { setError('Form submission failed. Please try again.'); } }; return (
); } ``` ### Vue Implementation ```vue ``` ------------------------------------------- # File: custom-components/index.md --- title: Custom Components description: Documentation coming soon. --- Documentation coming soon ------------------------------------------- # File: astro-integration/v2.0.0/index.md --- title: Astro Integration description: Astro Integration configuration reference guide. --- # Astro Integration Middleware and utilities to make developing Altitude websites with Astro even better. - ๐Ÿ‘ฏโ€โ™‚๏ธ **[Multitenancy](./v2.0.0/reference/multi-tenancy)** Support for multi-tenancy via GTLD-based tenant-switching. - ๐ŸŒŽ **[i18n](./v2.0.0/reference/i18n):** Support for i18n via [GTLD](https://en.wikipedia.org/wiki/Generic_top-level_domain)-based localisation, powered by the Altitude Platform [Edge KV store](http://localhost:8080/docs/platform/v2.3.0/edge/kv-store) - ๐Ÿš€ **Performance Patterns:** Edge performance optimisations out of the box. - ๐Ÿ”Œ **[Commerce APIs](./v2.0.0/reference/commerce-api):** Access to the commerce API with ready-made helpers and proxies --- ## Resources - [NPM Package](https://www.npmjs.com/package/@thg-altitude/astro-integration) - [Repo](https://github.com/THG-AltitudeSiteBuilds/astro-integration) ------------------------------------------- # File: astro-integration/v3.0.0/index.md --- title: Astro Integration description: Astro Integration configuration reference guide. --- # Astro Integration The Astro Integration provides a comprehensive toolkit for building high-performance e-commerce websites. It seamlessly integrates with Astro sites to unlock powerful commerce capabilities, multi-tenant architecture, and global i18n supportโ€”all optimised for edge performance. - ๐Ÿ‘ฏโ€โ™‚๏ธ **[Multitenancy](./v3.0.0/reference/multi-tenancy)** Support for multi-tenancy via GTLD-based tenant-switching. - ๐ŸŒŽ **[i18n](./v3.0.0/reference/i18n):** Support for i18n via [GTLD](https://en.wikipedia.org/wiki/Generic_top-level_domain)-based localisation, powered by the Altitude Platform [Edge KV store](http://localhost:8080/docs/platform/v2.3.0/edge/kv-store) - ๐Ÿš€ **Performance Patterns:** Edge performance optimisations out of the box. - ๐Ÿ”Œ **[Commerce APIs](./v3.0.0/reference/commerce-api):** Access to the commerce API with ready-made helpers and proxies --- ## Resources - [NPM Package](https://www.npmjs.com/package/@thg-altitude/astro-integration) ------------------------------------------- # File: captcha/implementation-guides/recaptcha.mdx --- title: reCAPTCHA order: 1 description: Complete implementation examples for Google reCAPTCHA v2 integration. --- # Google reCAPTCHA v2 Examples Google reCAPTCHA v2 provides robust bot protection with both visible checkbox and invisible modes. It requires different site keys for visible vs invisible implementations. ## Visible reCAPTCHA Visible reCAPTCHA displays a checkbox that users must interact with. The token is generated automatically when the user completes the challenge. ## Key Points for reCAPTCHA ### Configuration - **Visible mode**: Use standard site key with `size: 'normal'` or `size: 'compact'` - **Invisible mode**: Requires different site key and `size: 'invisible'` - **Badge position**: For invisible mode, control badge placement with `badge` option ### Best Practices - Always handle all three event callbacks (`onToken`, `onError`, `onExpired`) - Reset widgets after successful form submission - For invisible mode, trigger execution on form submission, not page load ### Basic Implementation ```typescript import { Captcha } from '@thg-altitude/captcha'; const recaptcha = new Captcha({ provider: 'recaptcha', siteKey: 'your-recaptcha-site-key' // Different key needed for invisible mode }); await recaptcha.render('captcha-container', 'contact-form', { size: 'normal', // 'normal' | 'compact' onToken: (captchaToken) => { console.log('reCAPTCHA solved:', captchaToken); setToken(captchaToken); }, onError: () => { showErrorMessage('Security verification failed. Please try again.'); }, onExpired: () => { console.log('reCAPTCHA token expired'); showMessage('Security verification expired. Please complete it again.'); } }); ``` ### React Implementation ```typescript import { useEffect, useRef, useState } from 'react'; import { Captcha } from '@thg-altitude/captcha'; function ContactForm() { const captchaRef = useRef(null); const captcha = useRef(); const [captchaToken, setCaptchaToken] = useState(null); const [error, setError] = useState(''); useEffect(() => { captcha.current = new Captcha({ provider: 'recaptcha', siteKey: 'your-recaptcha-site-key' }); if (captchaRef.current) { captcha.current.render(captchaRef.current, 'contact-form', { size: 'normal', onToken: handleCaptchaToken, onError: handleCaptchaError, onExpired: handleCaptchaExpired }); } return () => { captcha.current?.reset('contact-form'); }; }, []); const handleCaptchaToken = (token: string) => { setCaptchaToken(token); setError(''); }; const handleCaptchaError = () => { setCaptchaToken(null); setError('Security verification failed. Please try again.'); }; const handleCaptchaExpired = () => { setCaptchaToken(null); setError('Security verification expired. Please complete it again.'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!captchaToken) { setError('Please complete the security verification.'); return; } try { await submitForm(captchaToken); captcha.current?.reset('contact-form'); setCaptchaToken(null); } catch (error) { setError('Form submission failed. Please try again.'); } }; return (
); } ``` ### Vue Implementation ```vue ``` ------------------------------------------- # File: captcha/implementation-guides/turnstile.mdx --- title: Cloudflare Turnstile order: 3 description: Complete implementation examples for Cloudflare Turnstile integration. --- # Cloudflare Turnstile Examples Cloudflare Turnstile provides seamless bot protection with minimal user friction. Widget behavior is configured in the Cloudflare dashboard, offering flexible appearance and execution modes. ## Visible Turnstile Visible Turnstile displays a widget that users interact with. By default, it executes automatically on render. ## Key Points for Turnstile ### Configuration - **Widget behavior**: Configured in Cloudflare dashboard, not just code - **Appearance modes**: - `always`: Widget always visible - `interaction-only`: Shows only when user interaction detected - `execute`: Shows only when manually executed - **Execution modes**: - `render`: Executes immediately when rendered (default) - `execute`: Waits for manual execution via `execute()` method - **Size options**: `normal`, `compact`, `flexible` ### Best Practices - Use `execution: 'execute'` with `appearance: 'execute'` for invisible-like behavior - Configure challenge difficulty and appearance in Cloudflare dashboard - Always handle all three event callbacks (`onToken`, `onError`, `onExpired`) - Reset widgets after successful form submission ### Basic Implementation ```typescript import { Captcha } from '@thg-altitude/captcha'; const turnstile = new Captcha({ provider: 'turnstile', siteKey: 'your-turnstile-site-key' }); await turnstile.render('captcha-container', 'contact-form', { size: 'normal', // 'normal' | 'compact' | 'flexible' appearance: 'always', // 'always' | 'interaction-only' | 'execute' onToken: (captchaToken) => { console.log('Turnstile solved:', captchaToken); setToken(captchaToken); }, onError: () => { showErrorMessage('Security verification failed. Please try again.'); }, onExpired: () => { console.log('Turnstile token expired'); showMessage('Security verification expired. Please complete it again.'); } }); ``` ### React Implementation ```typescript import { useEffect, useRef, useState } from 'react'; import { Captcha } from '@thg-altitude/captcha'; function ContactForm() { const captchaRef = useRef(null); const captcha = useRef(); const [captchaToken, setCaptchaToken] = useState(null); const [error, setError] = useState(''); useEffect(() => { captcha.current = new Captcha({ provider: 'turnstile', siteKey: 'your-turnstile-site-key' }); if (captchaRef.current) { captcha.current.render(captchaRef.current, 'contact-form', { size: 'normal', appearance: 'always', onToken: handleCaptchaToken, onError: handleCaptchaError, onExpired: handleCaptchaExpired }); } return () => { captcha.current?.reset('contact-form'); }; }, []); const handleCaptchaToken = (token: string) => { setCaptchaToken(token); setError(''); }; const handleCaptchaError = () => { setCaptchaToken(null); setError('Security verification failed. Please try again.'); }; const handleCaptchaExpired = () => { setCaptchaToken(null); setError('Security verification expired. Please complete it again.'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!captchaToken) { setError('Please complete the security verification.'); return; } try { await submitForm(captchaToken); captcha.current?.reset('contact-form'); setCaptchaToken(null); } catch (error) { setError('Form submission failed. Please try again.'); } }; return (
``` ------------------------------------------- # File: elements/facets/accessibility.mdx --- title: Accessibility --- The Elements package includes significant built-in accessibility features, but requires developers to manually provide labels to support proper internationalization and translation requirements. Labels should be provided as props outlined in the [configuration](/facets/configuration) guide. Labels only need to be provided to one instance of a component. This means that if you have two UIs for different viewports, labels provided to one UI will be inherited by the other. The attribute `facet-aria` will be added to elements where aria labels are expected to be added, where each attribute will have a key associated to it. The key will be consumed by the value provided by the application. ```javascript // Application // Elements ``` ### Dynamic Injection There might be occasions where data should be used to aid in the description of the label, such as the display name of the facet group or the number of selected options within a facet group. The elements package supports placeholder injection leveraging the existing mapping layer the `facet-inject` attribute uses. See more on this [here](/docs/elements/facets/attributes). With this approach, you can create templated aria labels that dynamically update during client hydration, as demonstrated in the example below. ```javascript // Application // Elements ``` No additional setup is required when using the default UI, the elements package will automatically map valid placeholders to the respective value from the facet data. ------------------------------------------- # File: elements/facets/attributes.mdx --- title: Attributes --- import { Tabs, TabItem } from "@components/docs"; import Preview from "@components/docs/preview.astro"; The elements package uses templates that are snapshotted from the original server side DOM structure to maintain the original UI design throughout rehydration. These templates are the shell of what the UI should look like, but during the server render there is no awareness as to how many options for example, should be loaded within a facet group. Instead, data attributes are used to signal to the elements package where data should be injected. There are multiple custom facet attributes that signal different behaviour. ### Injection The `facet-inject` attributes instructs the elements package that data should be injected in the **node** where the attribute is present. When defining additional UI through [slots](/docs/elements/facets/slots) or defining your own custom UI, data attributes will enable dynamic data to be injected without manual injection. ```javascript

``` In the above snippet, the `facetHeader` of the facet group will be injected in the paragraph tag. Below is a list of all valid values that can be provided to this attribute. - `facet-option-name-display` The display name of a given option. I.e "Size 11" - `facet-option-count` The matched product count of this option - `facet-group-name-display` The name of the facet group. I.e "Size" - `facet-search-letter` The first letter uppercased of an option. This is useful for a search facet to section options by first letter. - `facet-active-selections-count` The number of active selections. If there are no selections, this will not inject. - `facet-selected-count` The number of active selections. If there are no selections, this will inject `0`. - `facet-selections` Injects the display name of selected options in a single facet group in a comma seperated string. I.e "Size 7, Size 8". ### Triggers The `facet-trigger` attribute signals the package to bind event handlers to the node. The handlers associated are determined by the value of this attribute. - `options` Click event handler to add/remove this particular option. The attributes `facet-option-group-raw` and `facet-option-name-raw` should be present on this element to ensure query parameters are updated correctly. - `form` Submit event handler to be used with the slider facet. This will extract a min/max value from the form data. The attribute `facet-option-group-raw` should be present on this element. - `clear-section` Click event handler to remove all options from a facet group. The attribute `facet-option-group-raw` should be present on this element. - `clear-all` Click event handler that removes all selected options. ```javascript ``` ### Data attributes The elements package attaches values to data attributes to provide context to utility functions, such as the `facet-trigger` bindings in the section above, when the options are hydrated. - `facet-option-group-raw` The raw name of the facet group will be added to the value of this attribute. - `facet-option-name-raw` The raw name of the option will be added to the value of this attribute. - `facet-search-letter` The first character of the options display name uppercased will be added to the value of this attribute. ```javascript ``` ### Transmitters and Receivers Transmitters and receivers enable toggling of an "active" state between nodes that share the same value. Transmitters will have an event listner attached, which toggles an `active` class on the receivers to enable styling to take effect such as toggling `display: none;` vs `display: block;` for example. The default facets UI configures two types of transmitters and receivers; `options` and `toggle` - `options` This value should be used on the node that triggers your options to become active. The receiver should be on the node that your options are contained within. - `toggle` This value should be used on the node that shows more/less facets. The receiver should be the most outer node of your individual facet group. ```javascript

...
``` ### Bindings There might be a UI where there are two input options and the value needs to be bound to one another. Providing the same value to the `facet-binding` attribute will link the values of these two input fields. A good example of where this is used, is in the `FacetSlider` component where the input types, `range` and `number` are bound. ```javascript ``` ### Utility attributes There are additional utility attributes that aid the package with injecting DOM nodes and UI visibility behaviour. - `facet-template-group` Marks this node and all of its children as a template. The value of this attributes should be one of the following: `SimpleFacet`, `RangedFacet`, `SearchFacet` or `SliderFacet` - `facet-template-hidden` Will keep the element in the DOM for the purpose of template snapshotting, but enforces `display: none;` so the element is not visible. - `facet-tabs` Used to query the DOM for the facet tabs template and injection. - `facet-options` The most outer container of where the facet template should be injected. - `facet-options-wrapper` The container where individual options should be injected into. - `facet-options-section` The template for each option. - `facet-slider-min` The input node(s) for miniumum values. If more than one input is used for the mininum value, use the `facet-binding` attribute. - `facet-slider-max` The input node(s) for maximum values. If more than one input is used for the maximum value, use the `facet-binding` attribute. - `facet-template-search` Used to query the DOM for the template of the search letter that is injected. - `facet-search` The input node for searching. ------------------------------------------- # File: elements/facets/configuration.mdx --- title: Configuration --- # Props ## FacetWrapper Props The `FacetWrapper` component requires one instance per page. Its props configure the API endpoint, URL parameters, and shared information such as currency symbols. #### filterName - โ€ข **Type:** `String` - โ€ข **Required:** Yes - The URL parameter name used for facets (e.g. `facetFilters`). ### Options Object The `options` prop contains settings for API requests: #### endpoint - โ€ข **Type:** `String` - โ€ข **Required:** Yes - Path to your GraphQL endpoint - this **must** be a relative path (e.g. `/api/commerce`). #### pageType - โ€ข **Type:** `'search' | 'collection'` - โ€ข **Required:** Yes - Determines the schema to be used. #### query - โ€ข **Type:** `String` - โ€ข **Required:** Yes - Search query or collection path handle. #### shippingDestination - โ€ข **Type:** `String` - โ€ข **Required:** Yes - Shipping destination code for Horizon requests. #### currency - โ€ข **Type:** `String` - โ€ข **Required:** Yes - Currency code for Horizon requests. #### currencySymbol - โ€ข **Type:** `String` - โ€ข **Required:** No - The currency symbol to be appended to pricing labels #### concessionCode - โ€ข **Type:** `String` - โ€ข **Required:** No - Concession code to be passed in Horizon requests. There is no default value here so concessionCode will not be included in the query if one is not provided in the props. #### skipRedirects - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Value of skipRedirects to be passed in Horizon requests #### skipRecursivePathSearch - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Value of skipRecursivePathSearch to be passed in Horizon requests, for collection page queries only ### Example Configuration ```javascript {/* Slot content */} ``` ## FacetResults Props Multiple `FacetResults` components can be used within one `FacetWrapper`. This is useful for different UIs between desktop and mobile viewports. #### id - โ€ข **Type:** `String` - โ€ข **Required:** Yes - Unique identifier for each instance of the FacetResults component. #### facetPayload - โ€ข **Type:** `Array` - โ€ข **Required:** Yes - Pre-hydration data for facet headers to aid in rendering an initial loading states. Must be processed using `validateFacets` helper to remove unnecessary facet headings. #### singleActiveFacet - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `true` - Controls whether multiple facets can be "active" at a time. If the desired behaviour is to enable multiple facets to be active at a time `false` should be provided. This prop also controls click away behaviour which is enabled when this prop is set to `true`. #### hydrateOptions - โ€ข **Type:** `visible` | `all` - โ€ข **Required:** No - โ€ข **Default:** `visible` - Controls the hydration behaviour upon initial render. By default options will only be hydrated when the `options` receiver is toggled i.e top level facet group is clicked. If options are to be hydrated immediately, set this props value to `all` #### facetRenderCount - โ€ข **Type:** `*` | `Number` - โ€ข **Required:** No - โ€ข **Default:** `*` - Determines how many options to hydrate. By default all options will be hydrated in else a number can be supplied to cap the number that are hydrated. Helper functions can be used in conjuction with custom UI to implement a "Show More" toggle. #### visibleOptions - โ€ข **Type:** `*` | `Number` - โ€ข **Required:** No - โ€ข **Default:** `*` - Determines the number of top level facets that are shown. By default all top level facets are shown. When a number is passed, if the number of facet headings exceeds this number, additional UI will be hydrated on to the page enabling toggle behaviour. #### keepSelectedGroupActive - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Attempts to keep the facet group where a selection was made active. By default this behaviour is turned off due to inconsistency in facet heading ordering. This prop should only be set to true when your UI does not display other facet headings when the facet is active i.e A mobile drilldown menu. The facet type `SliderFacet` will not stay active to provide visual feedback to users. #### text A text object can be passed to add copy to parts of the default UI. The only copy required for this default UI is the `toggle` key. #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `moreFiltersToggle` - Label applied to the toggle button when not expanded i.e. "Show more filters" - `lessFiltersToggle` - Label applied to the toggle button when expanded i.e. "Show less filters" - `toggleFacetButton` - Label applied to the top level facet element which triggers visibility of options. ### Example Configuration ```javascript {/* Slot content */} ``` ## FacetList Props The FacetList provides an out the box UI for facets with the following typename: `SimpleFacet`. This should be nested within the `FacetResults` component. This should consume the named slot `facet-list` #### open - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Sets the options to be active when first hydrated. If `true` is set, it is advised to set the `hydrateOptions="all"` prop in `FacetResults` to ensure the template is updated. Named [slots](/docs/elements/facets/slots) can be used to configure loading states to minimise CLS. #### text A text object can be passed to add copy to parts of the default UI. The only copy required for this default UI is the `clear` key. #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `clearFacetButton` - Label applied to the clear button within a facet group. ### Example Configuration ```javascript {/* slot content */} ``` ## FacetRange Props The FacetRange provides an out the box UI for facets with the following typename: `RangedFacet`. This should be nested within the `FacetResults` component. This should consume the named slot `facet-range` #### open - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Sets the options to be active when first hydrated. If `true` is set, it is advised to set the `hydrateOptions="all"` prop in `FacetResults` to ensure the template is updated. Named [slots](/docs/elements/facets/slots) can be used to configure loading states to minimise CLS. #### text A text object can be passed to add copy to parts of the default UI. The only copy required for this default UI is the `clear` key. #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `clearFacetButton` - Label applied to the clear button within a facet group. ### Example Configuration ```javascript {/* slot content */} ``` ## FacetSearch Props The FacetRange provides an out the box UI for facets where the `facetName` contains `_brand_`. This should be nested within the `FacetResults` component. This component enables users to filter options through an input field. This should consume the named slot `facet-search` #### open - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Sets the options to be active when first hydrated. If `true` is set, it is advised to set the `hydrateOptions="all"` prop in `FacetResults` to ensure the template is updated. Named [slots](/docs/elements/facets/slots) can be used to configure loading states to minimise CLS. #### text A text object can be passed to add copy to parts of the default UI. The only copy required for this default UI is the `clear` key. #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `clearFacetButton` - Label applied to the clear button within a facet group. - `searchInput` - Label applied to the search input. ### Example Configuration ```javascript {/* slot content */} ``` ## FacetSearchFilter The FacetSearchFilter is an optional component and should be used nested inside of the [FacetSearch](#facetsearch-props) component. This component should consume the slot `elements-facets-filter-bar` when used. This component enables character filtering allowing users to filter based on the first character of options. #### text A text object can be passed to add copy to UI. The only copy required for this UI is the `all` key. ### Example Usage ```javascript ``` ## FacetSlider Props The FacetSlider provides an out the box UI for facets with the following typename: `SliderFacet`. This should be nested within the `FacetResults` component. This should consume the named slot `facet-slider` #### open - โ€ข **Type:** `Boolean` - โ€ข **Required:** No - โ€ข **Default:** `false` - Sets the options to be active when first hydrated. If `true` is set, it is advised to set the `hydrateOptions="all"` prop in `FacetResults` to ensure the template is updated. Named [slots](/docs/elements/facets/slots) can be used to configure loading states to minimise CLS. #### step Enables configuration of the step attribute on inputs allowing more granular values to be visible. - โ€ข **Type:** `String` - โ€ข **Required:** No - โ€ข **Default:** `1` #### text A text object can be passed to add copy to parts of the default UI. The following keys can be supplied to the text object: - โ€ข `clear` - โ€ข `currency` - โ€ข `submit` #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `clearFacetButton` - Label applied to the clear button within a facet group. ### Example Configuration ```javascript {/* slot content */} ``` ## FacetTabs Props The FacetTabs component provides an out the box UI for selected facets with built in handlers for removing individual or all selections. This should be nested within the `FacetResults` component. This component can be consumed by one of two named slots: `facet-tabs-start` or `facet-tabs-end`. This renders the UI in the DOM flow within FacetResults before or after the Facets UI. #### text A text object can be passed to add copy to parts of the default UI. The only copy required for this components default UI is the `clear` key. #### aria An object can be passed to provide aria labels to elements. The following keys can be used to assign a label to this components default UI. More details on how to configure accessibility labels can be viewed [here](/docs/elements/facets/accessibility) - `clearOptions` - Label applied to each individual selected option. - `clearAll` - Label applied to the element to clear all selected options. ### Example Configuration ```javascript ``` ## Client Script The elements package will initialise facets on the users behalf hydrating facets in as per the `hydrateOptions` value on each facet instance. ### Initialisation An application will be responsible for defining the `elements-facet-wrapper` custom HTML Element on the page. ```javascript class ElementsFacetWrapper extends HTMLElement { constructor() { super(); // Additional setup } } customElements.get("elements-facet-wrapper") || customElements.define("elements-facet-wrapper", ElementsFacetWrapper); ``` ### Observer When notable events occur, the elements package will publish these events on different channels. This enables an application to subscribe to these channels and run any neccessary updates i.e refreshing a product list. The PubSub instance is contained with the facets class on the window object (`window.elements.facets`). The elements package will initialise this class upon setup and emit a custom event which will enable applications to access this class an the observer, as well as [helper functions](/docs/elements/facets/functions). There are three channels intended for applications to subscribe to: - `facet-toggle` This is published to when more/less facet groups are shown. - `facet-hydrated` This is published to when a facet group has it's options hydrated in. - `refresh-product-list` This is published to when facet data has been refetched, signalling an application should also refresh it's product list in line with the new selected facet options. ```javascript connectedCallback() { window.addEventListener( 'elements-facet-wrapper', () => { const { observer } = window.elements.facets observer.subscribe('facet-toggle', (data) => {...} ) } ); } ``` ------------------------------------------- # File: elements/facets/helpers.mdx --- title: Helper Functions --- # Helper Functions Helper functions are attached to the facet class on the window object: `window.elements.facets`. When using the default UI, the custom HTML elements leverage these helper functions to hydrate and bind attributes on the templates. Leveraging these helper functions will streamline custom UI setup, abstracting functionality into simple function calls. ## Core Functions ### hydrateOptionTemplate - โ€ข **Facet Element** - `HTMLElement` - The `facet-element` node - โ€ข **Template** - `HTMLElement` - The template to be injected - โ€ข **Facet Data** - `Object` - The data for this particular facet - โ€ข **Current Selections** - `Array` - Any current selected options - โ€ข **Keep Active** - `Boolean` - When true, applies the active class to the transmitter to keep this facet group active Hydrates the template into the facet element node, mapping any injectors or attributes before the template is appended. ### mapOptions - โ€ข **Facet Element** - `HTMLElement` - The `facet-element` node - โ€ข **Facet Data** - `Object` - The data for this particular facet - โ€ข **Current Selections** - `Array` - Any current selected options - โ€ข **Facet Type** - `String` - The type of facet (e.g., `RangedFacet`) - โ€ข **Options to render** - `Number | "*"` - How many options to render initially. Use `*` to show all options. Maps and injects data for list-based facet types. Supports `ListFacet`, `RangedFacet`, and `SearchFacet` types. ### mapSlider - โ€ข **Facet Element** - `HTMLElement` - The `facet-element` node - โ€ข **Facet Data** - `Object` - The data for this particular facet - โ€ข **Current Selections** - `Array` - Any current selected options Maps inputs and binds event listeners for slider-based facets. Supports the `SliderFacet` type. ### mapTabs - โ€ข **Tabs Element** - `HTMLElement` - The node with the attribute `facet-tabs` - โ€ข **Current Selections** - `Array` - Any current selected options Maps attributes and injectors for the tabs component based on current selections. If there are no current selections, the attribute `facet-template-hidden` will be applied to hide this element. ### toggleOptions - โ€ข **Facet Element** - `HTMLElement` - The `facet-element` node where the class is to be toggled - โ€ข **Event** - `Event` - The event object Toggles the `active` class on the specified facet element. ------------------------------------------- # File: elements/facets/index.mdx --- title: Get Started order: 1 --- import { Tabs, TabItem } from "@components/docs"; import Preview from "@components/docs/preview.astro"; import Facets from "@components/docs/facets/Facets.astro"; import StyleDownloader from "@components/docs/facets/StyleDownloader.astro"; ## Overview Facets provides an out of the box UI for filterable navigation for product listings. The Elements package handles: - โ€ข Facet loading and state management - โ€ข Automatic rehydration - โ€ข Out-the-box UI styling for medium to large viewports - โ€ข Accessibility features ## Installation ```sh npm install @thg-altitude/elements ``` ## Components The facets system consists of the following components: - โ€ข **FacetWrapper**: Required container component (one per page) - โ€ข **FacetResults**: Wrapper for facet groups - multiple can be nested within the wrapper i.e Desktop UI and Mobile UI - โ€ข **FacetList**: Template UI for a simple option list - โ€ข **FacetRange**: Template UI for a ranged option list - โ€ข **FacetSearch**: Template UI for an option list with search functionality for filtering options - โ€ข **FacetSlider**: Template UI for min/max values such a price filtering - โ€ข **FacetTabs**: Template UI to display currently selected options ### Basic Implementation Follow these steps to implement facets in your application: 1. **Import the components and helpers:** ```javascript import { FacetWrapper, FacetResults, FacetList, FacetSearch, FacetRange, FacetSlider, FacetTabs, } from "@thg-altitude/elements/astro"; import { validateFacets, formatQueryString, } from "@thg-altitude/elements/facets/helper"; ``` 2. **Fetch and format your facet data:** - โ€ข Use the `validateFacets` helper to process and filter unnecessary facets from your application once fetched. 3. **Configure the UI components:** - โ€ข Set the required props (see [Configuration](/docs/elements/facets/configuration) for details) 4. **Setup the client script to subscribe to events:** - โ€ข Use the observer to subscribe to channels when events occur (see [Configuration](/docs/elements/facets/configuration) for details) The below example displays the default UI out of the box for both desktop and mobile views. This UI can be customised either through styling or consuming named [slots](/facets/slots).
```astro --- import { FacetWrapper, FacetResults, FacetList, FacetSearch, FacetRange, FacetSlider, FacetTabs } from "@thg-altitude/elements/astro"; import { validateFacets } from "@thg-altitude/elements/facets/helper"; const response = await fetch(...) const { data } = await response.json(); const facets = validateFacets(data?.search?.productList?.facets;); const numberOfProducts = data?.search?.productList?.total --- // UI setup
// Client side script setup ````
## Custom Styling Components that the elements package exports all contain default styling using vanilla CSS. The styles can be overriden per application by using class based css selectors. The default styling uses lowest specificity possible. To override a specific class, the wrapper class for the element i.e `.elements-facets-wrapper` should prefix all selectors in your stylesheet. ```css .elements-facets-wrapper .elements-facets-dropdown { /* Your custom styles */ } /* Responsive breakpoints handle layout changes */ @media (width < 64rem) { .elements-facets-wrapper .elements-facets-dropdown { /* Small viewport styles */ } } ```` The default styling aligns with larger viewports due to it's horizontal layout. But the below code can be added to your applications stylesheet for a vertical layout which might be suitable for smaller viewports. ------------------------------------------- # File: elements/facets/slots.mdx --- title: Slots --- import { Tabs, TabItem } from "@components/docs"; import Preview from "@components/docs/preview.astro"; ## Overview The Elements package leverages slots to allow customisation on top of, or to replace the default UI. ## FacetResults The `FacetResults` component is the most outer layer of the Facets UI you can modify. There are two options in order to modify the UI: 1. Consume the slot `elements-facets-results-container` to create a completely bespoke UI. 2. Consumed additional named slots to modify aspects of the UI. Each slot has a unique name which can be consumed by additional out-the-box components exported by the Elements package, or through custom markup: - `` UI for currently selected Facets, placed **before** facets in the DOM tree - An out-the-box UI component for this slot is exported as `FacetTabs` - `` UI for currently selected Facets, placed **after** facets in the DOM - An out-the-box UI component for this slot is exported as `FacetTabs` - `` UI for sort options **within** parent node of any facets - `` UI for any additional content on the top level facet button i.e Selected counters - `` UI for the facet group `SimpleFacet` - An out-the-box UI component for this slot is exported as `FacetList` - `` UI for the facet group `RangedFacet` - An out-the-box UI component for this slot is exported as `FacetRange` - `` UI for the facet group `FacetSlider` - An out-the-box UI component for this slot is exported as `FacetSlider` - `` UI for the facets where the facetName contains `_brand_` - An out-the-box UI component for this slot is exported as `FacetSearch` ## UI Component slots When using the out-the-box UI components for the facet groups, additional named slots can be consumed to change or extend the display of these components. Adding additional markup **without** consuming a named slot will result in a completely custom UI. Below is a list of the named slots and the component these named slots are present in: - `` Additional UI can be added above the facet options.
_Components: **All**_ - `` The UI for the facet options can be overriden by consuming this slot.
_Components: **FacetList**, **FacetRange**, **FacetSearch**_ - `` The UI for the slider can be overriden by consuming this slot.
_Components: **FacetSlider**_ - `` Additional UI can be added below the facet options.
_Components: **All**_ - `` The UI for the search input can be overriden by consuming this slot.
_Components: **FacetSearch**_ ```astro

This is custom markup

``` ## Custom UI When creating a UI using custom markup, it is worth noting that the markup provided to the elements package is intended to be used as a template. Data [attributes](/docs/elements/facets/attributes) are used to inject and populate dynamic data to these templates. This means that the UI defined for a facet group will be the same UI for all instances of that same facet group. Before defining a custom UI or consuming named slots, it is recommended to consult the documentation on how attributes are used. ------------------------------------------- # File: elements/guides/example.md --- title: Example Guide description: A guide in my new Starlight docs site. --- Guides lead a user through a specific task they want to accomplish, often with a sequence of steps. Writing a good guide requires thinking about what your users are trying to do. ## Further reading - Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diรกtaxis framework ------------------------------------------- # File: elements/variations/configuration.mdx --- title: Configuration --- import VariationProduct from "@components/docs/variations/variationsProduct.vue"; import Variations from "@components/docs/variations/Variations.vue"; import VariationsAstro from "@components/docs/variations/VariationsAstro.astro"; import { Tabs, TabItem } from "@components/docs"; import Preview from "@components/docs/preview.astro"; The variation component takes in props which heavily influence the behaviour of the component. #### ID **Type:** `String` \ **Required: True** The prop `id` must always be passed and must be unique if there are multiple instances of this component on a page. The id will be attached to a container which will hydrate the interactive variations into the container. #### Mode **Type:** `String` \ **Required: False** \ **Default:** `'blocking'` \ **Accepted Values:** `'async'` | `'blocking'` The `mode` prop dictates whether the hydration should occur immediately or whether it should be deferred. - **`'blocking'` (default)**: Renders variations immediately with full functionality. The component displays a presentational layer using the provided product data, then hydrates interactive elements. This reduces Cumulative Layout Shift (CLS) and provides immediate user interaction. - **`'async'`**: Shows a loading state initially and defers variation hydration until manually triggered. When using `'blocking'` mode, product data **must** be sent to the component within the [options](/packages/elements/#options) prop. This will be used to display a presentational layer before interactive elements are hydrated, which reduces Cumulative Layout Shift (CLS). Components using `'async'` mode can utilise a named slot `elements-variations-loading` to apply a custom loading state. ## Vue For applications using the Vue variations component, a `ref` containing the value of the product data should always be passed to the options block regardless of `mode` as the component will be reactive to this ref being updated, such as an api call to retrieve data on the client. The package will trigger hydration as soon as product data is recieved. ```vue ``` ```vue ``` ## Astro For applications using the Astro variation component, there is required setup on the client side to define a custom class named `elements-variations`. For `mode: async` an application is required to invoke hydration by importing the `Variation` helper function. This function requires the following fields: - `id: String` - This should be the same ID as attached to the variation instance and can easily obtained from the custom class attribute `data-id`. - `Object.config: Object` - This contains the `payload` from the client side call including fields listed in the schema outlined in the [options](/packages/elements/#options) section, `layout` options and the `activeVariant` sku. The `Variation` function returns back an observer, which can be used to [subscribe to channels](/packages/elements/#event-subscription) For non-async modes, the custom element must still be defined, but the Astro variation component will hydrate the interactive elements in. The observer to be used to [subscribe to channels](/packages/elements/#event-subscription) will be added to the window object as `window.elements[id]`. ```astro --- import { VariationAstro } from "@thg-altitude/elements/astro"; const sku = Astro.props.sku ---

Loading...

```
```astro --- import { VariationAstro } from "@thg-altitude/elements/astro"; const sku = Astro.props.sku --- ```
#### Options The `options` prop will contain additional fields which allows customisability as to how the variations should be displayed in the presentational layer. ##### options.layout **Type:** `Object` \ **Required: False** The Variations component supports two variation styles, dropdowns and buttons. By default, all variations will display as a dropdown. If this is not desired, an optional prop `layout` can be supplied to `options` to specify what "Option Keys" should be displayed as buttons. Layout options will only take effect when `productData` is supplied through component props, or passed through the `config.layout` key to the [Variations](/packages/elements/#astro) helper function depending on the mode. ##### options.activeVariant **Type:** `Number` \ **Required: False** The active variant sku will be used to pre select a specific product variation. If no active variant sku has been supplied, the elements package will default to the first in stock product in the variants array, if no products are in stock the first variant will be used. ##### options.product **Type:** `Object` \ **Required: Framework Dependant - see [mode](/packages/elements/#mode)** When the `mode` prop is **not** set to `async` (or no mode is supplied), a product block must be sent to the component using the fields provided in the below schema. This product block will be used to render the presentational layer before the interactive elements are hydrated in. ```vue ``` ```astro ``` ```gql product(sku: $sku, strict: false) { sku linkedOn options { key choices { optionKey key colour title } } variants { sku inStock choices { optionKey key colour title } } defaultVariant( options: { currency: $currency shippingDestination: $shippingDestination } ) { sku inStock choices { optionKey key colour title } } } ``` ## Event Subscription When variations are interacted with, the variation component will publish data relating to that variation. The data published will contain the `sku` and `stock` of the given product. Applications can then opt in to make any relavant presentational updates when variations change. This data will be retrieved differently dependant on the framework used as seen in the examples below. ```vue ``` ```astro --- import { VariationAstro } from "@thg-altitude/elements/astro"; const sku = Astro.props.sku --- ``` ## Custom Styling Components that the elements package exports all contain default styling using vanilla CSS. The styles can be overriden per application by using class based css selectors. The default styling uses lowest specificity possible. To override a specific class, the container class for the element i.e `.elements-variations-container` should prefix all selectors in your stylesheet. ```css .elements-variations-container .elements-variant-button-selected { border-color: red; } ``` ------------------------------------------- # File: elements/variations/index.mdx --- title: Get Started description: Framework agnostic components for core commerce functionality. order: 1 --- Variations offers an abstraction from verbose business logic to render the variations of a given product, offering flexibility on presentational layers whilst providing out the box rehydration. import VariationProduct from "@components/docs/variations/variationsProduct.vue"; import Variations from "@components/docs/variations/Variations.vue"; import VariationsAstro from "@components/docs/variations/VariationsAstro.astro"; import { Tabs, TabItem } from "@components/docs"; import Preview from "@components/docs/preview.astro"; ## Installation ```sh npm install @thg-altitude/elements ``` ## Setup The elements package exports framework specific components which can be imported into your application.
```vue ``` ```astro --- import { VariationAstro } from "@thg-altitude/elements/astro"; --- ```
------------------------------------------- # File: insights/advanced/browser-requirements.mdx --- title: Browser Requirements order: 3 description: Browser compatibility and requirements for Insights --- # Browser Requirements Insights is built with modern browser APIs for optimal performance and reliability. The SDK uses ES Modules for loading and the Beacon API for reliable event transmission. ## Minimum Browser Versions | Browser | Minimum Version | | ---------------- | --------------- | | Chrome | 80+ | | Firefox | 72+ | | Safari | 14+ | | Edge | 80+ | | Chrome Android | 80+ | | iOS Safari | 14+ | | Samsung Internet | 14+ | ## Required APIs The following browser APIs are used by Insights: | API | Purpose | Fallback | |-----|---------|----------| | ES Modules | Script loading | None (required) | | Beacon API | Reliable event transmission | `fetch()` with `keepalive` | | Intersection Observer | Impression tracking | Disabled if unavailable | | Fetch API | Token requests, data upload | None (required) | | localStorage | Token persistence | In-memory (current session only) | ## Graceful Degradation Insights degrades gracefully in environments with limited API support: - **Beacon API unavailable:** Falls back to `fetch()` with the `keepalive` flag for reliable transmission during page unload - **Intersection Observer unavailable:** HTML attribute impression tracking is disabled; programmatic tracking via `window.insights.impression()` still works - **localStorage unavailable:** Tokens are stored in memory for the current session only - **Core Web Vitals unavailable:** Performance metrics are omitted from page view data In all cases, core page view and event tracking continue to function. No errors are thrown in unsupported environments. ## Script Loading Insights must be loaded as an ES Module: ```html ``` The `type="module"` attribute is required. Without it, the script will not execute. ## Next Steps - **[Installation](../installation)** - Add the script tag to your application - **[Tracking](../tracking)** - Set up tracking for your application ------------------------------------------- # File: insights/advanced/javascript-api.mdx --- title: JavaScript API order: 1 description: Programmatic tracking methods for advanced scenarios --- # JavaScript API Use the JavaScript API when HTML attribute tracking isn't suitable - for example, tracking hover events, form submissions, or integrating with existing event handlers. For most use cases, [HTML attribute tracking](/docs/insights/tracking) is simpler and recommended. ## API Reference All methods are available on the global `window.insights` object after the script has loaded. ### `window.insights.event(name, data)` Track custom events programmatically. **Parameters:** - `name` (string): Event type (e.g., `'hover'`, `'form_submit'`, `'video_play'`) - `data` (object): Event data with any custom properties **Returns:** `void` - Event is queued for batch upload ```javascript // Track a hover event window.insights.event("hover", { category: "product", id: "featured-product", price: "19.99", }); // Track form submission window.insights.event("form_submit", { category: "form", id: "newsletter-signup", success: true, }); // Track video interaction window.insights.event("video_play", { category: "video", id: "product-demo", duration: 120, }); ``` ### `window.insights.impression(data)` Track impressions programmatically. Useful when HTML attributes can't be applied. **Parameters:** - `data` (object): Impression data with any custom properties **Returns:** `void` - Event is queued for batch upload ```javascript // Track impression for dynamically loaded content window.insights.impression({ category: "recommendation", id: "similar-product-123", position: 3, }); ``` ### `window.insights.getPageUUID()` Get the current page UUID. Useful for correlating front-end events with back-end data. **Returns:** `string` - Current page UUID, or `'unknown'` if not yet initialised ```javascript const pageUUID = window.insights.getPageUUID(); console.log("Current page UUID:", pageUUID); ``` ### `window.insights.uuid` Direct access to the current page UUID as a property. **Type:** `string | undefined` - Current page UUID, or `undefined` before initialisation ```javascript console.log("Page UUID:", window.insights.uuid); ``` > **Note:** The `.uuid` property may be `undefined` before the SDK has initialised. Use `getPageUUID()` if you need a guaranteed string value (returns `'unknown'` when not initialised). ## Debug Mode Enable debug logging to see tracking activity in the browser console: ```javascript localStorage.setItem('insights_debug', 'true'); ``` Reload the page to see debug output. To disable: ```javascript localStorage.removeItem('insights_debug'); ``` ## Custom Events Track any event by defining your own event types. The event name and data structure are flexible - use whatever makes sense for your analytics needs. ```javascript // Track a hover event window.insights.event("hover", { category: "product", id: "featured-item", price: "29.99", }); ``` Events are queued and sent automatically - no additional configuration required. ## Safe Usage Pattern Always check that the SDK has loaded before calling methods: ```javascript if (typeof window.insights !== "undefined") { window.insights.event("purchase", { category: "ecommerce", id: "order-123", value: 99.99, }); } ``` ## Dynamic Content Elements added to the page after initial load are automatically detected for HTML attribute tracking. No additional JavaScript is needed for single-page applications or dynamically loaded content. ## Next Steps - **[Event Tracking](../tracking)** - HTML attribute tracking (simpler approach) ------------------------------------------- # File: insights/advanced/query-param-blocklist.mdx --- title: Query Parameter Blocklist order: 3 description: Filter sensitive query parameters from analytics tracking --- # Query Parameter Blocklist The query parameter blocklist allows you to prevent sensitive URL parameters from being captured in analytics data. This is useful for excluding tokens, passwords, or other private information that may appear in URLs. ## Configuration Add the `data-insights-blocklist` attribute to your script tag with comma-separated patterns: ```html ``` ## Pattern Syntax Blocklist patterns support both literal strings and regular expressions: | Pattern | Matches | Example URLs | |---------|---------|--------------| | `token` | Exact match | `?token=abc` | | `token.*` | Starts with "token" | `?token=abc`, `?tokenId=123` | | `^secret$` | Exact match (explicit) | `?secret=xyz` | | `pass\w+` | "pass" followed by word chars | `?password=abc`, `?passkey=123` | All matching is **case-insensitive**, so `token` will match `Token`, `TOKEN`, and `token`. ## How It Works When a page view is captured, the library: 1. Parses all query parameters from the current URL 2. Checks each **parameter name** against the blocklist patterns (values are not checked) 3. Excludes any matching parameters from the analytics payload > **Note**: The blocklist matches against parameter names only, not their values. A pattern like `token` will block any parameter named "token" regardless of its value, but will not block a parameter like `?type=token`. **Example**: Given the URL `?user=john&token=secret123&page=1` with blocklist `token`: ```json { "page": { "query_params": { "user": ["john"], "page": ["1"] } } } ``` The `token` parameter is excluded from the captured data. ## Multiple Patterns Separate multiple patterns with commas. Whitespace around patterns is automatically trimmed: ```html ``` ## Security Features The blocklist includes built-in protections: ### ReDoS Protection Dangerous regex patterns that could cause performance issues are automatically detected and rejected. These patterns fall back to literal string matching: - Nested quantifiers: `(a+)+` - Overlapping alternations: `(a|a)+` - Repeated wildcards: `.*.*` ### Invalid Pattern Handling If a pattern contains invalid regex syntax, it gracefully falls back to a literal string comparison rather than throwing an error. ## Programmatic Configuration When using the JavaScript API, pass the blocklist as an array: ```javascript window.insights.init({ channel: 'web', subsite: 'en', storefront: 'uk', queryParamBlocklist: ['token', 'secret', 'password'] }); ``` ## Common Use Cases | Use Case | Recommended Patterns | |----------|---------------------| | Authentication tokens | `token, auth, jwt, bearer` | | Session identifiers | `session.*, sid` | | Password reset flows | `password, reset_token` | | OAuth flows | `code, state, oauth.*` | | Tracking parameters | `utm_*, fbclid, gclid` | > **Note**: The blocklist only affects query parameters captured in page view data. It does not modify the actual URL or affect other tracking features. ## Next Steps - [JavaScript API](./javascript-api) - Programmatic control over analytics - [Browser Requirements](./browser-requirements) - Supported browsers and features ------------------------------------------- # File: platform/v2.1.0/getting-started.md --- title: Getting Started description: Get started with Altitude Platform order: 1 --- # Getting Started with Altitude Platform Welcome to Altitude Platform! This guide will help you get started with our platform. ## Prerequisites Before you begin, make sure you have: - A GitHub account - Basic familiarity with Git - Node.js installed (version 14 or higher) ## Quick Start 1. **Sign up for Altitude Platform** - Go to [Altitude Platform](https://www.platform.thgaltitude.com) - Create an account or log in 2. **Connect to GitHub** - Connect your GitHub account - Select the repositories you want to use 3. **Create Your First Site** - Choose a starter kit or select an existing repository - Configure your site settings 4. **Deploy Your Site** - Select a branch to deploy - Choose an environment - Deploy your site 5. **Access Your Site** - Use the provided URL to access your deployed site - Share the URL with others ## Next Steps Now that you've deployed your first site, you can: - [Explore Edge Functions](./edge/functions) - [Learn about Environment Management](./walkthrough/sites) - [Discover Starter Kits](./kits/introduction) For more detailed information, check out our [documentation](/docs/platform). ------------------------------------------- # File: astro-integration/guides/edge-kv.md --- title: Edge KV description: Configuring Cloudflare edge kv. --- Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. Edge Key-Value stores (KVs) enable fast, scalable, distributed data storage at network edges, reducing latency for global applications. To configure your Altitude KV store with Astro create a [middleware](https://docs.astro.build/en/guides/middleware/). Utilising the `getRuntime` provided in `@astrojs/cloudflare/runtime` method you can perform a lookup for KV. ```javascript // middleware/index.js import { getRuntime } from "@astrojs/cloudflare/runtime"; export async function onRequest(context, next) { const { env } = getRuntime(request); const tenantConfig = await env.ALTITUDE_KV_STORE.get("", { type: "json", }); context.locals.tenantConfig = tenantConfig; return next(); } ``` Ensure that you replace `` with the name of your KV listed in Altitude Platform. More details on creating a KV store using Altitude [here](https://docs.thgaltitude.com/edge/kv-store/). The following line will adds the KV store to the Astro Context object to make it available to read in any page. ```js context.locals.tenantConfig = tenantConfig; ``` Example use: ```js //page/index.astro --- const { tenantConfig } = Astro.locals --- { tenantConfig.exampleFeatureFlag &&

Feature Enabled

} ``` :::caution The runtime used for reading KV will only be accessible after deploying the website. Ensure you have a local JSON equivelant for local development in place of the runtime. ::: ## Framework Guides - [Astro: Edge KV](/frameworks/astro/#edge-kvs) ## Astro Integration Users of the integration will no longer have to bind KV's within their own middleware at runtime. The integration will handle the binding of KVs inside of entry middleware and attach the value(s) to the [altitude global context](/packages/astro-integration/#altitude-global-context). More information about the KV schema can be found [here](/packages/astro-integration/#kv). Multiple KV's can be provided for an application/site, the integration will iterate over each entry and attach the value to the corresponding namespace. Applications can rebind KVs to a preferred key if required within their middleware as shown below. ```js //middleware/index.js context.locals.tenantConfig = context.locals.altitude.runtime.kv. ``` For environments that are not running within a Cloudflare worker, a local copy of the KV should be supplied and will be used attaching it to the same `namespace` provided. ------------------------------------------- # File: astro-integration/guides/i18n.md --- title: Internationalisation description: Coming soon. sidebar: [object Object] --- # Internationalisation If you're looking to translate your website to multiple languages, there are a number of factors to consider. 1. **Language Dictionary** - The collection of language strings. Typically in JSON format. 2. **Routing by Language** - Path based rules that dictate which language dictionary and configuration to load. 3. **API Localisation** - A route to request translated data from the API. 4. **Domains** - How to associate a domian to a specific locale. 5. **Previewing Language Strings** - How to preview the object structure that represent a particular string on a website. Used to support non-technical stakeholders with understanding what keys to update. In this guide we will go through how to achieve each of these steps. ## Language Dictionaries ### Ingenuity Properties and Edge KV (Recommended) Using Ingenuity's Properties Service for managing your language dictionary is recommended as this allows non-technical users to manage the language used across the website without a deployment. Ingenuity Properties Service is a UI for managing your language dictionary. It's a list of keys and values. For the purpose of this guide we assume you have access to our Ingenuity Service's. If you do not, you can request access from your Ingenuity Project team. #### Adding Properties For Altitude deployed sites we syncronise the values in the Ingenuity Properties Service to a key-value store that lives with your deployed website. Typically referred to as an Edge KV. This allows for high-performance read operations and for the site to update language entries without requiring a deployment. We will sync any values from Properties Service that begin following the following format: `altitude.` #### Linking Properties Service to your KV :::note This is currently not yet available to all customers. Please contact the Altitude Platform team to enable this feature. ::: #### Reading Properties Once you have linked Properties Service to your KV you can begin to read the KV from your codebase. Refer to the framework guides below on how to read from a KV. If you're utilising our integrations there are helper methods available for i18n. Refer to the guides referenced below. ##### Framework Guides - [Astro: Reading from Edge KVs in Astro](/guides/edge-kv/) - [Astro Integration: Edge KV configuration](/packages/astro-integration/#kv) - [Astro Integration: i18n method](/packages/astro-integration/#i18n) ### Local Dictionary You can establish a local language dictionary in the codebase by creating local JSON files. For ease of access you can apply these in a middleware to make them accessible across your application. If you are using the Astro Integration, a local copy of your language dictionary can alternatively be supplied as part of the configuration setup for KV. This enables local environments to read directly from a local JSON file at runtime. [See more](/packages/astro-integration/#kv) ## Astro Integration Users of the integration have the ability to opt in to i18n to unlock localisation on their application. The integration is responsible for mapping to locale specific configs and validating a locale is supported when a request is made. To opt in to this behaviour additional config must be provided to the [configuration](/packages/astro-integration/#configuration) of a site. ### i18n.fallbackLocale **Type**: `String` \ **Required: True** The fallback locale if the locale of the request is invalid. ```javascript i18n: { fallbackLocale: "en-gb"; } ``` ### i18n.exclusionList **Type**: `Array[]` \ **Required: False** An array containing the prefix of the path that should not be localised and subject to localisation at runtime i.e proxies ```javascript i18n: { exclusionList: ["account", "images"]; } ``` ### i18n.localeCookie **Type**: `String` \ **Required: False** \ **Fallback**: `locale_V6` The name of the cookie that the locale will be stored in. This is also used to determine a users preferred locale from previous sessions and exposed on the [altitude global context](/packages/astro-integration/#altitude-global-context). If no value has been supplied, the integration will attempt to look for a cookie named `locale_V6`. If the integration is unable to resolve a value from the cookie or the locale is not supported, `Accept-Language` headers from the request will attempted to be used and exposed, otherwise the preferred locale will be `null`. ```javascript i18n: { localeCookie: "site_locale"; } ``` ### i18n.locales **Type**: `Array[]` \ **Required: True** An array of locale configs can be supplied. Below are the options that should be supplied per entry. ```javascript i18n: { locales: [ { // en-gb setup }, { // fr-fr setup }, ]; } ``` #### Locale Inheritance Locale configs are treated in a special way in the Astro integration and will inherit from the main config file e.g. this config ```javascript { domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ] } ], fallbackLocale: 'en-gb' } } ``` Will be treated like the below config by the integration. ```javascript { domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ], domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' } } ], fallbackLocale: 'en-gb' } } ``` Notice how the `domains` and `commerce` objects from the top level config are copied into the locale specific config. If a key is present in the locale specific it will **always** take precedence over the same key in the main config and thus act as an override. This inheritance and override behaviour is then applied to nested fields in the main and locale configs. However it does **not** apply to array elements, arrays are inherited or overidden in their entirety. If you want to explicitly opt out of inheriting a particular key from the main config then set that key or its parent as `null` in the locale config. ### i18n.locales.\.prefix **Type**: `String` \ **Required: True** The locale this config is associated with. ```javascript locales: [ { prefix: "en-gb", }, ]; ``` ### i18n.locales.\.domain **Type**: `String` \ **Required: False** The [global top level domain](/#localised-domains)(gTLDs) of this locale excluding protocol. The domain must be specified in the [variants](/packages/astro-integration/#domains-options) key of the `domains` object at the root of the config to ensure the integration can map to the sites config. :::note (Coming soon) The absence of this key will resolve to request to prepend the locale on the path of the `default` domain. ::: ```javascript locales: [ { prefix: "en-gb", domain: "www.example.com", }, { prefix: "fr-fr", domain: "www.example.fr", }, ]; ``` ### i18n.locales.\.kv Any locale specific KV keys to be retrieved from the cloudflare namespace. See config setup [here](/packages/astro-integration/#kv). If there are no locale specific KV settings then favour putting them in the top-level kv object instead. ### i18n.locales.\.commerce.endpoint Any locale specific api endpoint to be used. See config setup [here](/packages/astro-integration/#commerce) ### Custom Custom keys can also be supplied to the build config at locale level as well. [See more](/packages/astro-integration/#custom) ## Routing by Language :::note Guide coming soon ::: ## Localised Domains Global top level domains or gTLDs provide SEO benefits by providing localised content on a separate domain. The integration provides application owners the flexibility to configure whether valid locales should redirect to an associated gTLD. To enable localised domains, a locale should supply a `domain` within its respective locale config file. The integration will rewrite the underlying request to prefix the locale to the request which will resolve to the specific config file the locale and gTLD of the request is associated with. This domain should also be added to the `domains.variants` array within the sites config file. To mitigate the risk of duplicate content, any requests directly to the prefix that have not been subject to a rewrite will 404. Example request pattern: ```text www.example.com => rewrite => www.example.com/en-gb/ www.example.com/en-gb/ => www.example.com/en-gb/ (404) ``` ### Application setup As well as configuration updates, application owners must amend routing logic within their application. Routes within the application should be nested inside of a [dynamic route](https://docs.astro.build/en/guides/routing/) unless specified in the `exclusionList` as outlined in the above configuration. The param to use for this dynamic route **must be** `locale`. Further to this, to handle invalid locales as part of the request, an additional catch all page should be created at the root of the pages directory to force [on demand rendering](https://docs.astro.build/en/guides/server-side-rendering/#return-a-response-object). If your application already contains a catch all route, this should be moved inside of the dynamic `[locale]` route and the following snippet added to the new catch all route. ```javascript //pages/[...https].astro // new catch all page --- return new Response(null, Astro.response) --- ``` The 404.astro page should remain at the **root** of the pages directory to serve any custom 404 pages. ## API Localisation When switching the language of a site either through path based routing or gTLDs (Global top level domains) it might be desired that the copy and content/products on site also change if they are independently traded. The integration is able to seamlessly achieve API switching by enabling applications to configure custom configs per locale for sites, including defining different API endpoints. This value should be supplied in the `i18n.locales` locale specific config. ------------------------------------------- # File: platform/v2.1.0/index.mdx --- title: Altitude Platform v2.1.0 description: Documentation for Altitude Platform version 2.1.0 --- # Altitude Platform v2.1.0 Welcome to the documentation for Altitude Platform version 2.1.0. This version includes feature enhancements and API improvements compared to v2.0.0. ## What's New in v2.1.0 - **API Improvements**: Enhanced API with new endpoints and better performance - **Feature Enhancements**: Added new features based on user feedback - **Improved Documentation**: Expanded documentation with more examples - **Better Error Handling**: More descriptive error messages and improved error handling - **Security Enhancements**: Improved security features and fixed vulnerabilities ## Getting Started To get started with Altitude Platform v2.1.0, check out the following guides: - [Creating a Site](/docs/platform/v2.1.0/guides/create-a-site) - [Edge Functions](/docs/platform/v2.1.0/guides/edge-functions) - [Using the KV Store](/docs/platform/v2.1.0/guides/kv-store) - [Team Collaboration](/docs/platform/v2.1.0/guides/team-collaboration) ## Upgrading from v2.0.0 If you're upgrading from v2.0.0, please review the [migration guide](/docs/platform/v2.1.0/guides/migrating-from-v2.0) for important information about API changes and new features. ## Version History - **v2.1.0** (Current): Feature enhancements and API improvements - [v2.0.0](/docs/platform/v2.0.0): Major release with new features ------------------------------------------- # File: platform/v2.2.0/getting-started.md --- title: Getting Started description: Get started with Altitude Platform order: 1 --- # Getting Started with Altitude Platform Welcome to Altitude Platform! This guide will help you get started with our platform. ## Prerequisites Before you begin, make sure you have: - A GitHub account - Basic familiarity with Git - Node.js installed (version 14 or higher) ## Quick Start 1. **Sign up for Altitude Platform** - Go to [Altitude Platform](https://www.platform.thgaltitude.com) - Create an account or log in 2. **Connect to GitHub** - Connect your GitHub account - Select the repositories you want to use 3. **Create Your First Site** - Choose a starter kit or select an existing repository - Configure your site settings 4. **Deploy Your Site** - Select a branch to deploy - Choose an environment - Deploy your site 5. **Access Your Site** - Use the provided URL to access your deployed site - Share the URL with others ## Next Steps Now that you've deployed your first site, you can: - [Explore Edge Functions](./edge/functions) - [Learn about Environment Management](./walkthrough/sites) - [Discover Starter Kits](./kits/introduction) For more detailed information, check out our [documentation](/docs/platform). ------------------------------------------- # File: platform/v2.2.0/index.mdx --- title: Altitude Platform v2.2.0 description: Documentation for Altitude Platform version 2.2.0 --- # Altitude Platform v2.2.0 Welcome to the documentation for Altitude Platform version 2.2.0. This version includes performance improvements and bug fixes compared to v2.1.0. ## What's New in v2.2.0 - **Performance Optimizations**: Significant performance improvements for edge functions - **Enhanced KV Store**: Improved KV store with better caching and performance - **Bug Fixes**: Fixed several issues reported in v2.1.0 - **Improved Logging**: Enhanced logging capabilities with better filtering - **UI Improvements**: Redesigned dashboard with better usability ## Getting Started To get started with Altitude Platform v2.2.0, check out the following guides: - [Creating a Site](/docs/platform/v2.2.0/guides/create-a-site) - [Edge Functions](/docs/platform/v2.2.0/guides/edge-functions) - [Using the KV Store](/docs/platform/v2.2.0/guides/kv-store) - [Team Collaboration](/docs/platform/v2.2.0/guides/team-collaboration) ## Upgrading from Previous Versions If you're upgrading from a previous version, please review the migration guides: - [Upgrading from v2.1.0](/docs/platform/v2.2.0/guides/migrating-from-v2.1) - [Upgrading from v2.0.0](/docs/platform/v2.2.0/guides/migrating-from-v2.0) ## Version History - **v2.2.0** (Current): Performance improvements and bug fixes - [v2.1.0](/docs/platform/v2.1.0): Feature enhancements and API improvements - [v2.0.0](/docs/platform/v2.0.0): Major release with new features ------------------------------------------- # File: astro-integration/v1.7.6/index.md --- title: Astro Integration description: Astro Integration configuration reference guide. --- Middleware and utilities to make developing Altitude websites with Astro even better. - ๐ŸŒŽ **Internationalisation:** Support for internationalisation. - ๐Ÿ”ฅ **Performance Patterns:** Edge performance optimisations out of the box. Available Soon: - ๐Ÿ”Œ **Easy access to APIs:** Easier access our commerce APIs from your codebase using ready made helpers and proxies. --- # Installation ```sh npm i @thg-altitude/astro-integration ``` # Configuration The below reference covers all of the different configuration options for the Astro Integration providing further flexibility for your application. ```js //config/site.js export default { // configuration options here... } ``` ## Domains options ### domains.default **Type**: `String` \ **Required: True** The default domain of a site excluding protocol. This value is used in conjuction with i18n, see [internationalisation](/docs/astro-integration/guides/i18n) for further information on this value. ### domains.variants **Type:** `Array[]` \ **Required: True** Contains all additional domains associated with a site inclusive of the default. This is the value the integration uses to map to this config. The header `x-altitude-instance` can be used to switch between different configs for local development, see the [multi tenancy](/docs/astro-integration/guides/multitenancy) guide for further information on how this works. ```javascript domains: { default: "www.example.com", variants: ["wwww.example.com", "uat.www.example.com"] } ``` ## Commerce ### commerce.endpoint **Type:** `String` \ **Required: False** The commerce api endpoint the specified site uses. This value will be used with the [commerce api](#commerce-api) method and must be provided if your application intends to use this method. ```javascript commerce: { endpoint: 'https://horizon-api.www.example.com/graphql' } ``` ### commerce.headers **Type:** `Object` \ **Required: False** This block allows headers to be added to a request to the [commerce endpoint](#altitude-commerce-endpoint) on the server side. The name of the header is the key, and the value an object with a type key of "env"|"request" to specify if the value should be taken from environment variables, or from another request header. This is useful for sensitive headers that should not be accessible in the browser, and retaining the value of a header that may get overwritten or masked as it passes through proxies, e.g. client_ip. ```javascript commerce: { endpoint: "https://horizon-api.www.example.com/graphql";, headers: { 'x-example-secret-header': { "type": "env", "variable": "SECRET_HEADER_NAME" } }, 'x-example-new-header': { "type": "request", "variable": "old-header-name" } } ``` This would equate to: ``` x-example-secret-header: import.meta.env.SECRET_HEADER_NAME x-example-new-header : request.headers.get('old-header-name') ``` ## KV **Type:** `Array[]` \ **Required: True** An array of KV options can be supplied. Below are the options that should be supplied per entry. See the [Edge KV](/docs/astro-integration/guides/edge-kv) guide for more details. This field is required even if all locale specific configs have their own KV entries. So use this section either for a sensible set of default values or leave it as an empty array if you are sure that all locales have correct KV configs. ```javascript kv: [ { // kv option entry one goes here }, { // kv option entry two goes here }, ] ``` ### \.key **Type:** `String` \ **Required: True** The key to be retrieved in your Cloudflare KV store. ### \.namespace **Type:** `String` \ **Required: True** This value will be used to attach the contents of the KV key to a specified namespace on the altitude global context e.g. `altitude.runtime.kv.`. More details on the altitude namespace can be found [here](#altitude-global-context) ### \.local **Type:** `Any` \ **Required: True** Used for local development. This will be the value that is resolved when the application is not ran inside of a worker. The value can imported in or defined directly within this key. ```javascript import fooBar from '../local/config' kv: [ { key: 'standalone', namespace: 'config', local: { // local file import variable could also be used foo: 'bar', }, }, ] ``` ## Custom Custom keys can also be supplied to the build config, such as environment variables. These values will not affect the configuration of the integration but will be provided on the [altitude global context](#altitude-global-context) at runtime. This is useful for multi tenancy when values need to change based on each tenants config. Further information can be found in the [multi tenancy guide](/docs/astro-integration/guides/multitenancy) ## Invoking the integration The build config and `altitudeMiddleware` function should be imported and passed as an argument to the integration as shown below. ```js //astro.config.js import { altitudeMiddleware } from '@thg-altitude/astro-integration' import buildConfig from './config/site' export default defineConfig({ integrations: [ altitudeMiddleware( buildConfig ), ], // ...other Astro config setup }) ``` ## Methods The integration provides some useful common functions and patterns available to be used in applications out the box. Methods are attached to the `altitude` namespace and provides storefront owners a layer of abstraction away from verbose core commerce functionality and performance uplifts. ### Quick method reference ```javascript altitude: { i18n: (func, ...args) => String; commerce: { api: async ( operationFields: { operation: String!, variables: Object! }, headers: Object!, options: { apqEnabled: Boolean } ) => Object; }; blog: { api: async ( endpoint: String!, operationFields: { operation: String!, variables: Object, cacheKey: Request! || String!, wafBypass: String!, clientSecret: String!, clientId: String!}, options: { headers: Object } ) => Response; }; cache: { get: async(cacheKey: String! || Request!, operationName: String) => Response || null; set: async(cacheKey: String! || Request!, response: Response!, options: { expiry: Number }) => void; } } ``` ### i18n The i18n method aids with localising copy on site. The function provides flexibility to resolve langauge strings to their values or the object structure to support non-technical stakeholders understand what keys to update. #### valueFunc **Type:** `function() => String` \ **Required: True** An anonymous function that when invoked returns the string that is intended to be evaluated. #### args **Type:** `String` \ **Required: False** Optional argument that will be used to dynamically replace string placeholders in the value of the language string passed, or replace dynamic keys using bracket notation. **Example Use** ```javascript --- const { altitude: { i18n } } = Astro.locals const lang = Astro.locals.altitude.rutime.kv.lang const productTitle = "Vivienne Westwood Logo Ribbed Wool Beanie" const contentKey = "details" ---

{i18n(() => lang.product.promotionalOffer, productTitle)}

// Buy one Vivienne Westwood Logo Ribbed Wool Beanie get one free

{i18n(() => lang.product[contentKey], contentKey)}

//lang.product.details will be the string that is now evaluated ``` **Exposing Keys** The keys used for copy on site can be exposed using headers. This will allow the relevant teams to identify the entry on a site and update its value in Content UI on the fly. To expose the keys an additional request header should be added `Properties-Preview: SHOW-KEYS` ### Commerce API The integration provides out the box commerce api fetching on the server exposing the method as `altitude.commerce.api`. The method enables applications to configure the operation, variables and headers to retrieve commerce data for a given site or tenant. The endpoint the method will use for these calls will be the `commerce.endpoint` supplied in an application or tenants [build config](#configuration). #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: True** The variables to be passed as part of the api call. If no variables are required an empty object should be passed. #### headers **Type:** `Object` \ **Required: True** All required headers to be passed as part of the commerce fetch. No headers are defaulted so all should be provided. #### options.apqEnabled **Type:** `Boolean` \ **Required: False** \ **Default: False** This enables Horizons [Automatic Persisted Queries](https://thehutgroup.github.io/Horizon-Public-Docs/#automated-persisted-queries) feature. #### Return value From version 1.7.0 the api method returns the full response object. Version <=1.6.X The commerce api method will return an Object containing three values: `body`, `duration`, `status`. - `body`: The response from the api call. - `duration`: The duration of the api call in ms. - `status`: The status code of the response. **Example use** ```javascript //c/index.astro const body = await locals.query({ operation: Schema, variables: { handle: pathName, }, customHeaders: { foo: 'bar', }, }) ``` ```javascript //middleware/index.js import { print } from 'graphql' query: async (args) => { const { operation, variables = {}, customHeaders = {} } = args let query if (typeof operation == 'string') { query = operation } else { query = print(operation) } try { const { body, duration, status } = await locals.altitude.commerce.api( { operation: query, variables }, { ...customHeaders, 'Content-Type': 'application/json', 'User-Agent': request.headers.get('User-Agent'), 'X-Altitude-Instance': locals.tenantInstance, // application specific header }, { apqEnabled: false, } ) return body } catch (e) { console.log(e) } } ``` ### Cache API The cache api can be used to enhance the performance of sites by reducing the number of network calls being made as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer. Instead, the response of these calls can be set in cache so future requests can attempt to retrieve the response from cache instead of calling the origin. #### Get ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get a response from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### operationName **Type:** `String` \ **Required: False** \ **Default:** `""` The get cache api function logs out the operation name that has receieved a cache hit for observability. **Example Use** ```javascript let response, cacheKey if (!import.meta.env.DEV) { cacheKey = altitude.createCacheKey(`${horizonEndpoint}/${host}/headerfooter`) response = await altitude.cache.get(cacheKey, 'nav') } ``` #### Set ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to set a response in cache for request lookups. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### response **Type:** `Response` \ **Required: True** The response object to be put into cache to be retrieved for future cache lookups. ##### options.expiry **Type:** `Number` \ **Required: False** \ **Default:** `600` Optional value for how long this response should stay in the cache for in seconds. Defaulted to 600 seconds (10 minutes) **Example Use** ```javascript if (!response) { try { response = await Astro.locals.utils.query({ operation: HeaderFooter, }) if (response.statusText !== 'OK') throw new Error('Error Fetching nav') if (!import.meta.env.DEV) { await altitude.cache.set(cacheKey, response.clone(), { expiry: 600 }) } } catch (e) { console.log(e.message) } } ``` Performance can be improved by using the cache as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer of pages. For example, by using the functions described above to first check the cache, and if its empty, to populate the cache once the data has been fetched: ### Blog API The blog api function is used to fetch blog content from a specified `endpoint`. The fetch utilises the Cache API to get and set auth tokens which are sent as a header to reduce the amount of calls to auth service. #### endpoint **Type:** `String` \ **Required: True** The endpoint the integration should use to retrieve blog data. #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: False** The variables to be passed as part of the api call. #### operationFields.cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get and set auth tokens from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` #### operationFields.wafBypass **Type:** `String` \ **Required: True** Application specific WAF bypass key, used for auth. #### operationFields.clientSecret **Type:** `String` \ **Required: True** Application specific client secret, used for auth. #### operationFields.clientId **Type:** `String` \ **Required: True** Tenant or Application specific ID, used for auth. #### options.headers **Type:** `Object` \ **Required: False** Any additional headers to be sent as part of the request. `Content-Type: application/json` and `Authorization` are currently defaulted. **Example Use** ```javascript const blogEndpoint = Astro.locals?.tenantConfig?.application?.features?.tesseract?.endpoint let resp try { resp = await altitude.blog.api(blogEndpoint, { operation: TesseractHome, cacheKey: altitude.createCacheKey( `${Astro.locals.tenantConfig.application.horizonEndpoint}/${Astro.locals.host}/blog` ), wafBypass: import.meta.env.WAF_BYPASS, clientSecret: import.meta.env.AUTH_CLIENT_SECRET, clientId:import.meta.env.BLOG_CLIENT_ID }) } catch (e) { console.log(e) } ``` ## Altitude Commerce Endpoint Enabling the commerce endpoint creates a new endpoint `/api/commerce` that allows client side graphql calls to be proxied through the server to your specified endpoint in your build configs `commerce.endpoint`. The option to enable this is done at the point of invoking the altitudeMiddleware. The key benefit of this approach is reducing the size of client-size imports, through no longer needing to import the query in the client-side script. The Astro route injection uses pattern matching to direct requests to the endpoint, `/api/commerce/`. It then looks up the query value in the GraphQL object map, using the operationName search parameter value. ### Configuring the endpoint Firstly, there is a requirement to add `api` to the tenant build config `exclusionList` to avoid localisation rewrites, which would result in the route 404ing. More information on this can be found in the [documentation](/docs/astro-integration/guides/i18n/#i18nexclusionlist) ```js // tenant config obj { exclusionList: ['api'] } ``` The endpoint in enabled by an argument passed to the altitudeMiddleware function. This object needs two keys: `enabled` and `graphql`. The `enabled` key determines if the route to the endpoint should exist, and `graphql` requires an object containing Key/Value pairs of the query name, and the raw query as the value. This object does impact the build size, so it is important to only pass in queries that will be used client-side. If the \_worker.js file size becomes too large, the deployment will fail. ```js //astro.config.js import { altitudeMiddleware } from '@thg-altitude/astro-integration' import buildConfig from './config/site' const graphqlQueriesObj = // your chosen import method export default defineConfig({ integrations: [ altitudeMiddleware( buildConfig, {}, {"enabled": true, "graphql" : graphqlQueriesObj} ), ], // ...other Astro config setup }) ``` ### Sending a request to the endpoint Currently this endpoint uses 4 values in the body: - variables (optional, used for providing data in GraphQL mutations) - horizonApq (optional, defaults to false) - application (Multi-Tenanted Account only - 'account') - opaqueCookieDomain Application currently only applies to the use of the Multi-Tenanted Account option, with a value of 'account'.opaqueCookieDomain allows access the correct domains when setting response headers. Variables handles any input needed by the Horizon query. The horizonApq setting allows enabling [persisted queries](#commerce-api) ``` const variables {...} const url = /api/commerce?operation=ExampleOperation const data = await fetch(url, { method: 'POST', headers: {...}, body: JSON.stringify({ variables: variables, horizonApq: false, application: 'storefront', opaqueCookieDomain }) }) ``` ## Altitude Global Context The integration will provide additional information about the config resolvement at runtime and attach it to `context.locals.altitude`. Please see all available attachments below. ### altitude.runtime.config The build config object the integration has resolved to. For applications using the integration's [localisation](/docs/astro-integration/guides/i18n) solution this will be the locale specific config. ### altitude.runtime.kv.\ The value of [KV](#kv) retrieved using the key provided. This will be attached using the namespace value provided in the KV for the key retrieved.

Internationalisation

These keys will be provided on the altitude namespace for applications that are using the built in i18n solution. Further information can be found [here](/docs/astro-integration/guides/i18n) ### altitude.locale The locale the integration has resolved to from the request. - `en-gb` ### altitude.availableLocales Array containing all the locales a sites config supports. - `['en-gb', 'fr-fr']` ### altitude.localeDomains An object containing the ISO 639-1 code and domain path it corresponds to. - `{'en-gb': 'https://www.example.com', 'fr-fr': 'https://www.example.fr'}` ### altitude.preferredLocale ISO 639-1 code that resolves using the highest weighted valid `Accept-Language` header or existing cookie specified in the i18n section of the build config. If none are provided or invalid, `null` will be returned. ## Resources - [NPM Package](https://www.npmjs.com/package/@thg-altitude/astro-integration) ------------------------------------------- # File: platform/v2.3.0/getting-started.md --- title: Getting Started description: Get started with Altitude Platform order: 1 --- # Getting Started with Altitude Platform Welcome to Altitude Platform! This guide will help you get started with our platform. ## Prerequisites Before you begin, make sure you have: - A GitHub account - Basic familiarity with Git - Node.js installed (version 14 or higher) ## Quick Start 1. **Sign up for Altitude Platform** - Go to [Altitude Platform](https://www.platform.thgaltitude.com) - Create an account or log in 2. **Connect to GitHub** - Connect your GitHub account - Select the repositories you want to use 3. **Create Your First Site** - Choose a starter kit or select an existing repository - Configure your site settings 4. **Deploy Your Site** - Select a branch to deploy - Choose an environment - Deploy your site 5. **Access Your Site** - Use the provided URL to access your deployed site - Share the URL with others ## Next Steps Now that you've deployed your first site, you can: - [Explore Edge Functions](./edge/functions) - [Learn about Environment Management](./walkthrough/sites) - [Discover Starter Kits](./kits/introduction) For more detailed information, check out our [documentation](/docs/platform). ------------------------------------------- # File: platform/v2.3.0/index.mdx --- title: Altitude Platform v2.3.0 description: Documentation for Altitude Platform version 2.3.0 --- # Altitude Platform v2.3.0 Welcome to the documentation for Altitude Platform version 2.3.0. This version includes significant improvements and new features compared to previous versions. ## What's New in v2.3.0 - **Enhanced Edge Functions**: Further improvements to edge function performance and capabilities - **Advanced KV Store**: Extended KV store functionality with TTL support and atomic operations - **Real-time Monitoring**: Live monitoring dashboard with real-time metrics - **Multi-team Collaboration**: Enhanced team management with role-based access control - **Advanced CI/CD Integration**: Improved CI/CD pipeline integration with detailed deployment reports ## Getting Started To get started with Altitude Platform v2.3.0, check out the following guides: - [Creating a Site](/docs/platform/v2.3.0/guides/create-a-site) - [Advanced Edge Functions](/docs/platform/v2.3.0/guides/advanced-edge-functions) - [Using the KV Store](/docs/platform/v2.3.0/guides/kv-store) - [Team Collaboration](/docs/platform/v2.3.0/guides/team-collaboration) ## Upgrading from Previous Versions If you're upgrading from a previous version, please review the migration guides: - [Upgrading from v2.2.0](/docs/platform/v2.3.0/guides/migrating-from-v2.2) - [Upgrading from v2.1.0](/docs/platform/v2.3.0/guides/migrating-from-v2.1) - [Upgrading from v2.0.0](/docs/platform/v2.3.0/guides/migrating-from-v2.0) ## Version History - **v2.3.0** (Current): Enhanced monitoring and collaboration features - [v2.2.0](/docs/platform/v2.2.0): Performance improvements and bug fixes - [v2.1.0](/docs/platform/v2.1.0): Feature enhancements and API improvements - [v2.0.0](/docs/platform/v2.0.0): Major release with new features ------------------------------------------- # File: platform/v2.4.0/getting-started.md --- title: Getting Started description: Get started with Altitude Platform order: 1 --- # Getting Started with Altitude Platform Welcome to Altitude Platform! This guide will help you get started with our platform. ## Prerequisites Before you begin, make sure you have: - A GitHub account - Basic familiarity with Git - Node.js installed (version 14 or higher) ## Quick Start 1. **Sign up for Altitude Platform** - Go to [Altitude Platform](https://www.platform.thgaltitude.com) - Create an account or log in 2. **Connect to GitHub** - Connect your GitHub account - Select the repositories you want to use 3. **Create Your First Site** - Choose a starter kit or select an existing repository - Configure your site settings 4. **Deploy Your Site** - Select a branch to deploy - Choose an environment - Deploy your site 5. **Access Your Site** - Use the provided URL to access your deployed site - Share the URL with others ## Next Steps Now that you've deployed your first site, you can: - [Explore Edge Functions](./edge/functions) - [Learn about Environment Management](./walkthrough/sites) - [Discover Starter Kits](./kits/introduction) For more detailed information, check out our [documentation](/docs/platform). ------------------------------------------- # File: platform/v2.4.0/index.mdx --- title: Altitude Platform v2.4.0 description: Documentation for Altitude Platform version 2.4.0 --- # Altitude Platform v2.4.0 Welcome to the documentation for Altitude Platform version 2.4.0. This version includes significant improvements and new features compared to previous versions. ## What's New in v2.4.0 - **Health Checks**: Configure health check endpoints for your Cloudflare Worker functions to enable automatic failover to GKE/workerd when workers become unhealthy - **Enhanced Edge Functions**: Further improvements to edge function performance and capabilities - **Advanced KV Store**: Extended KV store functionality with TTL support and atomic operations - **Real-time Monitoring**: Live monitoring dashboard with real-time metrics - **Multi-team Collaboration**: Enhanced team management with role-based access control - **Advanced CI/CD Integration**: Improved CI/CD pipeline integration with detailed deployment reports ## Getting Started To get started with Altitude Platform v2.4.0, check out the following guides: - [Creating a Site](/docs/platform/v2.4.0/guides/create-a-site) - [Health Checks](/docs/platform/v2.4.0/edge/health-checks) - [Edge Functions](/docs/platform/v2.4.0/edge/functions) - [Using the KV Store](/docs/platform/v2.4.0/edge/kv-store) ## Upgrading from Previous Versions If you're upgrading from v2.3.0, please review the migration guide: - [Upgrading from v2.3.0](/docs/platform/v2.4.0/guides/migrating-from-v2.3) ## Version History - **v2.4.0** (Current): Health checks and automatic failover support - [v2.3.0](/docs/platform/v2.3.0): Enhanced monitoring and collaboration features - [v2.2.0](/docs/platform/v2.2.0): Performance improvements and bug fixes - [v2.1.0](/docs/platform/v2.1.0): Feature enhancements and API improvements ------------------------------------------- # File: astro-integration/v1.7.6/guides/edge-kv.md --- title: Edge KV description: Configuring Cloudflare edge kv. --- Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. Edge Key-Value stores (KVs) enable fast, scalable, distributed data storage at network edges, reducing latency for global applications. To configure your Altitude KV store with Astro create a [middleware](https://docs.astro.build/en/guides/middleware/). Utilising the `getRuntime` provided in `@astrojs/cloudflare/runtime` method you can perform a lookup for KV. ```javascript // middleware/index.js import { getRuntime } from "@astrojs/cloudflare/runtime"; export async function onRequest(context, next) { const { env } = getRuntime(request); const tenantConfig = await env.ALTITUDE_KV_STORE.get("", { type: "json", }); context.locals.tenantConfig = tenantConfig; return next(); } ``` Ensure that you replace `` with the name of your KV listed in Altitude Platform. More details on creating a KV store using Altitude [here](https://docs.thgaltitude.com/edge/kv-store/). The following line will adds the KV store to the Astro Context object to make it available to read in any page. ```js context.locals.tenantConfig = tenantConfig; ``` Example use: ```js //page/index.astro --- const { tenantConfig } = Astro.locals --- { tenantConfig.exampleFeatureFlag &&

Feature Enabled

} ``` :::caution The runtime used for reading KV will only be accessible after deploying the website. Ensure you have a local JSON equivelant for local development in place of the runtime. ::: ## Framework Guides - [Astro: Edge KV](/frameworks/astro/#edge-kvs) ## Astro Integration Users of the integration will no longer have to bind KV's within their own middleware at runtime. The integration will handle the binding of KVs inside of entry middleware and attach the value(s) to the [altitude global context](/packages/astro-integration/#altitude-global-context). More information about the KV schema can be found [here](/packages/astro-integration/#kv). Multiple KV's can be provided for an application/site, the integration will iterate over each entry and attach the value to the corresponding namespace. Applications can rebind KVs to a preferred key if required within their middleware as shown below. ```js //middleware/index.js context.locals.tenantConfig = context.locals.altitude.runtime.kv. ``` For environments that are not running within a Cloudflare worker, a local copy of the KV should be supplied and will be used attaching it to the same `namespace` provided. ------------------------------------------- # File: astro-integration/guides/multitenancy.md --- title: Multi-Tenancy description: Serve multiple brands from a single Altitude site. --- Multi-tenancy is a pattern in which multiple brands may be served from the same codebase. It can be used to quickly onboard a new brand, or 'tenant', via a new configuration, as a fast route to scale. --- # Multi-tenancy ## Custom Domains Altitude allows multiple [custom domains](https://docs.thgaltitude.com/edge/domains/) to be attached to a single site, giving the feel of a separately hosted site per-tenant. For example `www.allsole.com` and `www.biossance.com` point to the same Altitude deployment. ## Tenant Configuration There are certain properties which will want to vary between tenants to give then a distinct feel, such as: - Styling - Feature flags - API urls, reCaptcha keys etc. A solution to achieving this is for your application to read these properties from a configuration object which can vary per-tenant. ## Key-Value Stores Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. If your application is using the [Astro integration package](/packages/astro-integration/) the KV binding will occur at runtime from within the integration otherwise refer to your appropriate framework guide on how to set this up. #### Framework Guides - [Astro: Edge KV](/frameworks/astro/#edge-kvs) ## Determining the Tenant In order to read the correct tenant config in middleware, the tenant for the incoming request first needs to be determined. ### Production Environments A mapping between custom domain and tenant config key can be used to determine the correct key per request. Production environments can use `X-Forwarded-Host` which will be provided as a request header from Fastly. ### Local and UAT Environments For cases where custom domains are not attached, such as `localhost` and on deployments to other Altitude development environments, a custom HTTP header may be used to specify tenant per request. A browser extension such as [ModHeader](https://modheader.com/) is convenient for fast tenant switching in this case. ## Multi-tenancy Stylesheets When sharing a codebase with multiple tenants, being able to still differentiate branding colours and typography is essential. However it's equally essential to ensure that the codebase is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and you're not building stylesheets when not necessary. ### Tailwind By utilising [Tailwind Themes](https://tailwindcss.com/docs/theme) alongside a combination of data attributes and CSS variables you can achieve a pattern of having one shared UI but with tenant specific CSS. Example: ```css //main.css @tailwind base; @tailwind components; @tailwind utilities; @layer base { html[data-theme="default"] { --color-esther: 34, 39, 46; --color-maximus: 45, 51, 59; --color-linx: 55, 62, 71; } html[data-theme="neon"] { --color-esther: 20, 61, 42; --color-maximus: 13, 82, 66; --color-linx: 20, 82, 11; } } ``` Within your HTML you then switch to the relevant theme: ```html ``` ##### Alternative: DaisyUI + Tailwind DaisyUI offers a multi-theme configuration out of the box. This can help achieve the capability of creating multiple themes which can be used for creating variety across tenants. [DaisyUI Themes](https://daisyui.com/docs/themes/) [DaisyUI components](https://daisyui.com/components/) :::note Tailwind is a build time dependency and is unaware at the build stage what tenant is active. Therefore you need to consider that the impact of not optimising your theme file(s) efficiently so that minimal CSS changes are required will result in all tenants CSS weight increasing. Using this alongside a speific tenant CSS file that is loaded at runtime (for minor layout changes / adjustments) will help the CSS scale to a larger volume of sites without concerns of shared performance regressions. ::: ### Astro Integration Applications using the Astro Integration can leverage built in mapping to resolve configs to the correct tenant. This is determined at runtime and domains (x-altitude-instance for local development or the domain in prod) will be mapped by matching a value from within a tenants [`domains.variants`](/packages/astro-integration/#domains-options) array. Applications that have a multi tenancy model, will supply an array of configs to the integration function [`altitudeMiddleware`](/packages/astro-integration/#invoke-the-integration). It is recommended that each tenants config is in their own respective file and exported from a single file to keep the configs tidy, sectioned and consistent. ```javascript // /config/index.js import siteOne from "./siteOne"; import siteTwo from "./siteTwo"; import siteThree from "./siteThree"; export default [siteOne, siteTwo, siteThree]; ``` #### Multi Tenancy Config As well as defining tenant specific endpoints and KV keys, the config file allows application owners to extend any site specific values and expose these at runtime. One benefit that the integration allows is the ability to unlock tenant specific environment variables. Tenant specific secrets can be supplied to the build config. The integration exports an `env` function which can be imported and invoked inside of individual build configs. The `env` function takes in the **reference** of the environment variable as an argument not the value. ```js //config/siteOne.js import { env } from '@thg-altitude/astro-integration' export default { domains: { default: "www.example.com", variants: ["www.example.com"], }, ... // exsiting site setup blog: { secret: env('EXAMPLE_SITE_BLOG_SECRET') } }; ``` The value of this environment variable can then be accessed by using the [altitude global context](/packages/astro-integration/#altitude-global-context) `altitude.runtime.config`. ```javascript --- const { altitude, runtime } = Astro.locals // runtime(Production) vs vite development server(Local development) const blogSecret = runtime ? runtime.env[altitude.runtime.config.blog.secret] : import.meta.env[altitude.runtime.config.blog.secret] --- ``` ### Tenant Switching When developing in a multi tenanted application, it may be desired that you are able to switch between tenants on the fly. For local development and DEV/UAT cloudflare environments this is easily achieved by adding a custom HTTP header to the request. The header to be used must be `x-altitude-instance` and the value will determine which tenant to switch to. This value should exist in that tenants config file, listed under the [`domains.variants`](/packages/astro-integration/#domains-options) array. Tenant switching will not be enabled on live domains. Applications that are using [localised domains](/guides/i18n/#localised-domains) will use the domain associated to that locale. ------------------------------------------- # File: astro-integration/v1.7.6/guides/i18n.md --- title: Internationalisation sidebar: [object Object] --- # Internationalisation If you're looking to translate your website to multiple languages, there are a number of factors to consider. 1. **Language Dictionary** - The collection of language strings. Typically in JSON format. 2. **Routing by Language** - Path based rules that dictate which language dictionary and configuration to load. 3. **API Localisation** - A route to request translated data from the API. 4. **Domains** - How to associate a domian to a specific locale. 5. **Previewing Language Strings** - How to preview the object structure that represent a particular string on a website. Used to support non-technical stakeholders with understanding what keys to update. In this guide we will go through how to achieve each of these steps. ## Language Dictionaries ### Ingenuity Properties and Edge KV (Recommended) Using Ingenuity's Properties Service for managing your language dictionary is recommended as this allows non-technical users to manage the language used across the website without a deployment. Ingenuity Properties Service is a UI for managing your language dictionary. It's a list of keys and values. For the purpose of this guide we assume you have access to our Ingenuity Service's. If you do not, you can request access from your Ingenuity Project team. #### Adding Properties For Altitude deployed sites we syncronise the values in the Ingenuity Properties Service to a key-value store that lives with your deployed website. Typically referred to as an Edge KV. This allows for high-performance read operations and for the site to update language entries without requiring a deployment. We will sync any values from Properties Service that begin following the following format: `altitude.` #### Linking Properties Service to your KV :::note This is currently not yet available to all customers. Please contact the Altitude Platform team to enable this feature. ::: #### Reading Properties Once you have linked Properties Service to your KV you can begin to read the KV from your codebase. Refer to the framework guides below on how to read from a KV. If you're utilising our integrations there are helper methods available for i18n. Refer to the guides referenced below. ##### Framework Guides - [Astro: Reading from Edge KVs in Astro](/guides/edge-kv/) - [Astro Integration: Edge KV configuration](/packages/astro-integration/#kv) - [Astro Integration: i18n method](/packages/astro-integration/#i18n) ### Local Dictionary You can establish a local language dictionary in the codebase by creating local JSON files. For ease of access you can apply these in a middleware to make them accessible across your application. If you are using the Astro Integration, a local copy of your language dictionary can alternatively be supplied as part of the configuration setup for KV. This enables local environments to read directly from a local JSON file at runtime. [See more](/packages/astro-integration/#kv) ## Astro Integration Users of the integration have the ability to opt in to i18n to unlock localisation on their application. The integration is responsible for mapping to locale specific configs and validating a locale is supported when a request is made. To opt in to this behaviour additional config must be provided to the [configuration](/packages/astro-integration/#configuration) of a site. ### i18n.fallbackLocale **Type**: `String` \ **Required: True** The fallback locale if the locale of the request is invalid. ```javascript i18n: { fallbackLocale: "en-gb"; } ``` ### i18n.exclusionList **Type**: `Array[]` \ **Required: False** An array containing the prefix of the path that should not be localised and subject to localisation at runtime i.e proxies ```javascript i18n: { exclusionList: ["account", "images"]; } ``` ### i18n.localeCookie **Type**: `String` \ **Required: False** \ **Fallback**: `locale_V6` The name of the cookie that the locale will be stored in. This is also used to determine a users preferred locale from previous sessions and exposed on the [altitude global context](/packages/astro-integration/#altitude-global-context). If no value has been supplied, the integration will attempt to look for a cookie named `locale_V6`. If the integration is unable to resolve a value from the cookie or the locale is not supported, `Accept-Language` headers from the request will attempted to be used and exposed, otherwise the preferred locale will be `null`. ```javascript i18n: { localeCookie: "site_locale"; } ``` ### i18n.locales **Type**: `Array[]` \ **Required: True** An array of locale configs can be supplied. Below are the options that should be supplied per entry. ```javascript i18n: { locales: [ { // en-gb setup }, { // fr-fr setup }, ]; } ``` #### Locale Inheritance Locale configs are treated in a special way in the Astro integration and will inherit from the main config file e.g. this config ```javascript { domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ] } ], fallbackLocale: 'en-gb' } } ``` Will be treated like the below config by the integration. ```javascript { domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ], domains: { default: 'www.acme.com', variants: ['www.acme.com'] }, commerce: { endpoint: 'https://www.acme.com' } } ], fallbackLocale: 'en-gb' } } ``` Notice how the `domains` and `commerce` objects from the top level config are copied into the locale specific config. If a key is present in the locale specific it will **always** take precedence over the same key in the main config and thus act as an override. This inheritance and override behaviour is then applied to nested fields in the main and locale configs. However it does **not** apply to array elements, arrays are inherited or overidden in their entirety. If you want to explicitly opt out of inheriting a particular key from the main config then set that key or its parent as `null` in the locale config. ### i18n.locales.\.prefix **Type**: `String` \ **Required: True** The locale this config is associated with. ```javascript locales: [ { prefix: "en-gb", }, ]; ``` ### i18n.locales.\.domain **Type**: `String` \ **Required: False** The [global top level domain](/#localised-domains)(gTLDs) of this locale excluding protocol. The domain must be specified in the [variants](/packages/astro-integration/#domains-options) key of the `domains` object at the root of the config to ensure the integration can map to the sites config. :::note (Coming soon) The absence of this key will resolve to request to prepend the locale on the path of the `default` domain. ::: ```javascript locales: [ { prefix: "en-gb", domain: "www.example.com", }, { prefix: "fr-fr", domain: "www.example.fr", }, ]; ``` ### i18n.locales.\.kv Any locale specific KV keys to be retrieved from the cloudflare namespace. See config setup [here](/packages/astro-integration/#kv). If there are no locale specific KV settings then favour putting them in the top-level kv object instead. ### i18n.locales.\.commerce.endpoint Any locale specific api endpoint to be used. See config setup [here](/packages/astro-integration/#commerce) ### Custom Custom keys can also be supplied to the build config at locale level as well. [See more](/packages/astro-integration/#custom) ## Routing by Language :::note Guide coming soon ::: ## Localised Domains Global top level domains or gTLDs provide SEO benefits by providing localised content on a separate domain. The integration provides application owners the flexibility to configure whether valid locales should redirect to an associated gTLD. To enable localised domains, a locale should supply a `domain` within its respective locale config file. The integration will rewrite the underlying request to prefix the locale to the request which will resolve to the specific config file the locale and gTLD of the request is associated with. This domain should also be added to the `domains.variants` array within the sites config file. To mitigate the risk of duplicate content, any requests directly to the prefix that have not been subject to a rewrite will 404. Example request pattern: ```text www.example.com => rewrite => www.example.com/en-gb/ www.example.com/en-gb/ => www.example.com/en-gb/ (404) ``` ### Application setup As well as configuration updates, application owners must amend routing logic within their application. Routes within the application should be nested inside of a [dynamic route](https://docs.astro.build/en/guides/routing/) unless specified in the `exclusionList` as outlined in the above configuration. The param to use for this dynamic route **must be** `locale`. Further to this, to handle invalid locales as part of the request, an additional catch all page should be created at the root of the pages directory to force [on demand rendering](https://docs.astro.build/en/guides/server-side-rendering/#return-a-response-object). If your application already contains a catch all route, this should be moved inside of the dynamic `[locale]` route and the following snippet added to the new catch all route. ```javascript //pages/[...https].astro // new catch all page --- return new Response(null, Astro.response) --- ``` The 404.astro page should remain at the **root** of the pages directory to serve any custom 404 pages. ## API Localisation When switching the language of a site either through path based routing or gTLDs (Global top level domains) it might be desired that the copy and content/products on site also change if they are independently traded. The integration is able to seamlessly achieve API switching by enabling applications to configure custom configs per locale for sites, including defining different API endpoints. This value should be supplied in the `i18n.locales` locale specific config. ------------------------------------------- # File: astro-integration/v1.7.6/guides/multitenancy.md --- title: Multi-Tenancy description: Serve multiple brands from a single Altitude site. --- Multi-tenancy is a pattern in which multiple brands may be served from the same codebase. It can be used to quickly onboard a new brand, or 'tenant', via a new configuration, as a fast route to scale. --- # Multi-tenancy ## Custom Domains Altitude allows multiple [custom domains](https://docs.thgaltitude.com/edge/domains/) to be attached to a single site, giving the feel of a separately hosted site per-tenant. For example `www.allsole.com` and `www.biossance.com` point to the same Altitude deployment. ## Tenant Configuration There are certain properties which will want to vary between tenants to give then a distinct feel, such as: - Styling - Feature flags - API urls, reCaptcha keys etc. A solution to achieving this is for your application to read these properties from a configuration object which can vary per-tenant. ## Key-Value Stores Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. If your application is using the [Astro integration package](/packages/astro-integration/) the KV binding will occur at runtime from within the integration otherwise refer to your appropriate framework guide on how to set this up. #### Framework Guides - [Astro: Edge KV](/frameworks/astro/#edge-kvs) ## Determining the Tenant In order to read the correct tenant config in middleware, the tenant for the incoming request first needs to be determined. ### Production Environments A mapping between custom domain and tenant config key can be used to determine the correct key per request. Production environments can use `X-Forwarded-Host` which will be provided as a request header from Fastly. ### Local and UAT Environments For cases where custom domains are not attached, such as `localhost` and on deployments to other Altitude development environments, a custom HTTP header may be used to specify tenant per request. A browser extension such as [ModHeader](https://modheader.com/) is convenient for fast tenant switching in this case. ## Multi-tenancy Stylesheets When sharing a codebase with multiple tenants, being able to still differentiate branding colours and typography is essential. However it's equally essential to ensure that the codebase is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and you're not building stylesheets when not necessary. ### Tailwind By utilising [Tailwind Themes](https://tailwindcss.com/docs/theme) alongside a combination of data attributes and CSS variables you can achieve a pattern of having one shared UI but with tenant specific CSS. Example: ```css //main.css @tailwind base; @tailwind components; @tailwind utilities; @layer base { html[data-theme="default"] { --color-esther: 34, 39, 46; --color-maximus: 45, 51, 59; --color-linx: 55, 62, 71; } html[data-theme="neon"] { --color-esther: 20, 61, 42; --color-maximus: 13, 82, 66; --color-linx: 20, 82, 11; } } ``` Within your HTML you then switch to the relevant theme: ```html ``` ##### Alternative: DaisyUI + Tailwind DaisyUI offers a multi-theme configuration out of the box. This can help achieve the capability of creating multiple themes which can be used for creating variety across tenants. [DaisyUI Themes](https://daisyui.com/docs/themes/) [DaisyUI components](https://daisyui.com/components/) :::note Tailwind is a build time dependency and is unaware at the build stage what tenant is active. Therefore you need to consider that the impact of not optimising your theme file(s) efficiently so that minimal CSS changes are required will result in all tenants CSS weight increasing. Using this alongside a speific tenant CSS file that is loaded at runtime (for minor layout changes / adjustments) will help the CSS scale to a larger volume of sites without concerns of shared performance regressions. ::: ### Astro Integration Applications using the Astro Integration can leverage built in mapping to resolve configs to the correct tenant. This is determined at runtime and domains (x-altitude-instance for local development or the domain in prod) will be mapped by matching a value from within a tenants [`domains.variants`](/packages/astro-integration/#domains-options) array. Applications that have a multi tenancy model, will supply an array of configs to the integration function [`altitudeMiddleware`](/packages/astro-integration/#invoke-the-integration). It is recommended that each tenants config is in their own respective file and exported from a single file to keep the configs tidy, sectioned and consistent. ```javascript // /config/index.js import siteOne from "./siteOne"; import siteTwo from "./siteTwo"; import siteThree from "./siteThree"; export default [siteOne, siteTwo, siteThree]; ``` #### Multi Tenancy Config As well as defining tenant specific endpoints and KV keys, the config file allows application owners to extend any site specific values and expose these at runtime. One benefit that the integration allows is the ability to unlock tenant specific environment variables. Tenant specific secrets can be supplied to the build config. The integration exports an `env` function which can be imported and invoked inside of individual build configs. The `env` function takes in the **reference** of the environment variable as an argument not the value. ```js //config/siteOne.js import { env } from '@thg-altitude/astro-integration' export default { domains: { default: "www.example.com", variants: ["www.example.com"], }, ... // exsiting site setup blog: { secret: env('EXAMPLE_SITE_BLOG_SECRET') } }; ``` The value of this environment variable can then be accessed by using the [altitude global context](/packages/astro-integration/#altitude-global-context) `altitude.runtime.config`. ```javascript --- const { altitude, runtime } = Astro.locals // runtime(Production) vs vite development server(Local development) const blogSecret = runtime ? runtime.env[altitude.runtime.config.blog.secret] : import.meta.env[altitude.runtime.config.blog.secret] --- ``` ### Tenant Switching When developing in a multi tenanted application, it may be desired that you are able to switch between tenants on the fly. For local development and DEV/UAT cloudflare environments this is easily achieved by adding a custom HTTP header to the request. The header to be used must be `x-altitude-instance` and the value will determine which tenant to switch to. This value should exist in that tenants config file, listed under the [`domains.variants`](/packages/astro-integration/#domains-options) array. Tenant switching will not be enabled on live domains. Applications that are using [localised domains](/guides/i18n/#localised-domains) will use the domain associated to that locale. ------------------------------------------- # File: astro-integration/v2.0.0/guides/enable-i18n.mdx --- title: Enabling i18n ๐ŸŒŽ order: 2 --- import { Aside } from "@components/docs"; # Enabling internalisation for your sites ## Single tenancy mode: This guide will take you through adding internationalisation for your single-tenanted site. We will assume that you want to serve an english version of your site at www.exampledomain1.com and a german version at www.exampledomain1.de. ### Step 1: Add an i18n key to your buildConfig Add `www.exampledomain1.com` and `www.exampledomain1.de` to your `domains` key at the top level of your build config: ```js // config/site.js export default { domains: ["www.exampledomain1.com", "www.exampledomain1.de"], commerce: { endpoint: "https://horizon-api.www.example.com/graphql", }, kv: [ // ...your kv config ], }; ``` ### Step 2: Add the i18n key to your config We will add an i18n key with two locales, english and german. The `i18n.locales.domain` key must correspond to the domain you added in the `domains` array in step 1. `i18n.locales.kv` will contain locale-specific keys such as language files and locale-specific settings. ```js // config/site.js export default { domains: ["www.exampledomain1.com", "www.exampledomain1.de"], commerce: { endpoint: "https://horizon-api.www.example.com/graphql", }, kv: [ // ...top-level kv ], i18n: { locales: [ { prefix: "en-gb", domain: "www.exampledomain1.com", icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, { prefix: "de-de", domain: "www.exampledomain1.de", kv: [ { key: "tenant1", namespace: "config", local: tenant1Config, }, { key: "tenant1.de_de.properties", namespace: "lang", local: tenant1Lang, }, ], icons: { flag: await fetchIcon("circle-flags", "de"), }, }, ], fallbackLocale: "en-gb", exclusionList: ["api"], localeCookie: "locale_V6", }, }; ``` Please see the [i18n config reference](../reference/config/#i18n) for a full list of required and optional keys. ### Step 3: Support rewrites in astro folder structure When i18n is enabled, astro-integration rewrites every request to `/pathname` to `/localePrefix/pathname` This means that your storefront must have the appropriate request handlers. Please nest your normal handlers in the `/pages` directory into `pages/[locale]`, Example directory structure: ```text pages โ”œโ”€โ”€ [...https].astro โ”œโ”€โ”€ [locale] โ”‚ โ”œโ”€โ”€ [...https].astro โ”‚ โ”œโ”€โ”€ basket.astro โ”‚ โ”œโ”€โ”€ c โ”‚ โ”‚ โ””โ”€โ”€ [...slug] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ create-review โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ kv โ”‚ โ”‚ โ””โ”€โ”€ sessionSettings.js โ”‚ โ”œโ”€โ”€ p โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ reviews โ”‚ โ”‚ โ””โ”€โ”€ [sku].astro โ”‚ โ”œโ”€โ”€ robots.txt.js โ”‚ โ””โ”€โ”€ search.astro โ””โ”€โ”€ 404.astro ``` At request time, astro-integration will use the [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) header (in production) to decide which config to read. In local development, it will read the `x-altitude-instance` header to determine which tenantConfig to use. ### Step 4: Restart the dev server to apply the changes. ```sh $ npm run dev ``` You should no longer be able to view your app without an x-altitude-instance header. Requests to `/` are rewritten to `/en-gb/` when the x-altitude-instance header is www.exampledomain1.com and to to `/de-de/` when the x-altitude-instance header is www.exampldomain1.de. In production, the X-Forwarded-Host header will be used to determine which tenantConfig to use. ### New context variables When i18n is enabled, the following new variables are available in the context of your pages: `altitude.locale` and `altitude.availableLocales`. See the [global context reference](../reference/global-context) for more information on their types and usage. ## Multitenancy mode. This guide will take you through the process of turning on internationalisation for your sites if astro-integration is running in multitenancy mode. ### Prerequisites: Before you begin, make sure you have: - astro-integration running in multitenancy mode. See [guide](../getting-started#multitenancy-mode) for more information. ### Step 1: Add i18n keys to each buildConfig in the tenants array Make sure _every_ tenant in the `tenants` array has an i18n key. ```js import tenants from "./config/index"; // astro.config.js export default defineConfig({ integrations: [ altitudeMiddleware({ config: tenants, api: { enabled: true, graphql: gqlIndex }, }), ], }); ``` ```js import tenant1 from "./tenant1"; import tenant2 from "./tenant2"; import altitudedemo from "./altitudedemo"; export default [tenant1, tenant2, altitudedemo]; ``` `tenant1.js` and `tenant2.js` should both have an i18n key. To see all the required fields, see the [i18n config reference](docs/astro-integration/v2.0.0/reference/config/#i18n). ### Step 2: Support rewrites in astro folder structure When i18n is enabled, astro-integration rewrites every request to `/pathname` to `/localePrefix/pathname` This means that your storefront must have the appropriate request handlers. Please nest your normal handlers in the `/pages` directory into `pages/[locale]`, Example directory structure: ```text pages โ”œโ”€โ”€ [...https].astro โ”œโ”€โ”€ [locale] โ”‚ โ”œโ”€โ”€ [...https].astro โ”‚ โ”œโ”€โ”€ basket.astro โ”‚ โ”œโ”€โ”€ c โ”‚ โ”‚ โ””โ”€โ”€ [...slug] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ create-review โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ kv โ”‚ โ”‚ โ””โ”€โ”€ sessionSettings.js โ”‚ โ”œโ”€โ”€ p โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ reviews โ”‚ โ”‚ โ””โ”€โ”€ [sku].astro โ”‚ โ”œโ”€โ”€ robots.txt.js โ”‚ โ””โ”€โ”€ search.astro โ””โ”€โ”€ 404.astro ``` At request time, astro-integration will read the x-altitude-instance header (in dev mode) or the [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) header (in production) to decide which tenantConfig to read. ## New context variables When i18n is enabled, the following new variables are available in the context of your pages. `altitude.locale` and `altitude.availableLocales`. See the [global context reference](../reference/global-context) for more information on their types and usage. ------------------------------------------- # File: astro-integration/v2.0.0/guides/getting-started.md --- title: Getting started order: 0 --- # Installation ```sh npm i @thg-altitude/astro-integration ``` ## Invoking the integration ### Single-tenancy mode The build config and `altitudeMiddleware` function should be imported and passed as an argument to the integration as shown below. ```js // astro.config.mjs import { defineConfig } from "astro/config"; import config from "./config/site.js"; export default defineConfig({ output: "server", integrations: [ altitudeMiddleware({ config, api: { enabled: true, graphql: gqlIndex }, }), ], }); ``` The config might look something like: ```js // config/site.js import { fetchIcon } from "@thg-altitude/utils"; import siteConfigFile from "../local/tenants/siteone.json"; import siteLangFile from "../local/lang/siteone.en_gb.properties.json"; export default { domains: ["www.siteone.com"], tenantInstance: "siteone", commerce: { endpoint: "https://horizon-api.www.siteone.com/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigFile, }, { key: "siteone.en_gb.properties", namespace: "lang", local: siteLangFile, }, ], icons: { search: await fetchIcon("lucide", "search"), left: await fetchIcon("lucide", "chevron-left"), right: await fetchIcon("lucide", "chevron-right"), }, i18n: { locales: [ { prefix: "en-gb", domain: "www.siteone.com", icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, ], fallbackLocale: "en-gb", exclusionList: ["api", "images"], }, }; ``` For a full list of optional keys see the [config reference](../reference/config) ### Multi-tenancy mode Define each of the site configs in their own respective file: ```javascript // /config/siteone.js import { env } from "@thg-altitude/astro-integration"; export default { domains: ["www.siteone.com"], commerce: { endpoint: "", }, }; ``` Export the config files from a single file as an array containing each config object: ```javascript // /config/index.js import siteone from "./siteone"; import sitetwo from "./sitetwo"; export default [siteone, sitetwo]; ``` Inside of the astro.config.mjs file, import the integration function and invoke this inside of defineConfig with your site configs array. ```javascript import { altitudeMiddleware, env } from "@thg-altitude/astro-integration"; import tenants from "./config"; export default defineConfig({ integrations: [ altitudeMiddleware({ config: tenants, }), ], }); ``` That's it! You can now switch between tenants 'siteone' and 'sitetwo' by switching the x-altitude-instance header between www.siteone.com and www.sitetwo.com. When running your app locally, we recommend using the [ModHeader extension](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en) to change headers. See the [multitenancy reference](../reference/multi-tenancy) for more information. ## Config validation In development mode, the first thing that astro-integration will do is validate the config or configs you have passed to it to make sure they match the schema for the required keys. See the [config reference](../reference/config) for more information. ------------------------------------------- # File: astro-integration/v2.0.0/guides/migration-v2.mdx --- title: Migration from v1 to v2 description: A guide to migrating from v1 to v2 of the Altitude Astro integration --- import { Aside } from "@components/docs"; # Migration Guide: v1 โ†’ v2 This guide outlines the key changes and steps required to migrate your Altitude Astro integration from version 1.x to 2.x. ## 1. Update the Integration Package Update your `package.json` to use the new v2 version: ```sh npm install @thg-altitude/astro-integration@^2.0.0 ``` ## 2. Update Integration Usage in `astro.config.mjs` **Before (v1):** ```js altitudeMiddleware(tenants); ``` **After (v2):** ```js altitudeMiddleware({ config: tenants, }); ``` - In v1, you passed the tenants array directly to `altitudeMiddleware` as the first argument. In v2, you must pass a configuration object, with your tenants array assigned to the `config` property. - The `api` option is **optional**. Only include it if you want to disable or configure the Commerce API. If omitted, the default behavior will be used. If you are using the [commerce API](../reference/commerce-api), the change will look like this: **Before (v1):** ```js altitudeMiddleware(tenants, { enabled: true, graphql: gqlIndex }); ``` **After (v2):** ```js altitudeMiddleware({ config: tenants, api: { enabled: true, graphql: gqlIndex }, }); ``` ## 3. Update Tenant Config Structure **Before (v1):** ```js domains: { default: 'www.example.com', variants: ['www.example.com'] }, ``` **After (v2):** ```js domains: ['www.example.com'], ``` - The `domains` property is now a flat array of domains, not an object with `default` and `variants`. - To avoid regressions, make sure that the old value for `domains.default` is now the first item in the new `domains` array. - Update all tenant config files accordingly. ## 4. Update Middleware Domain Checks If you reference `domains.variants` in your code, update it to `domains`: **Before (v1):** ```js locals?.altitude?.runtime?.config?.domains?.variants.includes(...) ``` **After (v2):** ```js locals?.altitude?.runtime?.config?.domains?.includes(...) ``` ## 5. Test Your Migration - Run your site locally and verify that all tenants resolve correctly. - Check that domain-based routing and tenant switching still work as expected. --- For more details, see the [release notes](https://github.com/THG-AltitudeSiteBuilds/astro-integration/releases) on GitHub, consult the [changelog](../../changelog.mdx) or contact the Altitude Commerce team via the usual channels. ------------------------------------------- # File: astro-integration/v2.0.0/reference/commerce-api.md --- title: Commerce API --- ## Altitude Commerce Endpoint Enabling the commerce endpoint creates a new endpoint `/api/commerce` that allows client side graphql calls to be proxied through the server to your specified endpoint in your build configs `commerce.endpoint`. The option to enable this is done at the point of invoking the altitudeMiddleware. The key benefit of this approach is reducing the size of client-size imports, through no longer needing to import the query in the client-side script. The Astro route injection uses pattern matching to direct requests to the endpoint, `/api/commerce/`. It then looks up the query value in the GraphQL object map, using the operationName search parameter value. ### Configuring the endpoint Firstly, there is a requirement to add `api` to the tenant build config `exclusionList` to avoid localisation rewrites, which would result in the route 404ing. More information on this can be found in the [documentation](/docs/astro-integration/guides/i18n/#i18nexclusionlist) ```js // tenant config obj { exclusionList: ['api'] } ``` The endpoint in enabled by an argument passed to the altitudeMiddleware function. This object needs two keys: `enabled` and `graphql`. The `enabled` key determines if the route to the endpoint should exist, and `graphql` requires an object containing Key/Value pairs of the query name, and the raw query as the value. This object does impact the build size, so it is important to only pass in queries that will be used client-side. If the \_worker.js file size becomes too large, the deployment will fail. ```js //astro.config.js import { altitudeMiddleware } from '@thg-altitude/astro-integration' import buildConfig from './config/site' const graphqlQueriesObj = // your chosen import method export default defineConfig({ integrations: [ altitudeMiddleware( buildConfig, {}, {"enabled": true, "graphql" : graphqlQueriesObj} ), ], // ...other Astro config setup }) ``` ### Sending a request to the endpoint Currently this endpoint uses 4 values in the body: - variables (optional, used for providing data in GraphQL mutations) - horizonApq (optional, defaults to false) - application (Multi-Tenanted Account only - 'account') - opaqueCookieDomain Application currently only applies to the use of the Multi-Tenanted Account option, with a value of 'account'.opaqueCookieDomain allows access the correct domains when setting response headers. Variables handles any input needed by the Horizon query. The horizonApq setting allows enabling [persisted queries](#commerce-api) ``` const variables {...} const url = /api/commerce?operation=ExampleOperation const data = await fetch(url, { method: 'POST', headers: {...}, body: JSON.stringify({ variables: variables, horizonApq: false, application: 'storefront', opaqueCookieDomain }) }) ``` ------------------------------------------- # File: astro-integration/v2.0.0/reference/config.md --- title: Config --- # Configuration The below reference covers all of the different configuration options for the Astro Integration providing further flexibility for your application. ```js //config/site.js export default { // configuration options here... } ``` ### Domains **Type:** `Array[]` \ **Required: True** Contains all domains associated with a site, excluding protocol. If i18n is enabled, this will contain all the domains for which you want localised copy. Make sure to include the same domain in `i18n.locales..domain`. If i18n is not enabled, this array will contain a single item which is the GTLD for your site, which astro integration will read for the purposes of tenant resolution. If running in single-tenancy mode, this key won't be read by the integration. ```javascript domains: ["wwww.example.com", "www.example.fr"] ``` ## Commerce ### commerce.endpoint **Type:** `String` \ **Required: False** The commerce api endpoint the specified site uses. This value will be used with the [commerce api](#commerce-api) method and must be provided if your application intends to use this method. ```javascript commerce: { endpoint: 'https://horizon-api.www.example.com/graphql' } ``` ### commerce.headers **Type:** `Object` \ **Required: False** This block allows headers to be added to a request to the [commerce endpoint](#altitude-commerce-endpoint) on the server side. The name of the header is the key, and the value an object with a type key of "env"|"request" to specify if the value should be taken from environment variables, or from another request header. This is useful for sensitive headers that should not be accessible in the browser, and retaining the value of a header that may get overwritten or masked as it passes through proxies, e.g. client_ip. ```javascript commerce: { endpoint: "https://horizon-api.www.example.com/graphql";, headers: { 'x-example-secret-header': { "type": "env", "variable": "SECRET_HEADER_NAME" } }, 'x-example-new-header': { "type": "request", "variable": "old-header-name" } } ``` This would equate to: ``` x-example-secret-header: import.meta.env.SECRET_HEADER_NAME x-example-new-header : request.headers.get('old-header-name') ``` ## KV **Type:** `Array[]` \ **Required: False** An array of KV options can be supplied. Below are the options that should be supplied per entry. See the [Edge KV](/docs/astro-integration/guides/edge-kv) guide for more details. This field is required even if all locale specific configs have their own KV entries. So use this section either for a sensible set of default values or leave it as an empty array if you are sure that all locales have correct KV configs. ```javascript kv: [ { // kv option entry one goes here }, { // kv option entry two goes here }, ] ``` ### \.key **Type:** `String` \ **Required: True** The key to be retrieved in your Cloudflare KV store. ### \.namespace **Type:** `String` \ **Required: True** This value will be used to attach the contents of the KV key to a specified namespace on the altitude global context e.g. `altitude.runtime.kv.`. More details on the altitude namespace can be found [here](#altitude-global-context) ### \.local **Type:** `Any` \ **Required: True** Used for local development. This will be the value that is resolved when the application is not ran inside of a worker. The value can imported in or defined directly within this key. ```javascript import fooBar from '../local/config' kv: [ { key: 'standalone', namespace: 'config', local: { // local file import variable could also be used foo: 'bar', }, }, ] ``` ## i18n Users of the integration have the ability to opt in to i18n to unlock localisation on their application. The integration is responsible for mapping to locale specific configs and validating a locale is supported when a request is made. to opt-in, see the [guide](/docs/astro-integration/v2.0.0/guides/enable-i18n) on enabling i18n. ### i18n.fallbackLocale **Type**: `String` \ **Required: True** The fallback locale if the locale of the request is invalid. ```javascript i18n: { fallbackLocale: "en-gb"; } ``` ### i18n.exclusionList **Type**: `Array[]` \ **Required: False** An array containing the prefix of the path that should not be localised and subject to localisation at runtime i.e proxies ```javascript i18n: { exclusionList: ["account", "images"]; } ``` ### i18n.locales **Type**: `Array[]` \ **Required: True** An array of locale configs can be supplied. Below are the options that should be supplied per entry. ```javascript i18n: { locales: [ { // en-gb setup }, { // fr-fr setup }, ]; } ``` #### Locale Inheritance Locale configs are treated in a special way in the Astro integration and will inherit from the main config file e.g. this config ```javascript { domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ] } ], fallbackLocale: 'en-gb' } } ``` Will be treated like the below config by the integration. ```javascript { domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ], domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' } } ], fallbackLocale: 'en-gb' } } ``` Notice how the `domains` and `commerce` objects from the top level config are copied into the locale specific config. If a key is present in the locale specific it will **always** take precedence over the same key in the main config and thus act as an override. This inheritance and override behaviour is then applied to nested fields in the main and locale configs. However it does **not** apply to array elements, arrays are inherited or overidden in their entirety. If you want to explicitly opt out of inheriting a particular key from the main config then set that key or its parent as `null` in the locale config. ### i18n.locales.\.prefix **Type**: `String` \ **Required: True** The locale this config is associated with. ```javascript locales: [ { prefix: "en-gb", }, ]; ``` ### i18n.locales.\.domain **Type**: `String` \ **Required: True** The [global top level domain](/#localised-domains)(gTLDs) of this locale excluding protocol. The domain must be specified in the [domains](#domains) key at the root of the config to ensure the integration can map to the sites config. ```javascript locales: [ { prefix: "en-gb", domain: "www.example.com", }, { prefix: "fr-fr", domain: "www.example.fr", }, ]; ``` ### i18n.locales.\.kv Any locale specific KV keys to be retrieved from the cloudflare namespace. See config setup [here](#kv). If there are no locale specific KV settings then favour putting them in the top-level kv object instead. ### i18n.locales.\.commerce.endpoint Any locale specific api endpoint to be used. See config setup [here](/packages/astro-integration/#commerce) ### Custom Custom keys can also be supplied to the build config at locale level as well. ## Custom Custom keys can also be supplied to the build config, such as environment variables. These values will not affect the configuration of the integration but will be provided on the [altitude global context](#altitude-global-context) at runtime. This is useful for multi tenancy when values need to change based on each tenants config. Further information can be found in the [multi tenancy guide](/docs/astro-integration/v2.0.0/reference/multi-tenancy) # Reference - the full schema is available [here](https://github.com/THG-AltitudeSiteBuilds/astro-integration/blob/main/config_schemas/schemaV2.json) ------------------------------------------- # File: astro-integration/v2.0.0/reference/global-context.md --- title: Altitude Global Context --- ## Altitude Global Context The integration will provide additional information about the config resolvement at runtime and attach it to `context.locals.altitude`. Please see all available attachments below. ### altitude.runtime.config The build config object the integration has resolved to. For applications using the integration's [localisation](/docs/astro-integration/guides/i18n) solution this will be the locale specific config. ### altitude.runtime.config.domain The domain that the integration has resolved to. ### altitude.runtime.kv.\ The value of [KV](#kv) retrieved using the key provided. This will be attached using the namespace value provided in the KV for the key retrieved.

Internationalisation

These keys will be provided on the altitude namespace for applications that are using the built in i18n solution. Further information can be found [here](/docs/astro-integration/guides/i18n) ### altitude.locale The locale the integration has resolved to from the request. - `en-gb` ### altitude.availableLocales Array containing all the locales a sites config supports. - `['en-gb', 'fr-fr']` ### altitude.localeDomains (deprecated) An object containing the ISO 639-1 code and domain path it corresponds to. - `{'en-gb': 'https://www.example.com', 'fr-fr': 'https://www.example.fr'}` ### altitude.preferredLocale (deprecated) ISO 639-1 code that resolves to the 'prefix' in the i18n section of the build config. If none are provided or invalid, `null` will be returned. ------------------------------------------- # File: astro-integration/v2.0.0/reference/i18n.md --- title: i18n ๐ŸŒŽ description: Serve localised versions of the same website from different global top level domains --- # Internationalisation If you're looking to translate your website to multiple languages, there are a number of factors to consider. 1. **Language Dictionary** - The collection of language strings. Typically in JSON format. 2. **Routing by Language** - Path based rules that dictate which language dictionary and configuration to load. 3. **API Localisation** - A route to request translated data from the API. 4. **Domains** - How to associate a domian to a specific locale. 5. **Previewing Language Strings** - How to preview the object structure that represent a particular string on a website. Used to support non-technical stakeholders with understanding what keys to update. In this guide we will go through how to achieve each of these steps. ## Language Dictionaries ### Ingenuity Properties and Edge KV (Recommended) Using Ingenuity's Properties Service for managing your language dictionary is recommended as this allows non-technical users to manage the language used across the website without a deployment. Ingenuity Properties Service is a UI for managing your language dictionary. It's a list of keys and values. For the purpose of this guide we assume you have access to our Ingenuity Service's. If you do not, you can request access from your Ingenuity Project team. #### Adding Properties For Altitude deployed sites we syncronise the values in the Ingenuity Properties Service to a key-value store that lives with your deployed website. Typically referred to as an Edge KV. This allows for high-performance read operations and for the site to update language entries without requiring a deployment. We will sync any values from Properties Service that begin following the following format: `altitude.` #### Linking Properties Service to your KV :::note This is currently not yet available to all customers. Please contact the Altitude Platform team to enable this feature. ::: #### Reading Properties Once you have linked Properties Service to your KV you can begin to read the KV from your codebase. Refer to the framework guides below on how to read from a KV. If you're utilising our integrations there are helper methods available for i18n. Refer to the guides referenced below. ##### Framework Guides - [Astro: Reading from Edge KVs in Astro](/guides/edge-kv/) - [Astro Integration: Edge KV configuration](/packages/astro-integration/#kv) - [Astro Integration: i18n method](/packages/astro-integration/#i18n) ### Local Dictionary You can establish a local language dictionary in the codebase by creating local JSON files. For ease of access you can apply these in a middleware to make them accessible across your application. If you are using the Astro Integration, a local copy of your language dictionary can alternatively be supplied as part of the configuration setup for KV. This enables local environments to read directly from a local JSON file at runtime. [See more](/packages/astro-integration/#kv) ## Routing by Language :::note Guide coming soon ::: ## Localised Domains Global top level domains or gTLDs provide SEO benefits by providing localised content on a separate domain. The integration provides application owners the flexibility to configure whether valid locales should redirect to an associated gTLD. To enable localised domains, a locale should supply a `domain` within its respective locale config file. The integration will rewrite the underlying request to prefix the locale to the request which will resolve to the specific config file the locale and gTLD of the request is associated with. This domain should also be added to the `domains.variants` array within the sites config file. To mitigate the risk of duplicate content, any requests directly to the prefix that have not been subject to a rewrite will 404. Example request pattern: ```text www.example.com => rewrite => www.example.com/en-gb/ www.example.com/en-gb/ => www.example.com/en-gb/ (404) ``` ### Application setup As well as configuration updates, application owners must amend routing logic within their application. Routes within the application should be nested inside of a [dynamic route](https://docs.astro.build/en/guides/routing/) unless specified in the `exclusionList` as outlined in the above configuration. The param to use for this dynamic route **must be** `locale`. Further to this, to handle invalid locales as part of the request, an additional catch all page should be created at the root of the pages directory to force [on demand rendering](https://docs.astro.build/en/guides/server-side-rendering/#return-a-response-object). If your application already contains a catch all route, this should be moved inside of the dynamic `[locale]` route and the following snippet added to the new catch all route. ```javascript //pages/[...https].astro // new catch all page --- return new Response(null, Astro.response) --- ``` The 404.astro page should remain at the **root** of the pages directory to serve any custom 404 pages. ## API Localisation When switching the language of a site either through path based routing or gTLDs (Global top level domains) it might be desired that the copy and content/products on site also change if they are independently traded. The integration is able to seamlessly achieve API switching by enabling applications to configure custom configs per locale for sites, including defining different API endpoints. This value should be supplied in the `i18n.locales` locale specific config. ------------------------------------------- # File: astro-integration/v2.0.0/reference/methods.md --- title: Methods --- # Methods The integration provides some useful common functions and patterns available to be used in applications out the box. Methods are attached to the `altitude` namespace and provides storefront owners a layer of abstraction away from verbose core commerce functionality and performance uplifts. ### Quick method reference ```javascript altitude: { i18n: (func, ...args) => String; commerce: { api: async ( operationFields: { operation: String!, variables: Object! }, headers: Object!, options: { apqEnabled: Boolean } ) => Object; }; blog: { api: async ( endpoint: String!, operationFields: { operation: String!, variables: Object, cacheKey: Request! || String!, wafBypass: String!, clientSecret: String!, clientId: String!}, options: { headers: Object } ) => Response; }; cache: { get: async(cacheKey: String! || Request!, operationName: String) => Response || null; set: async(cacheKey: String! || Request!, response: Response!, options: { expiry: Number }) => void; } } ``` ### i18n The i18n method aids with localising copy on site. The function provides flexibility to resolve langauge strings to their values or the object structure to support non-technical stakeholders understand what keys to update. #### valueFunc **Type:** `function() => String` \ **Required: True** An anonymous function that when invoked returns the string that is intended to be evaluated. #### args **Type:** `String` \ **Required: False** Optional argument that will be used to dynamically replace string placeholders in the value of the language string passed, or replace dynamic keys using bracket notation. **Example Use** ```javascript --- const { altitude: { i18n } } = Astro.locals const lang = Astro.locals.altitude.rutime.kv.lang const productTitle = "Vivienne Westwood Logo Ribbed Wool Beanie" const contentKey = "details" ---

{i18n(() => lang.product.promotionalOffer, productTitle)}

// Buy one Vivienne Westwood Logo Ribbed Wool Beanie get one free

{i18n(() => lang.product[contentKey], contentKey)}

//lang.product.details will be the string that is now evaluated ``` **Exposing Keys** The keys used for copy on site can be exposed using headers. This will allow the relevant teams to identify the entry on a site and update its value in Content UI on the fly. To expose the keys an additional request header should be added `Properties-Preview: SHOW-KEYS` ### Commerce API The integration provides out the box commerce api fetching on the server exposing the method as `altitude.commerce.api`. The method enables applications to configure the operation, variables and headers to retrieve commerce data for a given site or tenant. The endpoint the method will use for these calls will be the `commerce.endpoint` supplied in an application or tenants [build config](#configuration). #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: True** The variables to be passed as part of the api call. If no variables are required an empty object should be passed. #### headers **Type:** `Object` \ **Required: True** All required headers to be passed as part of the commerce fetch. No headers are defaulted so all should be provided. #### options.apqEnabled **Type:** `Boolean` \ **Required: False** \ **Default: False** This enables Horizons [Automatic Persisted Queries](https://thehutgroup.github.io/Horizon-Public-Docs/#automated-persisted-queries) feature. #### Return value From version 1.7.0 the api method returns the full response object. Version <=1.6.X The commerce api method will return an Object containing three values: `body`, `duration`, `status`. - `body`: The response from the api call. - `duration`: The duration of the api call in ms. - `status`: The status code of the response. **Example use** ```javascript //c/index.astro const body = await locals.query({ operation: Schema, variables: { handle: pathName, }, customHeaders: { foo: 'bar', }, }) ``` ```javascript //middleware/index.js import { print } from 'graphql' query: async (args) => { const { operation, variables = {}, customHeaders = {} } = args let query if (typeof operation == 'string') { query = operation } else { query = print(operation) } try { const { body, duration, status } = await locals.altitude.commerce.api( { operation: query, variables }, { ...customHeaders, 'Content-Type': 'application/json', 'User-Agent': request.headers.get('User-Agent'), 'X-Altitude-Instance': locals.tenantInstance, // application specific header }, { apqEnabled: false, } ) return body } catch (e) { console.log(e) } } ``` ### Cache API The cache api can be used to enhance the performance of sites by reducing the number of network calls being made as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer. Instead, the response of these calls can be set in cache so future requests can attempt to retrieve the response from cache instead of calling the origin. #### Get ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get a response from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### operationName **Type:** `String` \ **Required: False** \ **Default:** `""` The get cache api function logs out the operation name that has receieved a cache hit for observability. **Example Use** ```javascript let response, cacheKey if (!import.meta.env.DEV) { cacheKey = altitude.createCacheKey(`${horizonEndpoint}/${host}/headerfooter`) response = await altitude.cache.get(cacheKey, 'nav') } ``` #### Set ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to set a response in cache for request lookups. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### response **Type:** `Response` \ **Required: True** The response object to be put into cache to be retrieved for future cache lookups. ##### options.expiry **Type:** `Number` \ **Required: False** \ **Default:** `600` Optional value for how long this response should stay in the cache for in seconds. Defaulted to 600 seconds (10 minutes) **Example Use** ```javascript if (!response) { try { response = await Astro.locals.utils.query({ operation: HeaderFooter, }) if (response.statusText !== 'OK') throw new Error('Error Fetching nav') if (!import.meta.env.DEV) { await altitude.cache.set(cacheKey, response.clone(), { expiry: 600 }) } } catch (e) { console.log(e.message) } } ``` Performance can be improved by using the cache as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer of pages. For example, by using the functions described above to first check the cache, and if its empty, to populate the cache once the data has been fetched: ### Blog API The blog api function is used to fetch blog content from a specified `endpoint`. The fetch utilises the Cache API to get and set auth tokens which are sent as a header to reduce the amount of calls to auth service. #### endpoint **Type:** `String` \ **Required: True** The endpoint the integration should use to retrieve blog data. #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: False** The variables to be passed as part of the api call. #### operationFields.cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get and set auth tokens from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` #### operationFields.wafBypass **Type:** `String` \ **Required: True** Application specific WAF bypass key, used for auth. #### operationFields.clientSecret **Type:** `String` \ **Required: True** Application specific client secret, used for auth. #### operationFields.clientId **Type:** `String` \ **Required: True** Tenant or Application specific ID, used for auth. #### options.headers **Type:** `Object` \ **Required: False** Any additional headers to be sent as part of the request. `Content-Type: application/json` and `Authorization` are currently defaulted. **Example Use** ```javascript const blogEndpoint = Astro.locals?.tenantConfig?.application?.features?.tesseract?.endpoint let resp try { resp = await altitude.blog.api(blogEndpoint, { operation: TesseractHome, cacheKey: altitude.createCacheKey( `${Astro.locals.tenantConfig.application.horizonEndpoint}/${Astro.locals.host}/blog` ), wafBypass: import.meta.env.WAF_BYPASS, clientSecret: import.meta.env.AUTH_CLIENT_SECRET, clientId:import.meta.env.BLOG_CLIENT_ID }) } catch (e) { console.log(e) } ``` ------------------------------------------- # File: astro-integration/v2.0.0/reference/multi-tenancy.md --- title: Multi-Tenancy description: Serve multiple brands from a single Altitude site. --- Multi-tenancy is a pattern in which multiple brands may be served from the same codebase. It can be used to quickly onboard a new brand, or 'tenant', via a new configuration, as a fast route to scale. To enable multi-tenancy for your site, see the [guide](/docs/astro-integration/v2.0.0/guides/getting-started#multi-tenancy-mode) --- # Multi-tenancy ## Custom Domains Altitude allows multiple [custom domains](https://docs.thgaltitude.com/edge/domains/) to be attached to a single site, giving the feel of a separately hosted site per-tenant. For example `www.allsole.com` and `www.biossance.com` point to the same Altitude deployment. ## Tenant Configuration There are certain properties which will want to vary between tenants to give then a distinct feel, such as: - Styling - Feature flags - API urls, reCaptcha keys etc. A solution to achieving this is for your application to read these properties from a configuration object which can vary per-tenant. ## Key-Value Stores Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. ## Determining the Tenant To select the correct tenant config, Altitude uses the request's domain or a special header: - **Production:** The `X-Forwarded-Host` header (set by Fastly) maps the domain to the tenant config. - **Development/local:** The `x-altitude-instance` header specifies the tenant. You can set this header using a tool like [ModHeader](https://modheader.com/) for easy switching. ## Multi-tenancy Stylesheets When sharing a codebase with multiple tenants, being able to differentiate branding colours and typography is essential. However it's equally essential to ensure that the codebase is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and you're not building stylesheets when not necessary. ### Tailwind By utilising [Tailwind Themes](https://tailwindcss.com/docs/theme) alongside a combination of data attributes and CSS variables you can achieve a pattern of having one shared UI but with tenant specific CSS. Example: ```css //main.css @tailwind base; @tailwind components; @tailwind utilities; @layer base { html[data-theme="default"] { --color-esther: 34, 39, 46; --color-maximus: 45, 51, 59; --color-linx: 55, 62, 71; } html[data-theme="neon"] { --color-esther: 20, 61, 42; --color-maximus: 13, 82, 66; --color-linx: 20, 82, 11; } } ``` Within your HTML you then switch to the relevant theme: ```html ``` ##### Alternative: DaisyUI + Tailwind DaisyUI offers a multi-theme configuration out of the box. This can help achieve the capability of creating multiple themes which can be used for creating variety across tenants. [DaisyUI Themes](https://daisyui.com/docs/themes/) [DaisyUI components](https://daisyui.com/components/) :::note Tailwind is a build time dependency and is unaware at the build stage what tenant is active. Therefore you need to consider that the impact of not optimising your theme file(s) efficiently so that minimal CSS changes are required will result in all tenants CSS weight increasing. Using this alongside a speific tenant CSS file that is loaded at runtime (for minor layout changes / adjustments) will help the CSS scale to a larger volume of sites without concerns of shared performance regressions. ::: ### Astro Integration Applications using the Astro Integration can leverage built in mapping to resolve configs to the correct tenant. This is determined at runtime and domains (x-altitude-instance for local development or the domain in prod) will be mapped by matching a value from within a tenants [`domains.variants`](/packages/astro-integration/#domains-options) array. Applications that have a multi tenancy model, will supply an array of configs to the integration function [`altitudeMiddleware`](/packages/astro-integration/#invoke-the-integration). It is recommended that each tenants config is in their own respective file and exported from a single file to keep the configs tidy, sectioned and consistent. ```javascript // /config/index.js import siteOne from "./siteOne"; import siteTwo from "./siteTwo"; import siteThree from "./siteThree"; export default [siteOne, siteTwo, siteThree]; ``` #### Multi Tenancy Config As well as defining tenant specific endpoints and KV keys, the config file allows application owners to extend any site specific values and expose these at runtime. One benefit that the integration allows is the ability to unlock tenant specific environment variables. Tenant specific secrets can be supplied to the build config. The integration exports an `env` function which can be imported and invoked inside of individual build configs. The `env` function takes in the **reference** of the environment variable as an argument not the value. ```js //config/siteOne.js import { env } from '@thg-altitude/astro-integration' export default { domains: { default: "www.example.com", variants: ["www.example.com"], }, ... // exsiting site setup blog: { secret: env('EXAMPLE_SITE_BLOG_SECRET') } }; ``` The value of this environment variable can then be accessed by using the [altitude global context](/packages/astro-integration/#altitude-global-context) `altitude.runtime.config`. ```javascript --- const { altitude, runtime } = Astro.locals // runtime(Production) vs vite development server(Local development) const blogSecret = runtime ? runtime.env[altitude.runtime.config.blog.secret] : import.meta.env[altitude.runtime.config.blog.secret] --- ``` ### Tenant Switching When developing in a multi tenanted application, it may be desired that you are able to switch between tenants on the fly. For local development and DEV/UAT cloudflare environments this is easily achieved by adding a custom HTTP header to the request. The header to be used must be `x-altitude-instance` and the value will determine which tenant to switch to. This value should exist in that tenants config file, listed under the [`domains.variants`](/packages/astro-integration/#domains-options) array. Tenant switching will not be enabled on live domains. Applications that are using [localised domains](/guides/i18n/#localised-domains) will use the domain associated to that locale. ------------------------------------------- # File: astro-integration/v3.0.0/guides/enable-i18n.mdx --- title: Enabling i18n ๐ŸŒŽ order: 2 --- import { Aside } from "@components/docs"; # Enabling internalisation for your sites ## Single tenancy mode: This guide will take you through adding internationalisation for your single-tenanted site. We will assume that you want to serve an english version of your site at www.exampledomain1.com and a german version at www.exampledomain1.de. ### Step 1: Add an i18n key to your buildConfig Add `www.exampledomain1.com` and `www.exampledomain1.de` to your `domains` key at the top level of your build config: ```js // config/site.js export default { domains: ["www.exampledomain1.com", "www.exampledomain1.de"], commerce: { endpoint: "https://horizon-api.www.example.com/graphql", }, kv: [ // ...your kv config ], }; ``` ### Step 2: Add the i18n key to your config We will add an i18n key with two locales, English and German. In v3.0.0, the configuration is organized by domain, with each domain specifying its routing strategy and locale configurations. ```js // config/site.js export default { domains: ["www.exampledomain1.com", "www.exampledomain1.de"], commerce: { endpoint: "https://horizon-api.www.example.com/graphql", }, kv: [ // ...top-level kv ], i18n: { domains: { "www.exampledomain1.com": { pathBasedRouting: false, // Domain-based routing for English site fallbackLocale: "en-gb", locales: { "en-gb": { icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, }, }, "www.exampledomain1.de": { pathBasedRouting: false, // Domain-based routing for German site fallbackLocale: "de-de", locales: { "de-de": { kv: [ { key: "tenant1", namespace: "config", local: tenant1Config, }, { key: "tenant1.de_de.properties", namespace: "lang", local: tenant1Lang, }, ], icons: { flag: await fetchIcon("circle-flags", "de"), }, }, }, }, }, localeCookie: "locale_V6", }, }; ``` Please see the [i18n config reference](../reference/config/#i18n) for a full list of required and optional keys. ### Step 3: Support rewrites in astro folder structure When i18n is enabled, astro-integration rewrites every request to `/pathname` to `/localePrefix/pathname` This means that your storefront must have the appropriate request handlers. Please nest your normal handlers in the `/pages` directory into `pages/[locale]`, Example directory structure: ```text pages โ”œโ”€โ”€ [...https].astro โ”œโ”€โ”€ [locale] โ”‚ โ”œโ”€โ”€ [...https].astro โ”‚ โ”œโ”€โ”€ basket.astro โ”‚ โ”œโ”€โ”€ c โ”‚ โ”‚ โ””โ”€โ”€ [...slug] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ create-review โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ kv โ”‚ โ”‚ โ””โ”€โ”€ sessionSettings.js โ”‚ โ”œโ”€โ”€ p โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ reviews โ”‚ โ”‚ โ””โ”€โ”€ [sku].astro โ”‚ โ”œโ”€โ”€ robots.txt.js โ”‚ โ””โ”€โ”€ search.astro โ””โ”€โ”€ 404.astro ``` At request time, astro-integration will use the [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) header (in production) to decide which config to read. In local development, it will read the `x-altitude-instance` header to determine which tenantConfig to use. ### Step 4: Restart the dev server to apply the changes. ```sh $ npm run dev ``` You should no longer be able to view your app without an x-altitude-instance header. Requests to `/` are rewritten to `/en-gb/` when the x-altitude-instance header is www.exampledomain1.com and to to `/de-de/` when the x-altitude-instance header is www.exampldomain1.de. In production, the X-Forwarded-Host header will be used to determine which tenantConfig to use. ### New context variables When i18n is enabled, the following new variables are available in the context of your pages: `altitude.locale` and `altitude.availableLocales`. See the [global context reference](../reference/global-context) for more information on their types and usage. ## Multitenancy mode. This guide will take you through the process of turning on internationalisation for your sites if astro-integration is running in multitenancy mode. ### Prerequisites: Before you begin, make sure you have: - astro-integration running in multitenancy mode. See [guide](../getting-started#multitenancy-mode) for more information. ### Step 1: Add i18n keys to each buildConfig in the tenants array Make sure _every_ tenant in the `tenants` array has an i18n key. ```js import tenants from "./config/index"; // astro.config.js export default defineConfig({ integrations: [ altitudeMiddleware({ config: tenants, api: { enabled: true, graphql: gqlIndex }, }), ], }); ``` ```js import tenant1 from "./tenant1"; import tenant2 from "./tenant2"; import altitudedemo from "./altitudedemo"; export default [tenant1, tenant2, altitudedemo]; ``` `tenant1.js` and `tenant2.js` should both have an i18n key. To see all the required fields, see the [i18n config reference](docs/astro-integration/v2.0.0/reference/config/#i18n). ### Step 2: Support rewrites in astro folder structure When i18n is enabled, astro-integration rewrites every request to `/pathname` to `/localePrefix/pathname` This means that your storefront must have the appropriate request handlers. Please nest your normal handlers in the `/pages` directory into `pages/[locale]`, Example directory structure: ```text pages โ”œโ”€โ”€ [...https].astro โ”œโ”€โ”€ [locale] โ”‚ โ”œโ”€โ”€ [...https].astro โ”‚ โ”œโ”€โ”€ basket.astro โ”‚ โ”œโ”€โ”€ c โ”‚ โ”‚ โ””โ”€โ”€ [...slug] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ create-review โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ kv โ”‚ โ”‚ โ””โ”€โ”€ sessionSettings.js โ”‚ โ”œโ”€โ”€ p โ”‚ โ”‚ โ””โ”€โ”€ [...handle] โ”‚ โ”‚ โ””โ”€โ”€ index.astro โ”‚ โ”œโ”€โ”€ reviews โ”‚ โ”‚ โ””โ”€โ”€ [sku].astro โ”‚ โ”œโ”€โ”€ robots.txt.js โ”‚ โ””โ”€โ”€ search.astro โ””โ”€โ”€ 404.astro ``` At request time, astro-integration will read the x-altitude-instance header (in dev mode) or the [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) header (in production) to decide which tenantConfig to read. ## New context variables When i18n is enabled, the following new variables are available in the context of your pages. `altitude.locale` and `altitude.availableLocales`. See the [global context reference](../reference/global-context) for more information on their types and usage. ------------------------------------------- # File: astro-integration/v3.0.0/guides/getting-started.md --- title: Getting started order: 0 --- # Installation ```sh npm i @thg-altitude/astro-integration ``` ## Invoking the integration ### Single-tenancy mode The build config and `altitudeMiddleware` function should be imported and passed as an argument to the integration as shown below. ```js // astro.config.mjs import { defineConfig } from "astro/config"; import config from "./config/site.js"; export default defineConfig({ output: "server", integrations: [ altitudeMiddleware({ config, api: { enabled: true, graphql: gqlIndex, dirName: "[locale]" }, }), ], }); ``` The config might look something like: ```js // config/site.js import { fetchIcon } from "@thg-altitude/utils"; import siteConfigFile from "../local/tenants/siteone.json"; import siteLangFile from "../local/lang/siteone.en_gb.properties.json"; export default { domains: ["www.siteone.com"], tenantInstance: "siteone", commerce: { endpoint: "https://horizon-api.www.siteone.com/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigFile, }, { key: "siteone.en_gb.properties", namespace: "lang", local: siteLangFile, }, ], icons: { search: await fetchIcon("lucide", "search"), left: await fetchIcon("lucide", "chevron-left"), right: await fetchIcon("lucide", "chevron-right"), }, i18n: { domains: { "www.siteone.com": { locales: { "en-gb": { icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, }, pathBasedRouting: false, fallbackLocale: "en-us", }, }, localeCookie: "locale_V6", }, }; ``` For a full list of optional keys see the [config reference](../reference/config) ### Multi-tenancy mode Define each of the site configs in their own respective file: ```javascript // /config/siteone.js import { env } from "@thg-altitude/astro-integration"; export default { domains: ["www.siteone.com"], commerce: { endpoint: "", }, }; ``` Export the config files from a single file as an array containing each config object: ```javascript // /config/index.js import siteone from "./siteone"; import sitetwo from "./sitetwo"; export default [siteone, sitetwo]; ``` Inside of the astro.config.mjs file, import the integration function and invoke this inside of defineConfig with your site configs array. ```javascript import { altitudeMiddleware, env } from "@thg-altitude/astro-integration"; import tenants from "./config"; export default defineConfig({ integrations: [ altitudeMiddleware({ config: tenants, }), ], }); ``` That's it! You can now switch between tenants 'siteone' and 'sitetwo' by switching the x-altitude-instance header between www.siteone.com and www.sitetwo.com. When running your app locally, we recommend using the [ModHeader extension](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en) to change headers. See the [multitenancy reference](../reference/multi-tenancy) for more information. ### Path based routing Version 3 of the Astro Integration now supports path based routing. One tenant can support both domain based and path based routing. For example, the config below has two domains, a .com and a .de, here the .com site does domain based routing and the .de does path based supporting both .de/de-de/ and .de/de-at/ paths. ```js // config/site.js import { fetchIcon } from "@thg-altitude/utils"; import siteConfigFile from "../local/tenants/siteone.json"; import siteLangFile from "../local/lang/siteone.en_gb.properties.json"; import siteConfigDeFile from "../local/tenants/siteoneDe.json"; import siteLangDeFile from "../local/lang/siteone.de_de.properties.json"; import siteConfigAtFile from "../local/tenants/siteoneAt.json"; import siteLangAtFile from "../local/lang/siteone.de_at.properties.json"; export default { domains: ["www.siteone.com", "www.siteone.de"], tenantInstance: "siteone", commerce: { endpoint: "https://horizon-api.www.siteone.com/en-gb/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigFile, }, { key: "siteone.en_gb.properties", namespace: "lang", local: siteLangFile, }, ], icons: { search: await fetchIcon("lucide", "search"), left: await fetchIcon("lucide", "chevron-left"), right: await fetchIcon("lucide", "chevron-right"), }, i18n: { domains: { "www.siteone.com": { locales: { "en-gb": { icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, }, pathBasedRouting: false, fallbackLocale: "de-de", }, "www.siteone.de": { locales: { "de-de": { icons: { flag: await fetchIcon("circle-flags", "de"), }, commerce: { endpoint: "https://horizon-api.www.siteone.com/de-de/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigDeFile, }, { key: "siteone.de_de.properties", namespace: "lang", local: siteLangDeFile, }, ], }, "de-at": { icons: { flag: await fetchIcon("circle-flags", "at"), }, commerce: { endpoint: "https://horizon-api.www.siteone.com/de-at/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigAtFile, }, { key: "siteone.de_at.properties", namespace: "lang", local: siteLangAtFile, }, ], }, }, pathBasedRouting: true, fallbackLocale: "de-de", }, }, localeCookie: "locale_V6", }, }; ``` ### Custom Locale Prefixes V3.0.0 introduces the ability to use custom URL prefixes instead of standard locale codes. This allows for more user-friendly URLs and better local branding: ```js // config/site.js with custom prefixes export default { domains: ["www.example.com"], tenantInstance: "example", commerce: { endpoint: "https://api.example.com/graphql", }, i18n: { domains: { "www.example.com": { pathBasedRouting: true, fallbackLocale: "en-us", locales: { "en-us": { commerce: { endpoint: "https://api.example.com/en/graphql" } }, "en-mt": { customPrefix: "en-eu", commerce: { endpoint: "https://api.example.com/fr/graphql" } }, "de-de": { commerce: { endpoint: "https://api.example.com/de/graphql" } } } } }, localeCookie: "locale_V6" } } ``` This configuration creates URL patterns like: - `www.example.com/en-us/p/123` (English - fallback locale) - `www.example.com/en-mt/p/123` (en-mt site with custom prefix) - `www.example.com/de-de/p/123` (German site) ``` ## Config validation In development mode, the astro-integration will validate your configuration against JSON Schema v3 to ensure all required properties are present and properly formatted. The validation includes: - **Required properties**: domains, commerce.endpoint, pathBasedRouting, fallbackLocale - **Format validation**: Locale codes must match `xx-xx` pattern - **Custom prefix validation**: Must be URL-safe if provided - **Domain constraints**: Domain-based routing limited to 1 locale per domain See the [config reference](../reference/config) for complete schema information. ------------------------------------------- # File: astro-integration/v3.0.0/guides/migration-v3.mdx --- title: Migration from v2 to v3 description: A guide to migrating from v2 to v3 of the Altitude Astro integration --- import { Aside } from "@components/docs"; # Migration Guide: v2 โ†’ v3 This guide outlines the key changes and steps required to migrate your Altitude Astro integration from version 2.x to 3.x. ## 1. Update the Integration Package Update your `package.json` to use the new v3 version: ```sh npm install @thg-altitude/astro-integration@^3.0.0 ``` ## 2. Update Integration Usage in `astro.config.mjs` if you are using the [commerce API](../reference/commerce-api): **Before (v2):** ```js altitudeMiddleware({ config: tenants, api: { enabled: true, graphql: gqlIndex } }); ``` **After (v3):** ```js altitudeMiddleware({ config: tenants, api: { enabled: true, graphql: gqlIndex, dirName: "[locale]" } }); ``` - In v3, if your application uses the commerce API you now need to specify the location of your api route using the dirName prop. - This will be "/api/commerce" if no dirName is supplied or "/'dirName'/api/commerce if it is supplied. ## 3. Update Tenant Config Structure The v3.0.0 release introduces significant changes to the configuration structure, particularly for i18n settings. ### Configuration Structure Changes **Before (v2):** ```json { "domains": ["example.com"], "commerce": { "endpoint": "https://api.example.com/graphql" }, "kv": [ { "key": "config", "namespace": "config", "local": {} } ], "i18n": { "locales": { "en-us": { "domain": "example.com" }, "fr-fr": { "domain": "fr.example.com" } }, "localeCookie": "locale" } } ``` **After (v3):** ```json { "domains": ["example.com", "fr.example.com"], "commerce": { "endpoint": "https://api.example.com/graphql" }, "kv": [ { "key": "config", "namespace": "config", "local": {} } ], "i18n": { "domains": { "example.com": { "pathBasedRouting": true, "fallbackLocale": "en-us", "locales": { "en-us": { "commerce": { "endpoint": "https://api.example.com/graphql" } }, "fr-fr": { "customPrefix": "french", "commerce": { "endpoint": "https://api.fr.example.com/graphql" } } } }, "fr.example.com": { "pathBasedRouting": false, "fallbackLocale": "fr-fr", "locales": { "fr-fr": { "commerce": { "endpoint": "https://api.fr.example.com/graphql" } } } } }, "localeCookie": "locale" } } ``` ### Key Changes Explained 1. **Domain-based i18n structure**: Configuration is now organized by domain rather than by locale 2. **Required `pathBasedRouting` property**: Each domain must specify whether to use path-based routing 3. **Required `fallbackLocale` property**: Each domain must specify a fallback locale 4. **`customPrefix` support**: Locales can now use custom URL prefixes instead of locale codes 5. **Nested commerce and kv configs**: Locale-specific configurations can override global settings ### Breaking Changes Checklist - [ ] **Remove `exclusionList` properties** - No longer read from in v3 - [ ] **Update i18n structure** - Move from locale-based to domain-based organization - [ ] **Add `pathBasedRouting`** - Specify routing strategy for each domain - [ ] **Add `fallbackLocale`** - Define fallback locale for each domain - [ ] **Update locale configurations** - Move locale-specific settings under domain structure - [ ] **Remove deprecated context usage** - Update code that uses `locals.altitude.preferredLocale` or `locals.altitude.localeDomains` ### Using Custom Prefixes The new `customPrefix` feature allows you to use custom URL paths instead of locale codes: **Example with custom prefixes:** ```json { "i18n": { "domains": { "example.com": { "pathBasedRouting": true, "fallbackLocale": "en-us", "locales": { "en-us": { "commerce": {...} }, "en-mt": { "customPrefix": "en-eu", "commerce": {...} }, "de-de": { "commerce": {...} } } } } } } ``` This configuration would create URLs like: - `example.com/en-us/` (English, default) - `example.com/en-eu/` (Europe, custom prefix used instead of locale) - `example.com/de-de/` (German) ### Configuration Validation V3 includes enhanced JSON schema validation. Validate your configuration: ```bash # Install the integration and run validation npm install @thg-altitude/astro-integration@^3.0.0 # Your configuration will be automatically validated at build time npm run build ``` ### Step-by-Step Migration Process 1. **Update the package version** in `package.json` 2. **Restructure each tenant configuration file**: - Group locales by domain in `i18n.domains` - Add required `pathBasedRouting` and `fallbackLocale` properties - Move locale-specific commerce/kv configs under each locale - Add `customPrefix` if desired for cleaner URLs 3. **Remove deprecated properties**: - Delete any `exclusionList` properties - Remove locale-based domain mappings from v2 structure 4. **Update application code**: - Replace usage of deprecated context properties - Update any direct configuration access patterns 5. **Test thoroughly** with the new configuration structure ## 4. Troubleshooting Common Issues ### Configuration Validation Errors **Error**: `"pathBasedRouting" property is required` **Solution**: Add `"pathBasedRouting": true` or `false` to each domain in `i18n.domains` **Error**: `"fallbackLocale" property is required` **Solution**: Add `"fallbackLocale": "xx-xx"` with a valid locale code for each domain **Error**: Domain-based routing with multiple locales **Solution**: When `pathBasedRouting: false`, ensure only one locale is configured per domain ### Runtime Issues **Issue**: Locale detection not working **Solution**: Check that your `localeCookie` configuration matches your application's cookie handling **Issue**: Custom prefixes not resolving **Solution**: Ensure `customPrefix` values are URL-safe and don't conflict with existing routes **Issue**: Commerce API endpoints not found **Solution**: Verify that locale-specific commerce configurations are properly nested under each locale ## 5. Test Your Migration - Run your site locally and verify that all tenants resolve correctly. - Check that domain-based routing and tenant switching still work as expected. --- For more details, see the [release notes](https://github.com/THG-AltitudeSiteBuilds/astro-integration/releases) on GitHub, consult the [changelog](../../changelog.mdx) or contact the Altitude Commerce team via the usual channels. ------------------------------------------- # File: astro-integration/v3.0.0/reference/commerce-api.md --- title: Commerce API --- ## Altitude Commerce Endpoint Enabling the commerce endpoint creates a new endpoint `/api/commerce` that allows client side graphql calls to be proxied through the server to your specified endpoint in your build configs `commerce.endpoint`. The option to enable this is done at the point of invoking the altitudeMiddleware. The key benefit of this approach is reducing the size of client-size imports, through no longer needing to import the query in the client-side script. The Astro route injection uses pattern matching to direct requests to the endpoint, `/api/commerce/`. It then looks up the query value in the GraphQL object map, using the operationName search parameter value. ### Configuring the endpoint The endpoint in enabled by an argument passed to the altitudeMiddleware function. This object can have three keys: `enabled`, `graphql` and additional `dirName`. The `enabled` key determines if the route to the endpoint should exist, and `graphql` requires an object containing Key/Value pairs of the query name, and the raw query as the value. In v3 of the Astro Integration you can now pass a `dirName` property, which can be either a static or dynamic value (e.g. `[locale]`). This determines the path where the `/api/commerce` endpoint is injected. For example, if `dirName` is set, the route will be injected at `/${dirName}/api/commerce`; otherwise, it defaults to `/api/commerce`. This is handled by the route injection logic: ```js injectRoute({ pattern: api.dirName ? "/" + api.dirName + "/api/commerce" : "/api/commerce", entrypoint: "@thg-altitude/astro-integration/api/commerce.js", }); ``` If you use a dynamic `dirName` (such as `[locale]`), make sure your client requests are prefixed with the correct dynamic segment, e.g. `/${Astro.locals.locale}/api/commerce?operation=ExampleOperation`. This object does impact the build size, so it is important to only pass in queries that will be used client-side. If the \_worker.js file size becomes too large, the deployment will fail. ```js //astro.config.js import { altitudeMiddleware } from '@thg-altitude/astro-integration' import buildConfig from './config/site' const graphqlQueriesObj = // your chosen import method export default defineConfig({ integrations: [ altitudeMiddleware( buildConfig, {}, {"enabled": true, "graphql" : graphqlQueriesObj, "dirName": "[locale]"} ), ], // ...other Astro config setup }) ``` ### Sending a request to the endpoint Currently this endpoint uses 4 values in the body: - variables (optional, used for providing data in GraphQL mutations) - horizonApq (optional, defaults to false) - application (Multi-Tenanted Account only - 'account') - opaqueCookieDomain Application currently only applies to the use of the Multi-Tenanted Account option, with a value of 'account'.opaqueCookieDomain allows access the correct domains when setting response headers. Variables handles any input needed by the Horizon query. The horizonApq setting allows enabling [persisted queries](#commerce-api) ``` const variables {...} const url = /api/commerce?operation=ExampleOperation // see note below const data = await fetch(url, { method: 'POST', headers: {...}, body: JSON.stringify({ variables: variables, horizonApq: false, application: 'storefront', opaqueCookieDomain }) }) ``` For the url, if a `dirName` has been passed this needs to match the url. If the `dirName` was a dynamic path like locale the url needs to be prefixed with this dynamically e.g. `/${Astro.locals.locale}/api/commerce?operation=ExampleOperation` ------------------------------------------- # File: astro-integration/v3.0.0/reference/config.md --- title: Config --- # Configuration The below reference covers all of the different configuration options for the Astro Integration v3.0.0, providing enhanced flexibility for your application. ```js //config/site.js export default { // configuration options here... } ``` ### Domains **Type:** `Array[]` \ **Required: True** Contains all domains associated with a site, excluding protocol. If i18n is enabled, this will contain all the domains for which you want localised copy. Make sure to include the same domain in `i18n`. If i18n is not enabled, this array will contain a single item which is the GTLD for your site, which astro integration will read for the purposes of tenant resolution. If running in single-tenancy mode, this key won't be read by the integration. ```javascript domains: ["wwww.example.com", "www.example.fr"] ``` ## Commerce ### commerce.endpoint **Type:** `String` \ **Required: False** The commerce api endpoint the specified site uses. This value will be used with the [commerce api](#commerce-api) method and must be provided if your application intends to use this method. ```javascript commerce: { endpoint: 'https://horizon-api.www.example.com/graphql' } ``` ### commerce.headers **Type:** `Object` \ **Required: False** This block allows headers to be added to a request to the [commerce endpoint](#altitude-commerce-endpoint) on the server side. The name of the header is the key, and the value an object with a type key of "env"|"request" to specify if the value should be taken from environment variables, or from another request header. This is useful for sensitive headers that should not be accessible in the browser, and retaining the value of a header that may get overwritten or masked as it passes through proxies, e.g. client_ip. ```javascript commerce: { endpoint: "https://horizon-api.www.example.com/graphql";, headers: { 'x-example-secret-header': { "type": "env", "variable": "SECRET_HEADER_NAME" } }, 'x-example-new-header': { "type": "request", "variable": "old-header-name" } } ``` This would equate to: ``` x-example-secret-header: import.meta.env.SECRET_HEADER_NAME x-example-new-header : request.headers.get('old-header-name') ``` ## KV **Type:** `Array[]` \ **Required: False** An array of KV options can be supplied. Below are the options that should be supplied per entry. See the [Edge KV](/docs/astro-integration/guides/edge-kv) guide for more details. This field is required even if all locale specific configs have their own KV entries. So use this section either for a sensible set of default values or leave it as an empty array if you are sure that all locales have correct KV configs. ```javascript kv: [ { // kv option entry one goes here }, { // kv option entry two goes here }, ] ``` ### \.key **Type:** `String` \ **Required: True** The key to be retrieved in your Cloudflare KV store. ### \.namespace **Type:** `String` \ **Required: True** This value will be used to attach the contents of the KV key to a specified namespace on the altitude global context e.g. `altitude.runtime.kv.`. More details on the altitude namespace can be found [here](#altitude-global-context) ### \.local **Type:** `Any` \ **Required: True** Used for local development. This will be the value that is resolved when the application is not ran inside of a worker. The value can imported in or defined directly within this key. ```javascript import fooBar from '../local/config' kv: [ { key: 'standalone', namespace: 'config', local: { // local file import variable could also be used foo: 'bar', }, }, ] ``` ## i18n Users of the integration have the ability to opt in to i18n to unlock localisation on their application. The integration is responsible for mapping to locale specific configs and validating a locale is supported when a request is made. The i18n configuration enables localisation and allows you to define domains and their associated locales, each with their own settings. to opt-in, see the [guide](/docs/astro-integration/v3.0.0/guides/enable-i18n) on enabling i18n. ### Example ```js i18n: { domains: { '': { locales: { '': { commerce: { endpoint: '' }, kv: [ /* KV config objects */ ], icons: { flag: /* icon object */ } }, // ...other locales }, pathBasedRouting: true, // Optional: enables path-based locale routing fallbackLocale: '' // Fallback locale for this domain }, // ...other domains }, localeCookie: '' // Optional: name of the cookie used to persist locale } ``` ### i18n.domains **Type**: `Object` \ **Required: True** Defines all domains for which you want to provide localised content. Each domain key maps to its own locale configuration. The domain must be specified in the [domains](#domains) key at the root of the config to ensure the integration can map to the sites config. ```javascript i18n: { domains: { 'domain1': {...}, 'domain2': {...} } } ``` ### i18n.domains.\.locales **Type**: `Object` \ **Required: True** Lists all supported locales for the domain. Each locale code (e.g. 'en-gb') maps to its own configuration object. If a domain is doing path based routing it can have more than one locale associated with it (in this case the `pathBasedRouting` flag must be set to true). ```javascript i18n: { domains: { '': { locales: { 'en-gb': {...} 'en-us': {...} } } } } ``` ### i18n.domains.\.pathBasedRouting **Type**: `Boolean` \ **Required: True** Enables path-based locale routing for this domain. When `true`, multiple locales can be served from the same domain using URL path prefixes (e.g., `/en-gb/`, `/fr-fr/`). When `false`, the domain serves only one locale without path prefixes. **Schema Validation**: - When `pathBasedRouting: false`, only one locale is allowed per domain - When `pathBasedRouting: true`, unlimited locales are allowed per domain **Examples**: ```javascript // Path-based routing (multiple locales) "www.example.com": { pathBasedRouting: true, fallbackLocale: "en-us", locales: { "en-us": {...}, "fr-fr": {...}, "de-de": {...} } } // Domain-based routing (single locale) "fr.example.com": { pathBasedRouting: false, fallbackLocale: "fr-fr", locales: { "fr-fr": {...} } } ``` ### i18n.domains.\.locales.\.customPrefix **Type**: `String` \ **Required: False** Allows you to define a custom URL path prefix for a locale instead of using the standard locale code. This is particularly useful for creating user-friendly URLs or branding purposes. **Examples:** ```javascript i18n: { domains: { 'www.example.com': { pathBasedRouting: true, fallbackLocale: 'en-us', locales: { 'en-us': { // No customPrefix - uses locale as prefix commerce: {...} }, 'fr-fr': { customPrefix: 'francais', commerce: {...} }, 'de-de': { customPrefix: 'deutsch', commerce: {...} }, 'es-es': { customPrefix: 'espanol', commerce: {...} } } } } } ``` This configuration creates URL patterns like: - `www.example.com/en-us/` (English - fallback locale) - `www.example.com/francais/` (French) - `www.example.com/deutsch/` (German) - `www.example.com/espanol/` (Spanish) Note: ensure an proxys you site uses is tolerant to different path prefix structures. ### i18n.domains.\.locales.\.kv Any locale specific KV keys to be retrieved from the cloudflare namespace. See config setup [here](#kv). If there are no locale specific KV settings then favour putting them in the top-level kv object instead. ### i18n.domains.\.locales.\.commerce.endpoint Any locale specific api endpoint to be used. See config setup [here](/packages/astro-integration/#commerce) ### i18n.domains.\.fallbackLocale **Type**: `String` \ **Required: True** **Pattern**: `^[a-z]{2}-[a-z]{2}$` The default locale to use for this domain if no valid locale is detected. Must be one of the locales defined in the `locales` object for this domain. ### i18n.localeCookie **Type**: `String` \ **Required: True** The name of the cookie used to persist the user's locale preference across sessions. This cookie stores the actual locale code (e.g., `"en-gb"`), not custom prefixes. #### Locale Inheritance Locale configs are treated in a special way in the Astro integration and will inherit from the main config file e.g. this config ```javascript { domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' }, i18n: { domains: { 'www.acme.com': { locales: { 'en-gb': { commerce: { endpoint: 'https://www.acme.com' }, kv: [ { key: 'acme', namespace: 'config' } ] } }, pathBasedRouting: false, fallbackLocale: 'en-gb' } }, localeCookie: 'locale_V6' } } ``` Will be treated like the below config by the integration. ```javascript { domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' }, i18n: { locales: [ { prefix: 'en-gb', domain: 'www.acme.com', kv: [ { key: 'acme', namespace: 'config' } ], domains: ['www.acme.com'], commerce: { endpoint: 'https://www.acme.com' } } ], fallbackLocale: 'en-gb' } } ``` Notice how the `domains` and `commerce` objects from the top level config are copied into the locale specific config. If a key is present in the locale specific it will **always** take precedence over the same key in the main config and thus act as an override. This inheritance and override behaviour is then applied to nested fields in the main and locale configs. However it does **not** apply to array elements, arrays are inherited or overidden in their entirety. If you want to explicitly opt out of inheriting a particular key from the main config then set that key or its parent as `null` in the locale config. ## Custom Custom keys can also be supplied to the build config, such as environment variables. These values will not affect the configuration of the integration but will be provided on the [altitude global context](#altitude-global-context) at runtime. This is useful for multi tenancy when values need to change based on each tenants config. Further information can be found in the [multi tenancy guide](/docs/astro-integration/v3.0.0/reference/multi-tenancy) # Reference - **JSON Schema v3**: [Full schema definition](https://github.com/THG-AltitudeSiteBuilds/astro-integration/blob/main/config_schemas/schemaV3.json) - **TypeScript Types**: [Type definitions](https://github.com/THG-AltitudeSiteBuilds/astro-integration/blob/main/types/index.ts) - **Migration Guide**: [Upgrading from v2](../guides/migration-v3) - **Path-based Routing**: [Detailed routing guide](./path-based-routing) ------------------------------------------- # File: astro-integration/v3.0.0/reference/global-context.md --- title: Altitude Global Context --- ## Altitude Global Context The integration will provide additional information about the config resolvement at runtime and attach it to `context.locals.altitude`. Please see all available attachments below. ### altitude.runtime.config The build config object the integration has resolved to. For applications using the integration's [localisation](/docs/astro-integration/guides/i18n) solution this will be the locale specific config. ### altitude.runtime.config.domain The domain that the integration has resolved to. ### altitude.runtime.kv.\ The value of [KV](#kv) retrieved using the key provided. This will be attached using the namespace value provided in the KV for the key retrieved.

Internationalisation

These keys will be provided on the altitude namespace for applications that are using the built in i18n solution. Further information can be found [here](/docs/astro-integration/guides/i18n) ### altitude.locale The locale the integration has resolved to from the request. - `en-gb` ### altitude.availableLocales Array containing all the locales a sites config supports. - `['en-gb', 'fr-fr']` ### altitude.localeDomains (deprecated) An object containing the ISO 639-1 code and domain path it corresponds to. - `{'en-gb': 'https://www.example.com', 'fr-fr': 'https://www.example.fr'}` ### altitude.preferredLocale (deprecated) ISO 639-1 code that resolves to the 'prefix' in the i18n section of the build config. If none are provided or invalid, `null` will be returned. ------------------------------------------- # File: astro-integration/v3.0.0/reference/i18n.md --- title: i18n ๐ŸŒŽ description: Serve localised versions of the same website from different global top level domains --- # Internationalisation If you're looking to translate your website to multiple languages, there are a number of factors to consider. 1. **Language Dictionary** - The collection of language strings. Typically in JSON format. 2. **Routing by Language** - Path based rules that dictate which language dictionary and configuration to load. 3. **API Localisation** - A route to request translated data from the API. 4. **Domains** - How to associate a domian to a specific locale. 5. **Previewing Language Strings** - How to preview the object structure that represent a particular string on a website. Used to support non-technical stakeholders with understanding what keys to update. In this guide we will go through how to achieve each of these steps. ## Language Dictionaries ### Ingenuity Properties and Edge KV (Recommended) Using Ingenuity's Properties Service for managing your language dictionary is recommended as this allows non-technical users to manage the language used across the website without a deployment. Ingenuity Properties Service is a UI for managing your language dictionary. It's a list of keys and values. For the purpose of this guide we assume you have access to our Ingenuity Service's. If you do not, you can request access from your Ingenuity Project team. #### Adding Properties For Altitude deployed sites we syncronise the values in the Ingenuity Properties Service to a key-value store that lives with your deployed website. Typically referred to as an Edge KV. This allows for high-performance read operations and for the site to update language entries without requiring a deployment. We will sync any values from Properties Service that begin following the following format: `altitude.` #### Linking Properties Service to your KV :::note This is currently not yet available to all customers. Please contact the Altitude Platform team to enable this feature. ::: #### Reading Properties Once you have linked Properties Service to your KV you can begin to read the KV from your codebase. Refer to the framework guides below on how to read from a KV. If you're utilising our integrations there are helper methods available for i18n. Refer to the guides referenced below. ##### Framework Guides - [Astro: Reading from Edge KVs in Astro](/guides/edge-kv/) - [Astro Integration: Edge KV configuration](/packages/astro-integration/#kv) - [Astro Integration: i18n method](/packages/astro-integration/#i18n) ### Local Dictionary You can establish a local language dictionary in the codebase by creating local JSON files. For ease of access you can apply these in a middleware to make them accessible across your application. If you are using the Astro Integration, a local copy of your language dictionary can alternatively be supplied as part of the configuration setup for KV. This enables local environments to read directly from a local JSON file at runtime. [See more](/packages/astro-integration/#kv) ## Routing by Language :::note Guide coming soon ::: ## Localised Domains Global top level domains or gTLDs provide SEO benefits by providing localised content on a separate domain. The integration provides application owners the flexibility to configure whether valid locales should redirect to an associated gTLD. To enable localised domains, a locale should supply a `domain` within its respective locale config file. The integration will rewrite the underlying request to prefix the locale to the request which will resolve to the specific config file the locale and gTLD of the request is associated with. To mitigate the risk of duplicate content, any requests directly to the prefix that have not been subject to a rewrite will 404. Example request pattern: ```text www.example.com => rewrite => www.example.com/en-gb/ www.example.com/en-gb/ => www.example.com/en-gb/ (404) ``` ### Application setup As well as configuration updates, application owners must amend routing logic within their application. Routes within the application should be nested inside of a [dynamic route](https://docs.astro.build/en/guides/routing/). The param to use for this dynamic route **must be** `locale`. Further to this, to handle invalid locales as part of the request, an additional catch all page should be created at the root of the pages directory to force [on demand rendering](https://docs.astro.build/en/guides/server-side-rendering/#return-a-response-object). If your application already contains a catch all route, this should be moved inside of the dynamic `[locale]` route and the following snippet added to the new catch all route. ```javascript //pages/[...https].astro // new catch all page --- return new Response(null, Astro.response) --- ``` The 404.astro page should remain at the **root** of the pages directory to serve any custom 404 pages. ## API Localisation When switching the language of a site either through path based routing or gTLDs (Global top level domains) it might be desired that the copy and content/products on site also change if they are independently traded. The integration is able to seamlessly achieve API switching by enabling applications to configure custom configs per locale for sites, including defining different API endpoints. This value should be supplied in the `i18n..` locale specific config. ------------------------------------------- # File: astro-integration/v3.0.0/reference/methods.md --- title: Methods --- # Methods The integration provides some useful common functions and patterns available to be used in applications out the box. Methods are attached to the `altitude` namespace and provides storefront owners a layer of abstraction away from verbose core commerce functionality and performance uplifts. ### Quick method reference ```javascript altitude: { i18n: (func, ...args) => String; commerce: { api: async ( operationFields: { operation: String!, variables: Object! }, headers: Object!, options: { apqEnabled: Boolean } ) => Object; }; blog: { api: async ( endpoint: String!, operationFields: { operation: String!, variables: Object, cacheKey: Request! || String!, wafBypass: String!, clientSecret: String!, clientId: String!}, options: { headers: Object } ) => Response; }; cache: { get: async(cacheKey: String! || Request!, operationName: String) => Response || null; set: async(cacheKey: String! || Request!, response: Response!, options: { expiry: Number }) => void; } } ``` ### i18n The i18n method aids with localising copy on site. The function provides flexibility to resolve langauge strings to their values or the object structure to support non-technical stakeholders understand what keys to update. #### valueFunc **Type:** `function() => String` \ **Required: True** An anonymous function that when invoked returns the string that is intended to be evaluated. #### args **Type:** `String` \ **Required: False** Optional argument that will be used to dynamically replace string placeholders in the value of the language string passed, or replace dynamic keys using bracket notation. **Example Use** ```javascript --- const { altitude: { i18n } } = Astro.locals const lang = Astro.locals.altitude.rutime.kv.lang const productTitle = "Vivienne Westwood Logo Ribbed Wool Beanie" const contentKey = "details" ---

{i18n(() => lang.product.promotionalOffer, productTitle)}

// Buy one Vivienne Westwood Logo Ribbed Wool Beanie get one free

{i18n(() => lang.product[contentKey], contentKey)}

//lang.product.details will be the string that is now evaluated ``` **Exposing Keys** The keys used for copy on site can be exposed using headers. This will allow the relevant teams to identify the entry on a site and update its value in Content UI on the fly. To expose the keys an additional request header should be added `Properties-Preview: SHOW-KEYS` ### Commerce API The integration provides out the box commerce api fetching on the server exposing the method as `altitude.commerce.api`. The method enables applications to configure the operation, variables and headers to retrieve commerce data for a given site or tenant. The endpoint the method will use for these calls will be the `commerce.endpoint` supplied in an application or tenants [build config](#configuration). #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: True** The variables to be passed as part of the api call. If no variables are required an empty object should be passed. #### headers **Type:** `Object` \ **Required: True** All required headers to be passed as part of the commerce fetch. No headers are defaulted so all should be provided. #### options.apqEnabled **Type:** `Boolean` \ **Required: False** \ **Default: False** This enables Horizons [Automatic Persisted Queries](https://thehutgroup.github.io/Horizon-Public-Docs/#automated-persisted-queries) feature. #### Return value From version 1.7.0 the api method returns the full response object. Version <=1.6.X The commerce api method will return an Object containing three values: `body`, `duration`, `status`. - `body`: The response from the api call. - `duration`: The duration of the api call in ms. - `status`: The status code of the response. **Example use** ```javascript //c/index.astro const body = await locals.query({ operation: Schema, variables: { handle: pathName, }, customHeaders: { foo: 'bar', }, }) ``` ```javascript //middleware/index.js import { print } from 'graphql' query: async (args) => { const { operation, variables = {}, customHeaders = {} } = args let query if (typeof operation == 'string') { query = operation } else { query = print(operation) } try { const { body, duration, status } = await locals.altitude.commerce.api( { operation: query, variables }, { ...customHeaders, 'Content-Type': 'application/json', 'User-Agent': request.headers.get('User-Agent'), 'X-Altitude-Instance': locals.tenantInstance, // application specific header }, { apqEnabled: false, } ) return body } catch (e) { console.log(e) } } ``` ### Cache API The cache api can be used to enhance the performance of sites by reducing the number of network calls being made as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer. Instead, the response of these calls can be set in cache so future requests can attempt to retrieve the response from cache instead of calling the origin. #### Get ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get a response from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### operationName **Type:** `String` \ **Required: False** \ **Default:** `""` The get cache api function logs out the operation name that has receieved a cache hit for observability. **Example Use** ```javascript let response, cacheKey if (!import.meta.env.DEV) { cacheKey = altitude.createCacheKey(`${horizonEndpoint}/${host}/headerfooter`) response = await altitude.cache.get(cacheKey, 'nav') } ``` #### Set ##### cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to set a response in cache for request lookups. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` ##### response **Type:** `Response` \ **Required: True** The response object to be put into cache to be retrieved for future cache lookups. ##### options.expiry **Type:** `Number` \ **Required: False** \ **Default:** `600` Optional value for how long this response should stay in the cache for in seconds. Defaulted to 600 seconds (10 minutes) **Example Use** ```javascript if (!response) { try { response = await Astro.locals.utils.query({ operation: HeaderFooter, }) if (response.statusText !== 'OK') throw new Error('Error Fetching nav') if (!import.meta.env.DEV) { await altitude.cache.set(cacheKey, response.clone(), { expiry: 600 }) } } catch (e) { console.log(e.message) } } ``` Performance can be improved by using the cache as it reduces load times and avoids repeated API calls, which is especially beneficial for large components which do not change often such as the header and footer of pages. For example, by using the functions described above to first check the cache, and if its empty, to populate the cache once the data has been fetched: ### Blog API The blog api function is used to fetch blog content from a specified `endpoint`. The fetch utilises the Cache API to get and set auth tokens which are sent as a header to reduce the amount of calls to auth service. #### endpoint **Type:** `String` \ **Required: True** The endpoint the integration should use to retrieve blog data. #### operationFields.operation **Type:** `String` \ **Required: True** The operation to be passed to the body as the query. Any parsing of the operation should be done at application level ahead of time. #### operationFields.variables **Type:** `Object` \ **Required: False** The variables to be passed as part of the api call. #### operationFields.cacheKey **Type:** `String || Request` \ **Required: True** The cache key to be used to get and set auth tokens from cache. This value must be unique when in a multi tenancy environment and cache keys can be easily created using the helper function `altitude.createCacheKey(key: String)` #### operationFields.wafBypass **Type:** `String` \ **Required: True** Application specific WAF bypass key, used for auth. #### operationFields.clientSecret **Type:** `String` \ **Required: True** Application specific client secret, used for auth. #### operationFields.clientId **Type:** `String` \ **Required: True** Tenant or Application specific ID, used for auth. #### options.headers **Type:** `Object` \ **Required: False** Any additional headers to be sent as part of the request. `Content-Type: application/json` and `Authorization` are currently defaulted. **Example Use** ```javascript const blogEndpoint = Astro.locals?.tenantConfig?.application?.features?.tesseract?.endpoint let resp try { resp = await altitude.blog.api(blogEndpoint, { operation: TesseractHome, cacheKey: altitude.createCacheKey( `${Astro.locals.tenantConfig.application.horizonEndpoint}/${Astro.locals.host}/blog` ), wafBypass: import.meta.env.WAF_BYPASS, clientSecret: import.meta.env.AUTH_CLIENT_SECRET, clientId:import.meta.env.BLOG_CLIENT_ID }) } catch (e) { console.log(e) } ``` ------------------------------------------- # File: astro-integration/v3.0.0/reference/multi-tenancy.md --- title: Multi-Tenancy description: Serve multiple brands from a single Altitude site. --- Multi-tenancy is a pattern in which multiple brands may be served from the same codebase. It can be used to quickly onboard a new brand, or 'tenant', via a new configuration, as a fast route to scale. To enable multi-tenancy for your site, see the [guide](/docs/astro-integration/v3.0.0/guides/getting-started#multi-tenancy-mode) --- # Multi-tenancy ## Custom Domains Altitude allows multiple [custom domains](https://docs.thgaltitude.com/edge/domains/) to be attached to a single site, giving the feel of a separately hosted site per-tenant. For example `www.myprotein.com` and `www.myvitamins.com` point to the same Altitude deployment. ## Tenant Configuration There are certain properties which will want to vary between tenants to give then a distinct feel, such as: - Styling - Feature flags - API urls, Captcha keys etc. A solution to achieving this is for your application to read these properties from a configuration object which can vary per-tenant. ## Key-Value Stores Altitude KV Stores can be used as a store for tenant configuration, which can be read at request time. ## Determining the Tenant To select the correct tenant config, Altitude uses the request's domain or a special header: - **Production:** The `X-Forwarded-Host` header (set by Fastly) maps the domain to the tenant config. - **Development/local:** The `x-altitude-instance` header specifies the tenant. You can set this header using a tool like [ModHeader](https://modheader.com/) for easy switching. ## Multi-tenancy Stylesheets When sharing a codebase with multiple tenants, being able to differentiate branding colours and typography is essential. However it's equally essential to ensure that the codebase is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and you're not building stylesheets when not necessary. ### Tailwind By utilising [Tailwind Themes](https://tailwindcss.com/docs/theme) alongside a combination of data attributes and CSS variables you can achieve a pattern of having one shared UI but with tenant specific CSS. Example: ```css //main.css @tailwind base; @tailwind components; @tailwind utilities; @layer base { html[data-theme="default"] { --color-esther: 34, 39, 46; --color-maximus: 45, 51, 59; --color-linx: 55, 62, 71; } html[data-theme="neon"] { --color-esther: 20, 61, 42; --color-maximus: 13, 82, 66; --color-linx: 20, 82, 11; } } ``` Within your HTML you then switch to the relevant theme: ```html ``` ##### Alternative: DaisyUI + Tailwind DaisyUI offers a multi-theme configuration out of the box. This can help achieve the capability of creating multiple themes which can be used for creating variety across tenants. [DaisyUI Themes](https://daisyui.com/docs/themes/) [DaisyUI components](https://daisyui.com/components/) :::note Tailwind is a build time dependency and is unaware at the build stage what tenant is active. Therefore you need to consider that the impact of not optimising your theme file(s) efficiently so that minimal CSS changes are required will result in all tenants CSS weight increasing. Using this alongside a speific tenant CSS file that is loaded at runtime (for minor layout changes / adjustments) will help the CSS scale to a larger volume of sites without concerns of shared performance regressions. ::: ### Astro Integration Applications using the Astro Integration can leverage built in mapping to resolve configs to the correct tenant. This is determined at runtime and domains (x-altitude-instance for local development or the domain in prod) will be mapped by matching a value from within a tenants [`domains.variants`](/packages/astro-integration/#domains-options) array. Applications that have a multi tenancy model, will supply an array of configs to the integration function [`altitudeMiddleware`](/packages/astro-integration/#invoke-the-integration). It is recommended that each tenants config is in their own respective file and exported from a single file to keep the configs tidy, sectioned and consistent. ```javascript // /config/index.js import siteOne from "./siteOne"; import siteTwo from "./siteTwo"; import siteThree from "./siteThree"; export default [siteOne, siteTwo, siteThree]; ``` #### Multi Tenancy Config As well as defining tenant specific endpoints and KV keys, the config file allows application owners to extend any site specific values and expose these at runtime. One benefit that the integration allows is the ability to unlock tenant specific environment variables. Tenant specific secrets can be supplied to the build config. The integration exports an `env` function which can be imported and invoked inside of individual build configs. The `env` function takes in the **reference** of the environment variable as an argument not the value. ```js //config/siteOne.js import { env } from '@thg-altitude/astro-integration' export default { domains: { default: "www.example.com", variants: ["www.example.com"], }, ... // exsiting site setup blog: { secret: env('EXAMPLE_SITE_BLOG_SECRET') } }; ``` The value of this environment variable can then be accessed by using the [altitude global context](/packages/astro-integration/#altitude-global-context) `altitude.runtime.config`. ```javascript --- const { altitude, runtime } = Astro.locals // runtime(Production) vs vite development server(Local development) const blogSecret = runtime ? runtime.env[altitude.runtime.config.blog.secret] : import.meta.env[altitude.runtime.config.blog.secret] --- ``` ### Tenant Switching When developing in a multi tenanted application, it may be desired that you are able to switch between tenants on the fly. For local development and DEV/UAT cloudflare environments this is easily achieved by adding a custom HTTP header to the request. The header to be used must be `x-altitude-instance` and the value will determine which tenant to switch to. This value should exist in that tenants config file, listed under the [`domains.variants`](/packages/astro-integration/#domains-options) array. Tenant switching will not be enabled on live domains. Applications that are using [localised domains](/guides/i18n/#localised-domains) will use the domain associated to that locale. ------------------------------------------- # File: astro-integration/v3.0.0/reference/path-based-routing.md --- title: Path Based Routing description: Use one GTLD to serve multiple locales. --- ## Overview This integration supports two i18n routing strategies: 1. **Domain-based routing** - Different domains serve different locales (e.g., `www.example.com`, `www.example.de`) 2. **Path-based routing** - Locale prefix in the URL path (e.g., `www.example.com/en-gb/p/123`, `www.example.com/de-de/p/123`) Path-based routing is ideal when you want to: - Serve multiple languages from a single domain - Simplify domain management and SSL certificates - Allow users to easily switch between languages - Maintain SEO benefits with clear locale-specific URLs ## How It Works ### Locale Detection The system can extract locales from URL paths using either standard locale codes or custom prefixes: 1. **Standard locale pattern**: `[a-z]{2}-[a-z]{2}` (e.g., `en-gb`, `de-de`, `fr-fr`) 2. **Custom prefix pattern**: User-defined URL-safe strings (e.g., `deutsch`, `row`, `en-mt`) When a user visits a URL, the system follows this detection order: 1. **Direct locale match**: If the path starts with a valid locale code or custom prefix, the system uses that locale 2. **Cookie fallback**: If no locale in path, checks for a locale preference cookie 3. **Default fallback**: Uses the configured `fallbackLocale` for the domain ### Configuration Options For full config explanations visit the [config page](./config.md). The key properties needed for path based routing are: | Property | Type | Description | |----------|------|-------------| | `pathBasedRouting` | boolean | Enables path-based routing for the domain | | `fallbackLocale` | string | Default locale to redirect to (format: `xx-xx`) | | `locales` | object | Map of locale codes to locale configurations | | `localeCookie` | string | Name of cookie used to store user's locale preference | | `customPrefix` | string | Optional custom URL prefix for each locale (within locale config) | ### Custom Prefix Configuration ```javascript i18n: { domains: { 'www.example.com': { pathBasedRouting: true, fallbackLocale: 'en-us', locales: { 'en-us': { commerce: { endpoint: 'https://api.example.com/en/graphql' } }, 'fr-fr': { customPrefix: 'francais', commerce: { endpoint: 'https://api.example.com/fr/graphql' } }, 'de-de': { customPrefix: 'german', commerce: { endpoint: 'https://api.example.com/de/graphql' } } } } }, localeCookie: 'locale_V6' } ``` ## Features ### Automatic Redirects The system automatically redirects users to appropriate localized URLs: **With standard locale codes:** - **No locale in path + valid cookie**: `/page` โ†’ `/de-de/page` (if cookie has `de-de`) - **No locale in path + no cookie**: `/page` โ†’ `/en-gb/page` (using fallbackLocale) - **Invalid locale in path**: `/invalid/page` โ†’ `/en-gb/page` (using fallbackLocale) **With custom prefixes:** - **No locale in path + valid cookie**: `/page` โ†’ `/deutsch/page` (if cookie has `de-de` with `customPrefix: "deutsch"`) - **No locale in path + no cookie**: `/page` โ†’ `/english/page` (using fallbackLocale with custom prefix) - **Invalid prefix in path**: `/invalid/page` โ†’ `/english/page` (using fallbackLocale) - **Consistency**: The same locale preference works across different configurations - **Flexibility**: You can change custom prefixes without invalidating user cookies - **Compatibility**: The system can switch between standard and custom prefix URLs seamlessly ### Multi-tenant Support Path-based routing works with both single and multi-tenant configurations: - **Single tenant**: One configuration serves all domains - **Multi-tenant**: Different domains can have different locale sets and routing strategies ### Mixed tenant configs supported One tenant can utilise both path-based and domain based routing. The config setup for this would look like: ```js // config/site.js import { fetchIcon } from "@thg-altitude/utils"; import siteConfigFile from "../local/tenants/siteone.json"; import siteLangFile from "../local/lang/siteone.en_gb.properties.json"; import siteConfigDeFile from "../local/tenants/siteoneDe.json"; import siteLangDeFile from "../local/lang/siteone.de_de.properties.json"; import siteConfigAtFile from "../local/tenants/siteoneAt.json"; import siteLangAtFile from "../local/lang/siteone.de_at.properties.json"; export default { domains: ["www.siteone.com", "www.siteone.de"], tenantInstance: "siteone", commerce: { endpoint: "https://horizon-api.www.siteone.com/en-gb/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigFile, }, { key: "siteone.en_gb.properties", namespace: "lang", local: siteLangFile, }, ], icons: { search: await fetchIcon("lucide", "search"), left: await fetchIcon("lucide", "chevron-left"), right: await fetchIcon("lucide", "chevron-right"), }, i18n: { domains: { "www.siteone.com": { locales: { "en-gb": { icons: { flag: await fetchIcon("circle-flags", "gb"), }, }, }, pathBasedRouting: false, fallbackLocale: "de-de", }, "www.siteone.de": { locales: { "de-de": { icons: { flag: await fetchIcon("circle-flags", "de"), }, commerce: { endpoint: "https://horizon-api.www.siteone.com/de-de/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigDeFile, }, { key: "siteone.de_de.properties", namespace: "lang", local: siteLangDeFile, }, ], }, "de-at": { icons: { flag: await fetchIcon("circle-flags", "at"), }, commerce: { endpoint: "https://horizon-api.www.siteone.com/de-at/graphql", }, kv: [ { key: "siteone", namespace: "config", local: siteConfigAtFile, }, { key: "siteone.de_at.properties", namespace: "lang", local: siteLangAtFile, }, ], }, }, pathBasedRouting: true, fallbackLocale: "de-de", }, }, localeCookie: "locale_V6", }, }; ``` ## Advanced Configuration Examples ### Combining Domain and Path-Based Routing You can use both routing strategies within the same configuration: ```javascript export default { domains: ["www.example.com", "www.example.de"], commerce: { endpoint: "https://api.global.com/graphql" }, i18n: { domains: { // Global domain with path-based routing "www.example.com": { pathBasedRouting: true, fallbackLocale: "en-us", locales: { "en-us": { commerce: { endpoint: ... } }, "es-es": { commerce: { endpoint: ... } }, "fr-fr": { commerce: { endpoint: ... } } } }, // German domain with domain-based routing "www.example.de": { pathBasedRouting: false, fallbackLocale: "de-de", locales: { "de-de": { commerce: { endpoint: ... } } } }, }, localeCookie: "user_locale_v3" } } ``` The result of this config would be two different domains supporting 4 locales: - `www.example.com/en-us/` - `www.example.com/es-es/` - `www.example.com/fr-fr/` - `www.example.de` ## Schema Validation The configuration is validated against JSON Schema v3 with these constraints: - **Domain-based routing**: Limited to 1 locale per domain when `pathBasedRouting: false` - **Path-based routing**: Unlimited locales allowed when `pathBasedRouting: true` - **Locale format**: Must match `^[a-z]{2}-[a-z]{2}$` pattern - **Custom prefix format**: Must be URL-safe strings when provided - **Required fields**: domains, commerce.endpoint, i18n.localeCookie, pathBasedRouting, fallbackLocale - **KV namespaces**: Only "config", "lang", or "session" allowed ------------------------------------------- # File: browser-components/v0.2.0/components/accordion.mdx --- title: Accordion description: A primitive accordion --- import { Aside } from "@components/docs"; import ComponentPreview from "@components/docs/ComponentPreview.astro"; import SummaryAccordion from "@components/docs/browser-components/v0.2.0/accordion/SummaryAccordion.html"; import NamedSummaryAccordion from "@components/docs/browser-components/v0.2.0/accordion/NamedSummaryAccordion.html"; # Accordion ## Overview Accordions provide a way to provide users with a large quantity of information, without visually overwhelming them. ## Example Usage ### Summary Accordion This accordion has a basic open close mechanic from the details/summary tag used. ### Named Summary Accordion Building on the initial Summary accordion, the use of the name attribute on the details tag allows only one section to be open at a time. {/* ### Style Override Example This is identical in functionality to the previous accordion, but demonstrates the styling flexibility of the accordion css variables
*/} ## CSS Variables The accordion system can be customized using CSS variables: ### Layout and Sizing | Variable | Default Value | Description | | ----------------------------- | ------------- | ---------------------------------------------------------------------- | | `--accordion-gap` | `0em` | Horizontal space between sections | | `--accordion-wrapper-padding` | `0px` | Horizontal padding between the border of the accordion and its content | ### Trigger Configuration | Variable | Default Value | Description | | --------------------------- | ------------- | ------------------------------------------------------ | | `--accordion-inner-margin` | `0px` | Shared value between trigger and content to align text | | `--accordion-inner-padding` | `1em 1.5em` | Shared value between trigger and content to align text | ### Icon Configuration | Variable | Default Value | Description | | -------------------------------------- | ----------------------------------------- | ----------------------------------------------- | | `--accordion-icon-expanded-transform` | `rotate(180deg)` | Transformation to apply when panel is expanded | | `--accordion-icon-transition` | `all var(--duration-speed-quick) ease-in` | Transition to apply for icon animations | | `--accordion-icon-collapsed-transform` | `rotate(0deg)` | Transformation to apply when panel is collapsed | ### Borders and Styling | Variable | Default Value | Description | | -------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `--accordion-radius` | `none` | Rounding on outer borders | | `--accordion-section-radius` | `none` | Rounding on section borders | | `--accordion-border` | `none` | Border thickness around the accordion | | `--accordion-divider-width` | `2px` | Width of divider lines between accordion sections | | `--accordion-closed-divider-lightness` | `20%` | Lightness of dividers between closed accordion sections, as a percentage of the skins border colour blended with its background colour | ## Accessibility Using the pattern of the html details and summary tags allows the open/close interactivity of the accordion. To make it WCAG compliant and to activate some css based on aria changes, some Javascript will need to be implemented in the final version. ### Current Implementation Features #### Structure and Labeling > Each trigger has a role of button to indicate it's interactivity to screen readers. > Every accordion section has aria to link the trigger / heading of the section to its content #### Keyboard Support The current implementation provides: > Tab navigation between focusable elements > Focus indicators on interactive elements > Space/Enter keys to open/close the dropdown #### Screen Reader Support #### ARIA State Management Update the following ARIA states dynamically: - `aria-expanded` This should be toggled with javascript - `aria-controls=ID` link the top level element around the trigger for an accordion panel - Use role=region / `aria-labelledby` on the accordion content panel div, to demonstrate the link to the button controlling visibility This is not a typical implementation of aria and accessibility. It is known to work against Android talkback and Apple VoiceOver screen-readers. If a fully WCAG compliant version is needed, please refer to the WCAGAccordion example. This will need Javascript implementation to make it interactive. #### Keyboard Navigation These are met by default, but after any Javascript additions to the accordion, it should be confirmed that they are still met: - Tab moves focus to the next focusable element - Shift / Tab moves focus to the previous focusable element - Enter / Space expands a collapsed tab ------------------------------------------- # File: browser-components/v0.2.0/components/alert.mdx --- title: Alert description: A platform messaging element --- import ComponentPreview from "@components/docs/ComponentPreview.astro"; import SuccessAlert from "@components/docs/browser-components/v0.2.0/alerts/SuccessAlert.html"; import InfoAlert from "@components/docs/browser-components/v0.2.0/alerts/InfoAlert.html"; import ErrorAlert from "@components/docs/browser-components/v0.2.0/alerts/ErrorAlert.html"; # Alert ## Overview Alerts can be used to highlight information to the user. ## Example Usage Through the use of the skin system, we can achieve any number of styles of alert. Below are examples using the success, attention and error skins. ### Success Alert ### Attention Alert ### Error Alert ## CSS Variables ### General Alert Variables | Variable | Default Value | Description | | ------------------------ | -------------------- | ---------------------------- | | `--alert-padding` | `0.5em` | Padding around the content | | `--alert-border-default` | `none` | Allow a border, deafult none | | `--alert-border-radius` | `var(--radius-site)` | Border radius | ### Alert Icon Variables | Variable | Default Value | Description | | ------------------------- | ------------- | ------------------------------------ | | `--alert-icon-padding-x` | `0rem` | Vertical padding around the content | | `--alert-icon-padding-y` | `0.25rem` | Horizonal padding around the content | | `--alert-icon-height` | `24px` | Default icon height | | `--alert-icon-min-height` | `24px` | Prevent shrinking of icon | | `--alert-icon-width` | `24px` | Default icon width | | `--alert-icon-min-width` | `24px` | Prevent shrinking of icon | ## Accessibility In these examples, the svg icon has the aria-hidden attribute so that the icon is not visible to the Accessibility tree. This is appropriate as these icons are not interactive and are considered decorative, as the paragraph content contains the information needing to be relayed to the screenreader. {/* */} ------------------------------------------- # File: browser-components/v0.2.0/components/breadcrumbs.mdx --- title: Breadcrumbs description: Navigation breadcrumbs --- import ComponentPreview from '@components/docs/ComponentPreview.astro'; import Breadcrumbs from '@components/docs/browser-components/v0.2.0/breadcrumbs/Breadcrumbs.html'; # Breadcrumbs ## Overview Breadcrumbs help users understand their position on a website and navigate to previous pages by showing a hierarchical trail back to the homepage. ## Example Usage The breadcrumbs component uses semantic HTML and CSS styled separators. Each breadcrumb item is clickable except for the current page, which is marked with `aria-current`. The component supports customization through CSS variables for spacing, typography, and arrow styling. ### Style Override Examples The separators between breadcrumb items are created using CSS styling of a single div and can be customised to create different visual styles using the CSS variables provided. ```css .breadcrumb-line-separator { --breadcrumb-icon-border-top: 1px; --breadcrumb-icon-height: 1rem; --breadcrumb-icon-width: 0; --breadcrumb-icon-rotate: 0; --breadcrumb-icon-border-top: 0; --breadcrumb-gap-left: 0.75rem; --breadcrumb-gap-right: 0.75rem; } ``` ```css .breadcrumb-slash-separator { --breadcrumb-icon-border-top: 1px; --breadcrumb-icon-height: 1rem; --breadcrumb-icon-width: 0.25rem; --breadcrumb-icon-rotate: 15deg; --breadcrumb-icon-border-top: 0; --breadcrumb-gap-left: 0.25rem; --breadcrumb-gap-right: 0.4rem; --breadcrumb-font-weight-last: var(--font-weight-normal); } ``` ## CSS Variables | Variable | Default Value | Description | | ---------------------------------| ------------------------------| ------------------------------------------------------ | | `--breadcrumb-padding` | `0 0.5rem` | Padding around the content | | `--breadcrumb-font-size` | `var(--text-sm)` | Font size for text | | `--breadcrumb-font-weight` | `var(--font-weight-light)` | Font weight for all text apart from the last breadcrumb| | `--breadcrumb-font-weight-last` | `var(--font-weight-semi-bold)`| Font weight for last breadcrumb text | | `--breadcrumb-icon-height` | `0.375rem` | Height of breadcrumb separators | | `--breadcrumb-icon-width` | `0.375rem` | Width of breadcrumb separators | | `--breadcrumb-icon-border-top` | `1px` | Width of top border of separator | | `--breadcrumb-icon-border-right` | `1px` | Width of right border of separator | | `--breadcrumb-gap-left` | `0.5rem` | Gap between left side of separator and breadcrumb text | | `--breadcrumb-gap-right` | `0.75rem` | Gap between right side of separator and breadcrumb text| | `--breadcrumb-icon-rotate` | `45deg` | Rotation of breadcrumb separator | ### Accessibility #### Implemented Accessibility Features - [x] Focus outline visible enclosing the whole element on keyboard navigation - [x] `aria-current` label included on last breadcrumb - [x] Use of semantic HTML with `