How to redirect the page from the backend
Intro#
Redirecting to another page is a common task that can be done from the backend using the datastar-execute-script
SSE event.
Goal#
Our goal is to indicate to the user that they will be redirected, wait 3 seconds, and then redirect them to /guide
, all from the backend.
Demo#
Steps#
We’ll place a data-on-click
attribute on a button and use the @get
action to send a GET
request to the backend. We’ll include an empty indicator div
to show the user that they will be redirected.
<button data-on-click="@get('/endpoint')">
Click to be redirected from the backend
</button>
<div id="indicator"></div>
We’ll set up our backend to first send a datastar-merge-fragments
event with a populated indicator fragment, then wait 3 seconds, and finally send a datastar-execute-script
SSE event to execute the JavaScript required to redirect the page.
event: datastar-execute-script
data: script window.location.href = "/guide"
Here’s how it might look using the SDKs.
(require
'[starfederation.datastar.clojure.api :as d*]
'[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]]
'[some.hiccup.library :refer [html]])
(defn handle [ring-request]
(->sse-response ring-request
{:on-open
(fn [sse]
(d*/merge-fragment! sse
(html [:div#indicator "Redirecting in 3 seconds..."]))
(Thread/sleep 3000)
(d*/execute-script! sse "window.location = \"/guide\"")
(d*/close-sse! sse)}))
using StarFederation.Datastar.DependencyInjection;
app.MapGet("/redirect", async (IDatastarServerSentEventService sse) =>
{
await sse.MergeFragmentsAsync("""<div id="indicator">Redirecting in 3 seconds...</div>""");
await Task.Delay(TimeSpan.FromSeconds(3));
await sse.ExecuteScriptAsync("""window.location = "/guide";""");
});
import (
"time"
datastar "github.com/starfederation/datastar/sdk/go"
)
sse := datastar.NewSSE(w, r)
sse.MergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`)
time.Sleep(3 * time.Second)
sse.ExecuteScript(`
window.location = "/guide"
`)
import ServerSentEventGenerator
import ServerSentEventGenerator.Server.Snap -- or whatever is appropriate
send (withDefaults mergeFragments "<div id=\"indicator\">Redirecting in 3 seconds...</div>")
threadDelay (3 * 1000 * 1000)
send (withDefaults executeScript "window.location = \"/guide\"")
use starfederation\datastar\ServerSentEventGenerator;
$sse = new ServerSentEventGenerator();
$sse->mergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`);
sleep(3);
$sse->executeScript(`
window.location = "/guide"
`);
datastar = Datastar.new(request:, response:)
datastar.stream do |sse|
sse.merge_fragments '<div id="indicator">Redirecting in 3 seconds...</div>'
sleep 3
sse.execute_script 'window.location = "/guide"'
end
use datastar::prelude::*;
use async_stream::stream;
use core::time::Duration;
Sse(stream! {
yield MergeFragments::new("<div id='indicator'>Redirecting in 3 seconds...</div>").into();
tokio::time::sleep(core::time::Duration::from_secs(3)).await;
yield ExecuteScript::new("window.location = '/guide'").into();
});
import { createServer } from "node:http";
import { ServerSentEventGenerator } from "../npm/esm/node/serverSentEventGenerator.js";
function delay(milliseconds: number) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
const server = createServer(async (req, res) => {
ServerSentEventGenerator.stream(req, res, async (sse) => {
sse.mergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`);
await delay(3000);
sse.executeScript(`
window.location = "/guide"
`);
});
});
const datastar = @import("datastar").httpz;
const std = @import("std");
var sse = try datastar.ServerSentEventGenerator.init(res);
sse.mergeFragments("<div id='indicator'>Redirecting in 3 seconds...</div>", .{});
std.Thread.sleep(std.time.ns_per_s * 3);
sse.executeScript("window.location = '/guide'", .{});
Note that in Firefox, if a redirect happens within a script
tag then the URL is replaced, rather than pushed, meaning that the previous URL won’t show up in the back history (or back/forward navigation).
To work around this, you can wrap the redirect in a setTimeout
function call. See issue #529 for reference.
(require
'[starfederation.datastar.clojure.api :as d*]
'[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]]
'[some.hiccup.library :refer [html]])
(defn handle [ring-request]
(->sse-response ring-request
{:on-open
(fn [sse]
(d*/merge-fragment! sse
(html [:div#indicator "Redirecting in 3 seconds..."]))
(Thread/sleep 3000)
(d*/execute-script! sse
"setTimeout(() => window.location = \"/guide\"")
(d*/close-sse! sse))}))
using StarFederation.Datastar.DependencyInjection;
app.MapGet("/redirect", async (IDatastarServerSentEventService sse) =>
{
await sse.MergeFragmentsAsync("""<div id="indicator">Redirecting in 3 seconds...</div>""");
await Task.Delay(TimeSpan.FromSeconds(3));
await sse.ExecuteScriptAsync("""setTimeout(() => window.location = "/guide");""");
});
import (
"time"
datastar "github.com/starfederation/datastar/sdk/go"
)
sse := datastar.NewSSE(w, r)
sse.MergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`)
time.Sleep(3 * time.Second)
sse.ExecuteScript(`
setTimeout(() => window.location = "/guide")
`)
import ServerSentEventGenerator
import ServerSentEventGenerator.Server.Snap -- or whatever is appropriate
send (withDefaults mergeFragments "<div id=\"indicator\">Redirecting in 3 seconds...</div>")
threadDelay (3 * 1000 * 1000)
send (withDefaults executeScript "window.location = \"/guide\"")
use starfederation\datastar\ServerSentEventGenerator;
$sse = new ServerSentEventGenerator();
$sse->mergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`);
sleep(3);
$sse->executeScript(`
setTimeout(() => window.location = "/guide")
`);
datastar = Datastar.new(request:, response:)
datastar.stream do |sse|
sse.merge_fragments '<div id="indicator">Redirecting in 3 seconds...</div>'
sleep 3
sse.execute_script <<~JS
setTimeout(() => {
window.location = '/guide'
})
JS
end
use datastar::prelude::*;
use async_stream::stream;
use core::time::Duration;
Sse(stream! {
yield MergeFragments::new("<div id='indicator'>Redirecting in 3 seconds...</div>").into();
tokio::time::sleep(core::time::Duration::from_secs(3)).await;
yield ExecuteScript::new("setTimeout(() => window.location = '/guide')").into();
});
import { createServer } from "node:http";
import { ServerSentEventGenerator } from "../npm/esm/node/serverSentEventGenerator.js";
function delay(milliseconds: number) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
const server = createServer(async (req, res) => {
ServerSentEventGenerator.stream(req, res, async (sse) => {
sse.mergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`);
await delay(3000);
sse.executeScript(`
setTimeout(() => window.location = "/guide")
`);
});
});
const datastar = @import("datastar").httpz;
const std = @import("std");
var sse = try datastar.ServerSentEventGenerator.init(res);
sse.mergeFragments("<div id='indicator'>Redirecting in 3 seconds...</div>", .{});
std.Thread.sleep(std.time.ns_per_s * 3);
sse.executeScript("setTimeout(() => window.location = '/guide')", .{});
Some SDKs provide a helper method that automatically wraps the statement in a setTimeout
function call, so you don’t have to worry about doing so (you’re welcome!).
(require
'[starfederation.datastar.clojure.api :as d*]
'[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response]]
'[some.hiccup.library :refer [html]])
(defn handler [ring-request]
(->sse-response ring-request
{:on-open
(fn [sse]
(d*/merge-fragment! sse
(html [:div#indicator "Redirecting in 3 seconds..."]))
(Thread/sleep 3000)
(d*/redirect! sse "/guide")
(d*/close-sse! sse))}))
using StarFederation.Datastar.DependencyInjection;
using StarFederation.Datastar.Scripts;
app.MapGet("/redirect", async (IDatastarServerSentEventService sse) =>
{
await sse.MergeFragmentsAsync("""<div id="indicator">Redirecting in 3 seconds...</div>""");
await Task.Delay(TimeSpan.FromSeconds(3));
await sse.Redirect("/guide");
});
import (
"time"
datastar "github.com/starfederation/datastar/sdk/go"
)
sse := datastar.NewSSE(w, r)
sse.MergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`)
time.Sleep(3 * time.Second)
sse.Redirect("/guide")
use starfederation\datastar\ServerSentEventGenerator;
$sse = new ServerSentEventGenerator();
$sse->mergeFragments(`
<div id="indicator">Redirecting in 3 seconds...</div>
`);
sleep(3);
$sse->location('/guide');
datastar = Datastar.new(request:, response:)
datastar.stream do |sse|
sse.merge_fragments '<div id="indicator">Redirecting in 3 seconds...</div>'
sleep 3
sse.redirect '/guide'
end
const datastar = @import("datastar").httpz;
const std = @import("std");
var sse = try datastar.ServerSentEventGenerator.init(res);
sse.mergeFragments("<div id='indicator'>Redirecting in 3 seconds...</div>", .{});
std.Thread.sleep(std.time.ns_per_s * 3);
sse.redirect("/guide", .{});
Conclusion#
Redirecting to another page can be done from the backend thanks to the ability to execute JavaScript on the frontend using the datastar-execute-script
SSE event.