Astro package
The @booking/astro-embed package embeds the booking flow in Astro sites, including sites on Cloudflare Workers. It uses the same widget bundle as the JavaScript widget and the WordPress plugin, so the booking flow behaves the same.
The package gives you:
- an Astro integration that adds the widget to every page
.astrocomponents that output the widget placeholders- a Paytrail return page
- an optional same-origin API proxy, so you don't need a CORS allow-list entry
- helpers for your Content-Security-Policy
Set up the integration
Add the integration to astro.config.mjs:
import { defineConfig } from 'astro/config';
import sessiqEmbed from '@booking/astro-embed';
export default defineConfig({
integrations: [
sessiqEmbed({
config: {
apiUrl: 'https://api.example.com',
tenantId: 'your-tenant-id',
siteId: 'your-site-id',
},
}),
],
});
The integration:
- adds one inline boot script to every page. It creates
window.dataLayerif it is missing, setswindow.sessiqConfig, picks the English or Finnish interface text from the page's<html lang>, loads the widget and callsSessiq.init. - serves the widget files from your own site under
/sessiq/, with a version number so browsers pick up new builds. - calls
Sessiq.rescan()after every View Transitions navigation (astro:page-load), so embeds work with<ClientRouter />and never render twice. - logs a clear console error if the widget has not loaded a few seconds after page load, for example because a CSP blocked it.
Set lang on every page (<html lang="fi">), not once in a shared layout. The interface text, dates, times and prices all follow it. To force one language everywhere, set config.locale.
Options
| Option | Default | Description |
|---|---|---|
config | — | Required. The widget configuration. Same fields as Sessiq.init, see below. |
defaultLocale | en | Interface language when a page has no <html lang>: en or fi. |
proxy | false | The same-origin API proxy. true is short for { enabled: true }. As an object: enabled (default false) and basePath (default /api/sessiq, must start with /). |
paytrailReturnRoute | /sessiq/booking-return | Path of the Paytrail return page. Must start with /. false turns the page off. |
diagnosticTimeoutMs | 4000 | Milliseconds after page load before the "widget did not load" console error. |
Unknown options are rejected at build time.
config field | Validation |
|---|---|
apiUrl | Required, a URL |
tenantId | Required, not empty |
siteId, apiKey, locale | Optional, not empty |
bookingBaseUrl, termsUrl, privacyUrl | Optional, a URL |
primaryColor, buttonColor, backgroundColor, surfaceColor, surfaceAltColor, textColor, borderColor | Optional, any CSS colour string |
debug | Optional, boolean |
See JavaScript widget → Configuration for what each field does.
Components
Import a component and place it on a page. Components output plain HTML placeholders. Don't add a client:* directive; the widget renders into the placeholder itself. Every component also accepts class, which is added to the placeholder element.
---
import SessiqBookingWidget from '@booking/astro-embed/components/SessiqBookingWidget.astro';
import SessiqProductGrid from '@booking/astro-embed/components/SessiqProductGrid.astro';
---
<SessiqBookingWidget productId="your-product-id" defaultView="week" />
<SessiqProductGrid siteId="your-site-id" columns={4} orderBy="name" />
| Component | Props (default) | Placeholder |
|---|---|---|
SessiqBookingWidget | productId (required), showSteps (true), compact (false), successUrl, cancelUrl, defaultView ('month') | sessiq-booking-widget-container |
SessiqCalendar | productId (required), months (2), showPrices (true), minPartySize (1), maxPartySize (20), defaultView ('month') | sessiq-calendar-widget |
SessiqProductCard | productId (required), showName (true), showPrice (true), showDescription (true), buttonText, useModal (false), theme, bare (false) | sessiq-product-card |
SessiqProductGrid | siteId, limit (12), columns (3), orderBy, showName (true), showPrice (true), showDescription (true), buttonText, useModal (false), theme, bare (false) | sessiq-products-grid |
SessiqProductListing | siteId, limit (12), columns (3), showPrice (true), showDescription (true) | server-rendered cards, see below |
SessiqCourses | siteId, columns (3), orderBy, showSteps (true), successUrl, cancelUrl, theme, bare (false) | sessiq-courses-grid |
SessiqCourseProduct | productId (required), siteId, showSteps (true), successUrl, cancelUrl, theme, bare (false) | sessiq-course-product-container |
SessiqCourse | courseId (required), showSteps (true), compact (false), successUrl, cancelUrl | sessiq-course-widget-container |
SessiqWaiver | bookingId, productId, signatureMode ('both'). At least one ID is required, or the build fails. | sessiq-waiver-standalone |
SessiqCampaign | slug (omit for the single active campaign), returnUrl (this page) | sessiq-campaign |
There is no component for the location-first booking flow. To use it on an Astro page, write its placeholder directly, as described in JavaScript widget → Location-first booking flow:
<div class="sessiq-party-selector-container" data-product-tag="birthday"></div>
Server-rendered product listing
SessiqProductListing fetches products on the server and outputs plain, crawlable HTML, which helps search engines and avoids layout shift. The widget then turns each product into an interactive card. It needs the Cloudflare adapter, the page must opt out of prerendering (export const prerender = false), and it reads the Worker variables listed under Same-origin API proxy. On a static build it falls back to SessiqProductGrid.
The theme prop
SessiqProductCard, SessiqProductGrid, SessiqCourses and SessiqCourseProduct accept a theme object. Each field becomes an inline CSS variable on that one placeholder, so it overrides the page-wide colours for that embed only. It matches the WordPress style attributes. Values are cleaned the same way: only letters, digits, spaces and # , . % ( ) _ - are kept.
theme field | CSS variable |
|---|---|
buttonColor | --sessiq-button |
buttonTextColor | --sessiq-button-text |
buttonHoverColor | --sessiq-button-hover |
buttonHoverTextColor | --sessiq-button-hover-text |
buttonShadow | --sessiq-button-shadow |
buttonHoverShadow | --sessiq-button-hover-shadow |
buttonRadius | --sessiq-button-radius |
buttonPadding | --sessiq-button-padding |
buttonBorder | --sessiq-button-border |
buttonFontSize | --sessiq-button-font-size |
buttonWeight | --sessiq-button-weight |
buttonLetterSpacing | --sessiq-button-letter-spacing |
buttonUppercase | --sessiq-button-transform: uppercase when true |
primaryColor | --sessiq-primary |
textColor | --sessiq-text |
mutedColor | --sessiq-text-muted |
backgroundColor | --sessiq-background |
borderColor | --sessiq-border |
radius | --sessiq-radius |
bare removes the default card background, border, shadow and padding.
<SessiqProductGrid
siteId="your-site-id"
bare
theme={{ primaryColor: '#0af', buttonRadius: '999px', buttonUppercase: true }}
/>
See Theming for how these combine with your branding.
Paytrail return page
When a customer pays with a Finnish bank through Paytrail, the browser leaves your site and comes back. The integration adds a page at /sessiq/booking-return (change it with paytrailReturnRoute). The page loads the widget, which shows the booking result. If the URL has a productId query parameter, the page also shows the booking flow for that product, restored from the customer's session; without it, the page says the booking has been processed.
The booking itself is confirmed by Sessiq on the server, so a customer who closes the tab still has a valid booking.
By default the widget returns the customer to the page they booked on, which also works. Use the return page if you want a dedicated landing page: point successUrl / cancelUrl at it. If your booking pages already handle the return, set paytrailReturnRoute: false.
Same-origin API proxy
By default the browser calls the Sessiq API directly, so your site's origin must be on the site's Allowed widget origins list (see Before you start).
With the proxy on, the browser calls your own site instead and a Worker route forwards the request:
sessiqEmbed({
config: { apiUrl: 'https://api.example.com', tenantId: 'your-tenant-id' },
proxy: true, // or { enabled: true, basePath: '/api/sessiq' }
});
- The integration adds a route at
<basePath>/[...path]and points the widget'sapiUrlat it. - The tenant ID and API key are not put in the page. The Worker adds them to each request from its environment.
- Only API paths under
/api/v1/are forwarded, and only theGET,HEAD,POST,PUT,PATCH,DELETEandOPTIONSmethods. Other requests are refused. - Only the
Content-Type,AcceptandAccept-Languagerequest headers are passed on. Responses are never cached. - No CORS and no allow-list entry are needed.
The proxy needs an SSR adapter such as @astrojs/cloudflare; the build warns if there is none. Set these Worker environment variables:
| Variable | Required | Purpose |
|---|---|---|
SESSIQ_API_URL | Yes | The Sessiq API origin. |
SESSIQ_TENANT_ID | Yes | Your tenant ID, sent to the API with each request. |
SESSIQ_API_KEY | No | A public API key, sent with each request. |
Content-Security-Policy
The integration does not change your site's CSP. The booking flow loads Stripe Elements and redirects to Paytrail, so your policy must allow them. Import the helpers from @booking/astro-embed/csp and merge the Sessiq part into your own policy:
import {
buildSessiqCsp,
mergeCsp,
serializeCsp,
} from '@booking/astro-embed/csp';
const sessiqCsp = buildSessiqCsp({ apiOrigin: 'https://api.example.com' });
// With the proxy on, the API is same-origin:
// buildSessiqCsp({ apiOrigin: "'self'" });
const policy = serializeCsp(mergeCsp(mySiteCsp, sessiqCsp));
// Send it as the Content-Security-Policy header.
| Export | Description |
|---|---|
buildSessiqCsp(opts) | Returns the directives the booking flow needs. Options: apiOrigin (required), imageOrigins (extra image hosts), allowInlineStyles (default true), allowInlineScripts (default true). |
mergeCsp(base, add) | Merges two directive maps and removes duplicate sources. |
serializeCsp(map) | Turns a directive map into a header value. |
STRIPE_SCRIPT_ORIGIN, STRIPE_API_ORIGIN, STRIPE_FRAME_ORIGINS, PAYTRAIL_ORIGINS | The fixed Stripe and Paytrail origins. |
buildSessiqCsp returns these directives:
| Directive | Sources |
|---|---|
default-src | 'self' |
script-src | 'self', https://js.stripe.com, 'unsafe-inline' (unless allowInlineScripts: false) |
connect-src | 'self', the API origin, https://api.stripe.com, https://services.paytrail.com, https://pay.paytrail.com |
frame-src | https://js.stripe.com, https://hooks.stripe.com, https://services.paytrail.com, https://pay.paytrail.com |
img-src | 'self', data:, the API origin, any imageOrigins |
style-src | 'self', 'unsafe-inline' (unless allowInlineStyles: false) |
If you turn off allowInlineScripts or allowInlineStyles, give the inline boot script and styles a nonce through Astro's CSP support. If you use a tag manager, add its origins to your policy yourself.
Without the package
You can also embed the widget in Astro by hand, as on any other site (see JavaScript widget). Every Sessiq <script> tag must then carry is:inline. Astro otherwise bundles and reorders scripts, and window.Sessiq ends up undefined.
<script is:inline src="/sessiq/sessiq.min.js"></script>
<script is:inline>
window.Sessiq.init({ apiUrl: 'https://api.example.com', tenantId: 'your-tenant-id' });
</script>