Skip to content

Conversation

@dipankarmaikap
Copy link
Contributor

@dipankarmaikap dipankarmaikap commented Jan 8, 2026

This PR introduces persistent server-side data support for Astro live preview, adds a new getPayload API and enables per-route and editor-controlled live preview behaviour.

Astro live preview can now preserve server-side data across preview updates, enabling more efficient and flexible data handling. It also gives developers and editors finer control over when live preview updates should occur.


What’s new

Persistent server-side data

  • Adds StoryblokServerData.astro, a new component used to embed sanitized server-side data.
  • Embedded server data survives live preview updates, preventing unnecessary refetching.
  • Server data is automatically sanitized to prevent malicious data injection.
---
import StoryblokServerData from '@storyblok/astro/StoryblokServerData.astro';

const users = await getUsers();
---

<StoryblokServerData users={users} />

This component embeds sanitized server-side data into the page so it can be reused across live preview updates without refetching.


New getPayload API

A new getPayload function is introduced to support persistent server-side data without changing the existing getLiveStory API.

getLiveStory remains unchanged and fully backward compatible.

The new API returns both live preview story data and persistent server-side data in a single call.

Example:

interface ServerData {
  users?: User[];
}

interface MyStory extends ISbStoryData {
  content: { myField: string };
}

const { slug } = Astro.params;

const payload = await getPayload<ServerData, MyStory>({
  locals: Astro.locals,
});

The returned payload contains:

  • story – live preview story data
  • serverData – persistent server-side data

Live preview control per route

Developers can disable live preview behavior on a per-route basis using a meta tag. This is useful for routes where live preview updates are not desired and a full page reload is preferred.

Example:

{slug === 'test' && (
  <meta name="storyblok-live-preview" content="disabled" />
)}

When live preview is disabled for a route:

  • Live preview updates on change are prevented
  • The page falls back to standard SSR behavior with a full reload

Editor-controlled live preview via events

This release also enables editor-facing control over live preview updates by listening to an existing browser event. Teams can build custom UI controls, such as a checkbox or toggle, to allow editors to opt in or out of live preview updates.

Example:

document.addEventListener('storyblok-live-preview-updating', (event: Event) => {
  if (!checkbox?.checked) {
    event.preventDefault();
    console.log('Live preview update prevented by user preference');
  } else {
    console.log(
      'Live preview is updating...',
      (event as CustomEvent).detail
    );
  }
});

Spinner and loading state considerations

If this feature is used together with a loading spinner or editor-side loading indicator, the spinner logic must account for prevented events. Otherwise, the spinner may remain visible even when live preview updates are cancelled.

Recommended pattern:

document.addEventListener('storyblok-live-preview-updating', (event) => {
  if (spinner) {
    spinner.style.display = 'block';

    queueMicrotask(() => {
      if (event.defaultPrevented && spinner) {
        spinner.style.display = 'none';
      }
    });
  }
});

This adjustment is only required if consumers actively prevent live preview updates.

isEditorRequest helper

A new isEditorRequest utility is now exposed for consumers. It allows detecting whether a request originates from the Storyblok editor, which is useful for rendering editor-only UI or widgets that should not appear on the public site.


Security

  • Introduces an internal sanitizeJSON utility with test coverage.
  • Used internally by StoryblokServerData to sanitize embedded server data.

Fixes


Breaking changes

None.

Existing integrations using getLiveStory continue to work without modification. The new getPayload API is fully opt-in.


Note

Enhances Astro live preview with persistent server data and fine-grained control.

  • Adds StoryblokServerData.astro to embed sanitized server data (via new sanitizeJSON) that persists across live preview updates
  • Introduces getPayload() to access story and serverData; keeps getLiveStory() (deprecated) for backward compatibility
  • Extends live preview client handler to: honor <meta name="storyblok-live-preview" content="disabled">, dispatch cancelable storyblok-live-preview-updating and storyblok-live-preview-updated events, and post serverData back on updates
  • Adds isEditorRequest() util and updates middleware to validate editor requests and store { story, serverData } in locals
  • Exposes new APIs/exports and typings; updates build to ship StoryblokServerData.astro
  • Adds tests for sanitizeJSON and isEditorRequest; playground examples for spinner and editor toggle
  • Updates release tooling/docs to support pre-release branches (alpha, beta, next) and improve script checks

Written by Cursor Bugbot for commit cfbc155. This will update automatically on new commits. Configure here.

dipankarmaikap and others added 2 commits January 8, 2026 13:33
@dipankarmaikap dipankarmaikap changed the title Feat/astro live preview feat!(astro): astro live preview now support custom payload Jan 8, 2026
@dipankarmaikap dipankarmaikap changed the title feat!(astro): astro live preview now support custom payload feat(astro)!: astro live preview now support custom payload Jan 8, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@alexjoverm alexjoverm self-requested a review January 8, 2026 08:50
Copy link
Contributor

