diff --git a/apps/web/e2e/pwa.spec.ts b/apps/web/e2e/pwa.spec.ts
index dd03a27..797b2d6 100644
--- a/apps/web/e2e/pwa.spec.ts
+++ b/apps/web/e2e/pwa.spec.ts
@@ -33,6 +33,24 @@ test('installs the app shell and requests persistent storage', async ({ page })
expect(await page.evaluate(() => Reflect.get(globalThis, '__inkfinitePersistRequested'))).toBe(
true
);
+
+ await page.getByRole('button', { name: 'About Inkfinite' }).click();
+ const displayedVersion = (await page.getByText(/^Version v/).textContent())?.replace(
+ 'Version ',
+ ''
+ );
+ const cachedVersions = await page.evaluate(async () =>
+ (await caches.keys())
+ .filter((name) => name.startsWith('inkfinite-'))
+ .flatMap((name) => {
+ try {
+ return [JSON.parse(name.slice('inkfinite-'.length)).display as string];
+ } catch {
+ return [];
+ }
+ })
+ );
+ expect(cachedVersions).toContain(displayedVersion);
});
test('creates, reopens, and exports a board while offline', async ({ editor, page, context }) => {
diff --git a/apps/web/src/lib/docs/DocsShell.svelte b/apps/web/src/lib/docs/DocsShell.svelte
index 56d36d6..417444b 100644
--- a/apps/web/src/lib/docs/DocsShell.svelte
+++ b/apps/web/src/lib/docs/DocsShell.svelte
@@ -6,6 +6,7 @@
import DocsPageNavigation from './DocsPageNavigation.svelte';
import DocsSeo from './DocsSeo.svelte';
import DocsSidebar from './DocsSidebar.svelte';
+ import SiteFooter from './SiteFooter.svelte';
import SiteHeader from './SiteHeader.svelte';
import DocsToc from './DocsToc.svelte';
import { getAdjacentDocs } from './content';
@@ -52,6 +53,8 @@
+
+
diff --git a/apps/web/src/lib/version.ts b/apps/web/src/lib/version.ts
new file mode 100644
index 0000000..b31c2c3
--- /dev/null
+++ b/apps/web/src/lib/version.ts
@@ -0,0 +1,39 @@
+import { version as VERSION } from '$app/environment';
+
+const REPOSITORY_URL = 'https://github.com/stormlightlabs/inkfinite';
+
+/** Git-derived version details embedded in each web build. */
+export type BuildVersion = { display: string; commit: string; version: string };
+
+const fallbackVersion: BuildVersion = { display: 'v0.0.0', commit: 'unknown', version: 'v0.0.0' };
+
+function isBuildVersion(value: unknown): value is BuildVersion {
+ if (!value || typeof value !== 'object') return false;
+
+ const candidate = value as Record;
+ return (
+ typeof candidate.display === 'string' &&
+ typeof candidate.commit === 'string' &&
+ typeof candidate.version === 'string'
+ );
+}
+
+function parseBuildVersion(value: string): BuildVersion {
+ try {
+ const parsed: unknown = JSON.parse(value);
+ if (isBuildVersion(parsed)) return parsed;
+ } catch {
+ // Development and fallback builds can expose an ordinary SvelteKit version string.
+ }
+
+ return fallbackVersion;
+}
+
+/** Version details for the current web build. */
+export const buildVersion = parseBuildVersion(VERSION);
+
+/** Source page for the commit used by the current web build. */
+export const buildCommitUrl =
+ buildVersion.commit === 'unknown'
+ ? REPOSITORY_URL
+ : `${REPOSITORY_URL}/commit/${buildVersion.commit}`;
diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte
index 05b486c..06d149b 100644
--- a/apps/web/src/routes/+page.svelte
+++ b/apps/web/src/routes/+page.svelte
@@ -4,6 +4,7 @@
import Seo from '$lib/Seo.svelte';
import { site } from '$lib/site';
import '$lib/docs/docs.css';
+ import SiteFooter from '$lib/docs/SiteFooter.svelte';
import SiteHeader from '$lib/docs/SiteHeader.svelte';
import { Icon } from '$ui';
@@ -287,6 +288,7 @@
+