diff --git a/frontend/src/routes/geschichten/+page.server.ts b/frontend/src/routes/geschichten/+page.server.ts
new file mode 100644
index 00000000..66e3fdf1
--- /dev/null
+++ b/frontend/src/routes/geschichten/+page.server.ts
@@ -0,0 +1,36 @@
+import { error } from '@sveltejs/kit';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ url, fetch }) => {
+ const api = createApiClient(fetch);
+ const personId = url.searchParams.get('personId') ?? undefined;
+ const documentId = url.searchParams.get('documentId') ?? undefined;
+
+ const [listResult, personResult] = await Promise.all([
+ api.GET('/api/geschichten', {
+ params: {
+ query: {
+ status: 'PUBLISHED',
+ personId,
+ documentId
+ }
+ }
+ }),
+ personId
+ ? api.GET('/api/persons/{id}', { params: { path: { id: personId } } })
+ : Promise.resolve(null)
+ ]);
+
+ if (!listResult.response.ok) {
+ const code = (listResult.error as unknown as { code?: string })?.code;
+ throw error(listResult.response.status, getErrorMessage(code));
+ }
+
+ return {
+ geschichten: listResult.data ?? [],
+ personFilter: personResult && personResult.response.ok ? personResult.data! : null,
+ documentFilter: documentId ?? null
+ };
+};
diff --git a/frontend/src/routes/geschichten/+page.svelte b/frontend/src/routes/geschichten/+page.svelte
new file mode 100644
index 00000000..d85aeab3
--- /dev/null
+++ b/frontend/src/routes/geschichten/+page.svelte
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+ {#if data.personFilter}
+
+ {:else}
+
+ {/if}
+
+
+ {#if showPersonPicker}
+
+ {/if}
+
+
+ {#if data.geschichten.length === 0}
+
+ {#if data.personFilter}
+ {m.geschichten_empty_for_person({ name: filterName })}
+ {:else}
+ {m.geschichten_empty_no_filter()}
+ {/if}
+
+ {:else}
+
+ {/if}
+
diff --git a/frontend/src/routes/geschichten/[id]/+page.server.ts b/frontend/src/routes/geschichten/[id]/+page.server.ts
new file mode 100644
index 00000000..bb4b3314
--- /dev/null
+++ b/frontend/src/routes/geschichten/[id]/+page.server.ts
@@ -0,0 +1,16 @@
+import { error } from '@sveltejs/kit';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/geschichten/{id}', {
+ params: { path: { id: params.id } }
+ });
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+ return { geschichte: result.data! };
+};
diff --git a/frontend/src/routes/geschichten/[id]/+page.svelte b/frontend/src/routes/geschichten/[id]/+page.svelte
new file mode 100644
index 00000000..e96af176
--- /dev/null
+++ b/frontend/src/routes/geschichten/[id]/+page.svelte
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {@html sanitized}
+
+
+
+
+ {#if g.persons && g.persons.length > 0}
+
+
+ {m.geschichten_persons_section()}
+
+
+
+ {/if}
+
+
+ {#if g.documents && g.documents.length > 0}
+
+
+ {m.geschichten_documents_section()}
+
+
+
+ {/if}
+
+
+ {#if data.canBlogWrite}
+
+ {/if}
+
diff --git a/frontend/src/routes/geschichten/[id]/edit/+page.server.ts b/frontend/src/routes/geschichten/[id]/edit/+page.server.ts
new file mode 100644
index 00000000..7d397921
--- /dev/null
+++ b/frontend/src/routes/geschichten/[id]/edit/+page.server.ts
@@ -0,0 +1,20 @@
+import { error, redirect } from '@sveltejs/kit';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ params, fetch, parent }) => {
+ const layout = await parent();
+ if (!layout.canBlogWrite) {
+ throw redirect(303, `/geschichten/${params.id}`);
+ }
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/geschichten/{id}', {
+ params: { path: { id: params.id } }
+ });
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+ return { geschichte: result.data! };
+};
diff --git a/frontend/src/routes/geschichten/[id]/edit/+page.svelte b/frontend/src/routes/geschichten/[id]/edit/+page.svelte
new file mode 100644
index 00000000..80bf0bf7
--- /dev/null
+++ b/frontend/src/routes/geschichten/[id]/edit/+page.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+ {m.btn_edit()}: {data.geschichte.title}
+
+
+ {#if errorMessage}
+
+ {errorMessage}
+
+ {/if}
+
+
+
diff --git a/frontend/src/routes/geschichten/new/+page.server.ts b/frontend/src/routes/geschichten/new/+page.server.ts
new file mode 100644
index 00000000..66f748e8
--- /dev/null
+++ b/frontend/src/routes/geschichten/new/+page.server.ts
@@ -0,0 +1,33 @@
+import { redirect } from '@sveltejs/kit';
+import { createApiClient } from '$lib/api.server';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ url, fetch, parent }) => {
+ const layout = await parent();
+ if (!layout.canBlogWrite) {
+ throw redirect(303, '/geschichten');
+ }
+
+ const api = createApiClient(fetch);
+ const personId = url.searchParams.get('personId');
+ const documentId = url.searchParams.get('documentId');
+
+ const [personResult, documentResult] = await Promise.all([
+ personId
+ ? api.GET('/api/persons/{id}', { params: { path: { id: personId } } })
+ : Promise.resolve(null),
+ documentId
+ ? api.GET('/api/documents/{id}', { params: { path: { id: documentId } } })
+ : Promise.resolve(null)
+ ]);
+
+ // Silently ignore 404/403 to avoid leaking entity existence on unknown IDs.
+ const initialPersons =
+ personResult && personResult.response.ok && personResult.data ? [personResult.data] : [];
+ const initialDocuments =
+ documentResult && documentResult.response.ok && documentResult.data
+ ? [documentResult.data]
+ : [];
+
+ return { initialPersons, initialDocuments };
+};
diff --git a/frontend/src/routes/geschichten/new/+page.svelte b/frontend/src/routes/geschichten/new/+page.svelte
new file mode 100644
index 00000000..57f9cf49
--- /dev/null
+++ b/frontend/src/routes/geschichten/new/+page.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
{m.geschichten_new_button()}
+
+ {#if errorMessage}
+
+ {errorMessage}
+
+ {/if}
+
+
+