@alexjoverm alexjoverm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great work @dipankarmaikap! Just a few quick fixes to go through, other than that looks on point 👍

Additional one - can you also link the issues the PR solves in the the PR description?
Closes #95, closes #96

Change payload variable from extraData to serverData. Sanitization added for the user-provided server payload
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 8, 2026

Open in StackBlitz

@storyblok/astro

npm i https://pkg.pr.new/@storyblok/astro@404

storyblok

npm i https://pkg.pr.new/storyblok@404

@storyblok/eslint-config

npm i https://pkg.pr.new/@storyblok/eslint-config@404

@storyblok/js

npm i https://pkg.pr.new/@storyblok/js@404

storyblok-js-client

npm i https://pkg.pr.new/storyblok-js-client@404

@storyblok/management-api-client

npm i https://pkg.pr.new/@storyblok/management-api-client@404

@storyblok/nuxt

npm i https://pkg.pr.new/@storyblok/nuxt@404

@storyblok/react

npm i https://pkg.pr.new/@storyblok/react@404

@storyblok/region-helper

npm i https://pkg.pr.new/@storyblok/region-helper@404

@storyblok/richtext

npm i https://pkg.pr.new/@storyblok/richtext@404

@storyblok/svelte

npm i https://pkg.pr.new/@storyblok/svelte@404

@storyblok/vue

npm i https://pkg.pr.new/@storyblok/vue@404

commit: cfbc155

r="70"></circle></svg
>
</div>
<LivePreviewSpinner />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems with this you also addressed #97

I am just not sure if there is a way to have live preview already disabled on initialization or should I load frontend app and dispatch something?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wait, it is just a spinner, either shown or hidden, so the toggling is not part of this functionality, isnt it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have a separate PR coming for #97

@@ -0,0 +1,71 @@
---
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: can this be removed?

let story: ISbStoryData | null = null;
if (Astro && Astro.locals._storyblok_preview_data) {
story = Astro.locals._storyblok_preview_data;
interface Payload<T = unknown> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think we could make T more specific and rename the function to match its new functionality:

type StoryblokServerData = typeof Astro.props

export async function getPayload<ServerData extends StoryblokServerData = StoryblokServerData, Story extends ISbStoryData = ISbStoryData>({
  locals: { _storyblok_preview_data: { story, serverData } }
}: { locals: { _storyblok_preview_data?: { serverData?: ServerData; story?: Story; } } }) {
  return { story, serverData };
}

// Correct usage:
interface ServerData {
  users?: User[];
}

interface MyStory {
  content: { myField: string }
}

const payload = await getPayload<ServerData /* optional */, MyStory /* optional */>(Astro);
const story = payload.story ?? /* ... */
const users = payload.serverData?.users ?? /* ... */

// Usage with type error:
type ServerData = string;

const payload = await getPayload<ServerData /* Type error! */>(Astro);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had the thought that introducing getPayload might open the possibility of keeping the original getLiveStory making this a backwards compatible change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be great since it avoids any breaking changes for existing users. Should we also consider adding a deprecated warning so users can take action on their own?

const event = new CustomEvent<T>(name, { detail, cancelable: true });
document.dispatchEvent(event);
return event;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What was your reasoning for introducing a new function instead of adding a third options param to dispatchStoryblokEvent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, this is internal-only and used once. I’ll switch it to the existing function with an additional parameter. Thanks.

Removed `dispatchCancelableStoryblokEvent` and added a new param in `dispatchStoryblokEvent`
introduced `isEditorRequest` function for both internal and end user
&& url.searchParams.has('_storyblok_c');
// First do a check if its coming from within storyblok
const editorRequest
= isEditorRequest(new URL(request.url));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Middleware validation now requires additional parameter

Medium Severity

The middleware now uses isEditorRequest which requires three URL parameters (_storyblok, _storyblok_c, and _storyblok_tk[space_id]), but the previous validation only required two (_storyblok and _storyblok_c). This stricter validation could silently break live preview functionality if editor requests don't always include _storyblok_tk[space_id]. When validation fails, the middleware won't set _storyblok_preview_data, causing the page to fall back to API fetching instead of using live preview data. The PR states "No breaking changes" but this behavioral change could affect existing setups.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dipankarmaikap this functionality would be ideal to have it in the JS SDK. There are some checks there but are hardcoded. Ideally, it would be encapsulated in a utility that can be also exported so the Astro SDK or any other SDK can reuse it

https://github.com/storyblok/monoblok/blob/main/packages/js/src/index.ts#L93

I'll leave it up to you if you want do it as part of this PR, or separately later in the future (which is perfectly fine)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants