diff --git a/docs-app/app/app-ssr.ts b/docs-app/app/app-ssr.ts
new file mode 100644
index 000000000..4a7090ebd
--- /dev/null
+++ b/docs-app/app/app-ssr.ts
@@ -0,0 +1,27 @@
+import PageTitleService from 'ember-page-title/services/page-title';
+import Application from 'ember-strict-application-resolver';
+
+import Router from './router.ts';
+
+export default class SsrApp extends Application {
+ modules = {
+ ...import.meta.glob('./router.ts', { eager: true }),
+ ...import.meta.glob('./templates/**/*.{gjs,gts,md}', { eager: true }),
+ ...import.meta.glob('./routes/**/*.{gjs,gts,js,ts,md}', { eager: true }),
+ './router': Router,
+ './services/page-title': PageTitleService,
+ };
+}
+
+/**
+ * Exported so vite-ember-ssr's worker can await it with a timeout per
+ * render (`settledTimeout`, default 10s). Demos with `ReactiveImage`
+ * register `waitForPromise` waiters that never resolve under Node
+ * (happy-dom doesn't fire `` onload), so an unbounded `settled()`
+ * inside `app.visit` would hang the whole render forever.
+ */
+export { settled } from '@ember/test-helpers';
+
+export function createSsrApp() {
+ return SsrApp.create({ autoboot: false });
+}
diff --git a/docs-app/app/boot.ts b/docs-app/app/boot.ts
new file mode 100644
index 000000000..2b50b8949
--- /dev/null
+++ b/docs-app/app/boot.ts
@@ -0,0 +1,14 @@
+import { shouldRehydrate } from 'vite-ember-ssr/client';
+
+import Application from './app.ts';
+import environment from './config/environment.ts';
+
+if (shouldRehydrate()) {
+ const app = Application.create({ ...environment.APP, autoboot: false });
+
+ void app.visit(window.location.pathname + window.location.search, {
+ _renderMode: 'rehydrate',
+ });
+} else {
+ Application.create(environment.APP);
+}
diff --git a/docs-app/app/routes/application.ts b/docs-app/app/routes/application.ts
index 90677cacf..28c5c560a 100644
--- a/docs-app/app/routes/application.ts
+++ b/docs-app/app/routes/application.ts
@@ -33,7 +33,7 @@ export default class Application extends Route {
});
await Promise.all([
- setupTabster(this),
+ import.meta.env?.SSR ? Promise.resolve() : setupTabster(this),
setupKolay(this, {
// This won't work, because the compiler can't find the element to rendedr in to.
// remarkPlugins: [
diff --git a/docs-app/index.html b/docs-app/index.html
index 3003a4016..bbc1df1fa 100644
--- a/docs-app/index.html
+++ b/docs-app/index.html
@@ -13,13 +13,10 @@
as="font"
rel="font/woff2"
/>
+