mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
feat: show a raw app's document.title in the page tab (#11747)
* feat: show a raw app's document.title in the page tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: drop unrelated formatting changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: restore the page title only while the tab shows the app's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b7b3306195
commit
5153a5e7b8
@@ -1015,7 +1015,6 @@ fn raw_app_wrapper_html(secret: &str) -> String {
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>App</title>
|
||||
<link rel="stylesheet" href="./__SECRET__.css" />
|
||||
<script>
|
||||
(function () {
|
||||
@@ -1067,6 +1066,17 @@ fn raw_app_wrapper_html(secret: &str) -> String {
|
||||
history.pushState = function () { _ps.apply(this, arguments); notifyParent(); };
|
||||
history.replaceState = function () { _rs.apply(this, arguments); notifyParent(); };
|
||||
|
||||
// The parent shows the app's document.title in its tab. This document has no
|
||||
// <title> of its own: document.title reads the first one, so a placeholder would
|
||||
// shadow the one a framework (React 19, react-helmet) appends.
|
||||
var lastTitle = '';
|
||||
new MutationObserver(function () {
|
||||
var t = document.title;
|
||||
if (t === lastTitle) return;
|
||||
lastTitle = t;
|
||||
try { window.parent.postMessage({ type: 'windmill:title', title: t }, '*'); } catch (_) {}
|
||||
}).observe(document.head, { childList: true, subtree: true, characterData: true });
|
||||
|
||||
// ctx handshake: the embedding parent hands us the user context (and any
|
||||
// persisted storage) before we evaluate the bundle, so `window.ctx` is set
|
||||
// synchronously when the bundle runs. The bundle <script> is injected only
|
||||
|
||||
Generated
+2
@@ -6681,6 +6681,8 @@ Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! }
|
||||
|
||||
**Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`.
|
||||
|
||||
**Set the browser tab title with \`document.title = '...'\`** (or a \`<title>\` the framework renders into \`<head>\`). The app has no \`index.html\` of its own, so a \`<title>\` written there is never served; the deployed app's page shows whatever \`document.title\` holds, and follows it when it changes.
|
||||
|
||||
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
|
||||
|
||||
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
* (publish flow) needs it to read the app's DOM, which is only possible on
|
||||
* the unsandboxed path. */
|
||||
oniframe?: (iframe: HTMLIFrameElement | undefined) => void
|
||||
/** Mirror the app's own route into the page URL's hash, and start from it. Off where
|
||||
* the app is embedded in a page that is not its own — an AI session preview tab —
|
||||
* since there the hash belongs to that page and to whichever app was opened first. */
|
||||
/** Mirror the app's own route into the page URL's hash, and start from it, and its
|
||||
* `document.title` into the page's. Off where the app is embedded in a page that is
|
||||
* not its own — an AI session preview tab — since there the hash and title belong to
|
||||
* that page and to whichever app was opened first. */
|
||||
syncHashToUrl?: boolean
|
||||
}
|
||||
|
||||
@@ -241,6 +242,19 @@
|
||||
}
|
||||
})
|
||||
|
||||
// The page's own title, put back when the app clears its title or unmounts. Only
|
||||
// while the tab still shows the app's: anything else is the page's newer title.
|
||||
let pageTitle = ''
|
||||
let appTitle: string | undefined = undefined
|
||||
function setAppTitle(title: string) {
|
||||
if (document.title !== appTitle) pageTitle = document.title
|
||||
appTitle = title.trim().slice(0, 200) || pageTitle
|
||||
document.title = appTitle
|
||||
}
|
||||
onMount(() => () => {
|
||||
if (appTitle !== undefined && document.title === appTitle) document.title = pageTitle
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
function handleMessage(event: MessageEvent) {
|
||||
const data = event.data
|
||||
@@ -287,6 +301,8 @@
|
||||
if (syncHashToUrl && window.location.hash !== newHash) {
|
||||
history.replaceState(null, '', newHash || window.location.pathname)
|
||||
}
|
||||
} else if (data?.type === 'windmill:title') {
|
||||
if (syncHashToUrl) setAppTitle(typeof data.title === 'string' ? data.title : '')
|
||||
} else if (runnerFailed) {
|
||||
refuseRequest(event)
|
||||
} else if (!runner) {
|
||||
|
||||
@@ -294,7 +294,6 @@ export function unsandboxedRawAppHtml(
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>App</title>
|
||||
<!-- Before the app's own stylesheet, so a bundle that wants a margin can still set one: the
|
||||
browser default of 8px would otherwise frame every app with a gap it never asked for. -->
|
||||
<style>html, body { margin: 0; }</style>
|
||||
@@ -319,6 +318,21 @@ export function unsandboxedRawAppHtml(
|
||||
history.pushState = function () { _ps.apply(this, arguments); notifyParent(); };
|
||||
history.replaceState = function () { _rs.apply(this, arguments); notifyParent(); };
|
||||
setTimeout(notifyParent, 0);
|
||||
|
||||
// The parent shows the app's document.title in its tab. This document has no
|
||||
// <title> of its own: document.title reads the first one, so a placeholder would
|
||||
// shadow the one a framework (React 19, react-helmet) appends.
|
||||
var lastTitle = '';
|
||||
new MutationObserver(function () {
|
||||
var t = document.title;
|
||||
if (t === lastTitle) return;
|
||||
lastTitle = t;
|
||||
try {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'windmill:title', title: t }, '*');
|
||||
}
|
||||
} catch (_) {}
|
||||
}).observe(document.head, { childList: true, subtree: true, characterData: true });
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -955,6 +955,8 @@ Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! }
|
||||
|
||||
**Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`.
|
||||
|
||||
**Set the browser tab title with \`document.title = '...'\`** (or a \`<title>\` the framework renders into \`<head>\`). The app has no \`index.html\` of its own, so a \`<title>\` written there is never served; the deployed app's page shows whatever \`document.title\` holds, and follows it when it changes.
|
||||
|
||||
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
|
||||
|
||||
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
|
||||
|
||||
@@ -311,6 +311,8 @@ Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`.
|
||||
|
||||
**Never replace the entrypoint with a bare component** (`export default function App() { ... }` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts `App` into `#root`.
|
||||
|
||||
**Set the browser tab title with `document.title = '...'`** (or a `<title>` the framework renders into `<head>`). The app has no `index.html` of its own, so a `<title>` written there is never served; the deployed app's page shows whatever `document.title` holds, and follows it when it changes.
|
||||
|
||||
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
|
||||
|
||||
### Generated bindings (`wmill.d.ts` / `wmill.ts`)
|
||||
|
||||
@@ -70,6 +70,8 @@ Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`.
|
||||
|
||||
**Never replace the entrypoint with a bare component** (`export default function App() { ... }` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts `App` into `#root`.
|
||||
|
||||
**Set the browser tab title with `document.title = '...'`** (or a `<title>` the framework renders into `<head>`). The app has no `index.html` of its own, so a `<title>` written there is never served; the deployed app's page shows whatever `document.title` holds, and follows it when it changes.
|
||||
|
||||
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
|
||||
|
||||
### Generated bindings (`wmill.d.ts` / `wmill.ts`)
|
||||
|
||||
Reference in New Issue
Block a user