diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f717e83ca6..91137d4ee1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -364,6 +364,10 @@ importers:
version: 4.0.16(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@25.0.1)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2)
svelte:
+ dependencies:
+ '@crates-io/api-client':
+ specifier: workspace:*
+ version: link:../packages/crates-io-api-client
devDependencies:
'@chromatic-com/storybook':
specifier: 4.1.3
diff --git a/svelte/package.json b/svelte/package.json
index d82ad4e24e..a21d4acb2c 100644
--- a/svelte/package.json
+++ b/svelte/package.json
@@ -21,6 +21,9 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
+ "dependencies": {
+ "@crates-io/api-client": "workspace:*"
+ },
"devDependencies": {
"@chromatic-com/storybook": "4.1.3",
"@eslint/compat": "2.0.0",
diff --git a/svelte/src/lib/components/frontpage/CrateLists.stories.svelte b/svelte/src/lib/components/frontpage/CrateLists.stories.svelte
new file mode 100644
index 0000000000..f7f3ab3f41
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/CrateLists.stories.svelte
@@ -0,0 +1,91 @@
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/CrateLists.svelte b/svelte/src/lib/components/frontpage/CrateLists.svelte
new file mode 100644
index 0000000000..a5d4680fd2
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/CrateLists.svelte
@@ -0,0 +1,141 @@
+
+
+
+
+ {#snippet item(crate: Crate, index: number)}
+
+ {/snippet}
+
+
+
+ {#snippet item(crate: Crate, index: number)}
+
+ {/snippet}
+
+
+
+ {#snippet item(crate: Crate, index: number)}
+
+ {/snippet}
+
+
+
+ {#snippet item(crate: Crate, index: number)}
+
+ {/snippet}
+
+
+
+ {#snippet item(keyword: Keyword)}
+
+ {/snippet}
+
+
+
+ {#snippet item(category: Category)}
+
+ {/snippet}
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ErrorState.stories.svelte b/svelte/src/lib/components/frontpage/ErrorState.stories.svelte
new file mode 100644
index 0000000000..2c642b9c24
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ErrorState.stories.svelte
@@ -0,0 +1,15 @@
+
+
+ alert('Retry clicked!') }} />
+
+
diff --git a/svelte/src/lib/components/frontpage/ErrorState.svelte b/svelte/src/lib/components/frontpage/ErrorState.svelte
new file mode 100644
index 0000000000..f762281d47
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ErrorState.svelte
@@ -0,0 +1,38 @@
+
+
+
+ Unfortunately something went wrong while loading the crates.io summary data. Feel free to try again, or let the
+ crates.io team
+ know if the problem persists.
+
+
+
+ Try Again
+ {#if isLoading}
+
+ {/if}
+
+
+
diff --git a/svelte/src/lib/components/frontpage/HeroButtons.stories.svelte b/svelte/src/lib/components/frontpage/HeroButtons.stories.svelte
new file mode 100644
index 0000000000..d2d6170b91
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/HeroButtons.stories.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/svelte/src/lib/components/frontpage/HeroButtons.svelte b/svelte/src/lib/components/frontpage/HeroButtons.svelte
new file mode 100644
index 0000000000..48d9a80750
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/HeroButtons.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/IntroBlurb.stories.svelte b/svelte/src/lib/components/frontpage/IntroBlurb.stories.svelte
new file mode 100644
index 0000000000..1e692ecbe5
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/IntroBlurb.stories.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/IntroBlurb.svelte b/svelte/src/lib/components/frontpage/IntroBlurb.svelte
new file mode 100644
index 0000000000..c01dc0b48a
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/IntroBlurb.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+ Instantly publish your crates and install them. Use the API to interact and find out more information about
+ available crates. Become a contributor and enhance the site with your work.
+
+
+
+
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ListItem.stories.svelte b/svelte/src/lib/components/frontpage/ListItem.stories.svelte
new file mode 100644
index 0000000000..e0466b456f
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ListItem.stories.svelte
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ListItem.svelte b/svelte/src/lib/components/frontpage/ListItem.svelte
new file mode 100644
index 0000000000..5b03cfb079
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ListItem.svelte
@@ -0,0 +1,89 @@
+
+
+
+
+
+
{title}
+ {#if subtitle}
{subtitle}
{/if}
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ListItemPlaceholder.stories.svelte b/svelte/src/lib/components/frontpage/ListItemPlaceholder.stories.svelte
new file mode 100644
index 0000000000..63f2dd2a53
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ListItemPlaceholder.stories.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ListItemPlaceholder.svelte b/svelte/src/lib/components/frontpage/ListItemPlaceholder.svelte
new file mode 100644
index 0000000000..45e0255c0b
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ListItemPlaceholder.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
+ {#if withSubtitle}
+
+ {/if}
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/ListSection.svelte b/svelte/src/lib/components/frontpage/ListSection.svelte
new file mode 100644
index 0000000000..62f206059d
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/ListSection.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ {#if !items}
+ {#each { length: 10 } as _, i (i)}
+
+ {/each}
+ {:else}
+ {#each items as it, index (it.id)}
+ {@render item(it, index)}
+ {/each}
+ {/if}
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/StatsValue.stories.svelte b/svelte/src/lib/components/frontpage/StatsValue.stories.svelte
new file mode 100644
index 0000000000..f01a88c5a4
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/StatsValue.stories.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/svelte/src/lib/components/frontpage/StatsValue.svelte b/svelte/src/lib/components/frontpage/StatsValue.svelte
new file mode 100644
index 0000000000..1e4c7160cd
--- /dev/null
+++ b/svelte/src/lib/components/frontpage/StatsValue.svelte
@@ -0,0 +1,52 @@
+
+
+
+ {value}
+ {label}
+
+
+
+
diff --git a/svelte/src/routes/+page.svelte b/svelte/src/routes/+page.svelte
index 1a0164136a..f259ddd9f9 100644
--- a/svelte/src/routes/+page.svelte
+++ b/svelte/src/routes/+page.svelte
@@ -1,4 +1,38 @@
-Welcome to SvelteKit
-
- Visit svelte.dev/docs/kit to read the documentation
-
+
+
+
+
+{#await data.summary}
+
+ {#if isFirstLoad}
+
+
+ {:else}
+
+
+ {/if}
+{:then summary}
+
+
+{:catch _error}
+
+
+{/await}
diff --git a/svelte/src/routes/+page.ts b/svelte/src/routes/+page.ts
new file mode 100644
index 0000000000..bd4bf7f323
--- /dev/null
+++ b/svelte/src/routes/+page.ts
@@ -0,0 +1,38 @@
+import type { operations } from '@crates-io/api-client';
+
+import { browser } from '$app/environment';
+import { createClient } from '@crates-io/api-client';
+
+type SummaryResponse = operations['get_summary']['responses']['200']['content']['application/json'];
+
+let cachedSummary: SummaryResponse | undefined;
+
+/**
+ * Load function to fetch summary data for the page.
+ *
+ * The summary data is cached on the client side to avoid redundant network requests
+ * and is streamed to the page instead of waiting for the entire data to be fetched
+ * before rendering.
+ */
+export async function load({ fetch }) {
+ const client = createClient({ fetch });
+
+ return { summary: fetchSummary(client) };
+}
+
+async function fetchSummary(client: ReturnType): Promise {
+ if (browser && cachedSummary) {
+ return cachedSummary;
+ }
+
+ const response = await client.GET('/api/v1/summary');
+ if (response.error) {
+ throw new Error('Failed to fetch summary data');
+ }
+
+ if (browser) {
+ cachedSummary = response.data;
+ }
+
+ return response.data;
+}
diff --git a/svelte/src/routes/page.svelte.spec.ts b/svelte/src/routes/page.svelte.spec.ts
deleted file mode 100644
index 8069a8bc1e..0000000000
--- a/svelte/src/routes/page.svelte.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { page } from 'vitest/browser';
-
-import Page from './+page.svelte';
-
-describe('/+page.svelte', () => {
- it('should render h1', async () => {
- render(Page);
-
- const heading = page.getByRole('heading', { level: 1 });
- await expect.element(heading).toBeInTheDocument();
- });
-});