create-shapio 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/LICENSE +201 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +311 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +37 -0
  6. package/templates/astro/.env.example +10 -0
  7. package/templates/astro/README.md +31 -0
  8. package/templates/astro/astro.config.mjs +50 -0
  9. package/templates/astro/gitignore +4 -0
  10. package/templates/astro/package.json +31 -0
  11. package/templates/astro/public/favicon.svg +9 -0
  12. package/templates/astro/public/robots.txt +3 -0
  13. package/templates/astro/scripts/incrementalBuild.ts +142 -0
  14. package/templates/astro/scripts/lib/admin.ts +76 -0
  15. package/templates/astro/scripts/lib/callback.ts +46 -0
  16. package/templates/astro/scripts/lib/content.ts +462 -0
  17. package/templates/astro/scripts/lib/png.ts +47 -0
  18. package/templates/astro/scripts/reportStatus.ts +40 -0
  19. package/templates/astro/scripts/seed.ts +396 -0
  20. package/templates/astro/scripts/smoke.ts +127 -0
  21. package/templates/astro/scripts/smokeHttp.ts +162 -0
  22. package/templates/astro/shapio/components/callToAction.json +85 -0
  23. package/templates/astro/shapio/components/feature.json +46 -0
  24. package/templates/astro/shapio/components/featureGrid.json +51 -0
  25. package/templates/astro/shapio/components/gallery.json +52 -0
  26. package/templates/astro/shapio/components/hero.json +108 -0
  27. package/templates/astro/shapio/models/article.json +150 -0
  28. package/templates/astro/shapio/models/author.json +73 -0
  29. package/templates/astro/shapio/models/page.json +93 -0
  30. package/templates/astro/shapio/models/siteSettings.json +87 -0
  31. package/templates/astro/src/layouts/Base.astro +52 -0
  32. package/templates/astro/src/lib/config.ts +42 -0
  33. package/templates/astro/src/lib/preview.ts +57 -0
  34. package/templates/astro/src/lib/render.ts +146 -0
  35. package/templates/astro/src/lib/shapio.ts +104 -0
  36. package/templates/astro/src/lib/site.ts +55 -0
  37. package/templates/astro/src/lib/types.ts +96 -0
  38. package/templates/astro/src/pages/404.astro +8 -0
  39. package/templates/astro/src/pages/[locale]/[...slug].astro +28 -0
  40. package/templates/astro/src/pages/[locale]/articles/[slug].astro +25 -0
  41. package/templates/astro/src/pages/[locale]/articles/index.astro +22 -0
  42. package/templates/astro/src/pages/[locale]/colophon.astro +19 -0
  43. package/templates/astro/src/pages/build.json.ts +9 -0
  44. package/templates/astro/src/pages/index.astro +18 -0
  45. package/templates/astro/src/pages/preview.astro +21 -0
  46. package/templates/astro/src/scripts/preview.ts +47 -0
  47. package/templates/astro/src/styles/site.css +199 -0
  48. package/templates/astro/tsconfig.json +17 -0
  49. package/templates/next/.env.example +15 -0
  50. package/templates/next/README.md +69 -0
  51. package/templates/next/gitignore +5 -0
  52. package/templates/next/next.config.ts +69 -0
  53. package/templates/next/package.json +34 -0
  54. package/templates/next/public/favicon.svg +9 -0
  55. package/templates/next/scripts/lib/admin.ts +76 -0
  56. package/templates/next/scripts/lib/content.ts +462 -0
  57. package/templates/next/scripts/lib/png.ts +47 -0
  58. package/templates/next/scripts/seed.ts +396 -0
  59. package/templates/next/scripts/smokeHttp.ts +162 -0
  60. package/templates/next/shapio/components/callToAction.json +85 -0
  61. package/templates/next/shapio/components/feature.json +46 -0
  62. package/templates/next/shapio/components/featureGrid.json +51 -0
  63. package/templates/next/shapio/components/gallery.json +52 -0
  64. package/templates/next/shapio/components/hero.json +108 -0
  65. package/templates/next/shapio/models/article.json +150 -0
  66. package/templates/next/shapio/models/author.json +73 -0
  67. package/templates/next/shapio/models/page.json +93 -0
  68. package/templates/next/shapio/models/siteSettings.json +87 -0
  69. package/templates/next/src/app/[locale]/[[...slug]]/page.tsx +40 -0
  70. package/templates/next/src/app/[locale]/articles/[slug]/page.tsx +37 -0
  71. package/templates/next/src/app/[locale]/articles/page.tsx +28 -0
  72. package/templates/next/src/app/[locale]/colophon/page.tsx +25 -0
  73. package/templates/next/src/app/[locale]/layout.tsx +77 -0
  74. package/templates/next/src/app/api/revalidate/route.ts +31 -0
  75. package/templates/next/src/app/preview/layout.tsx +20 -0
  76. package/templates/next/src/app/preview/page.tsx +6 -0
  77. package/templates/next/src/app/site.css +199 -0
  78. package/templates/next/src/components/ArticleCard/index.tsx +19 -0
  79. package/templates/next/src/components/ArticleView/index.tsx +40 -0
  80. package/templates/next/src/components/Byline/index.tsx +15 -0
  81. package/templates/next/src/components/LocaleSwitch/index.tsx +16 -0
  82. package/templates/next/src/components/Preview/hooks/usePreviewDraft.ts +53 -0
  83. package/templates/next/src/components/Preview/index.tsx +33 -0
  84. package/templates/next/src/components/ResponsiveImage/index.tsx +36 -0
  85. package/templates/next/src/components/RichText/index.tsx +13 -0
  86. package/templates/next/src/components/Sections/CallToAction/index.tsx +14 -0
  87. package/templates/next/src/components/Sections/FeatureGrid/index.tsx +17 -0
  88. package/templates/next/src/components/Sections/Gallery/index.tsx +20 -0
  89. package/templates/next/src/components/Sections/Hero/index.tsx +31 -0
  90. package/templates/next/src/components/Sections/SectionView/index.tsx +30 -0
  91. package/templates/next/src/components/Sections/index.tsx +19 -0
  92. package/templates/next/src/lib/articles.ts +5 -0
  93. package/templates/next/src/lib/config.ts +57 -0
  94. package/templates/next/src/lib/liveSnapshot.ts +74 -0
  95. package/templates/next/src/lib/log.ts +4 -0
  96. package/templates/next/src/lib/preview.ts +56 -0
  97. package/templates/next/src/lib/revalidation.ts +91 -0
  98. package/templates/next/src/lib/revalidationTargets.test.ts +67 -0
  99. package/templates/next/src/lib/revalidationTargets.ts +66 -0
  100. package/templates/next/src/lib/shapio.ts +75 -0
  101. package/templates/next/src/lib/site.ts +66 -0
  102. package/templates/next/src/lib/types.ts +96 -0
  103. package/templates/next/src/lib/webhookEvent.test.ts +61 -0
  104. package/templates/next/src/lib/webhookEvent.ts +47 -0
  105. package/templates/next/src/styles.d.ts +3 -0
  106. package/templates/next/tsconfig.json +23 -0
  107. package/templates/sveltekit/.env.example +10 -0
  108. package/templates/sveltekit/README.md +38 -0
  109. package/templates/sveltekit/gitignore +4 -0
  110. package/templates/sveltekit/package.json +38 -0
  111. package/templates/sveltekit/scripts/lib/admin.ts +76 -0
  112. package/templates/sveltekit/scripts/lib/content.ts +462 -0
  113. package/templates/sveltekit/scripts/lib/png.ts +47 -0
  114. package/templates/sveltekit/scripts/seed.ts +396 -0
  115. package/templates/sveltekit/scripts/smokeHttp.ts +162 -0
  116. package/templates/sveltekit/shapio/components/callToAction.json +85 -0
  117. package/templates/sveltekit/shapio/components/feature.json +46 -0
  118. package/templates/sveltekit/shapio/components/featureGrid.json +51 -0
  119. package/templates/sveltekit/shapio/components/gallery.json +52 -0
  120. package/templates/sveltekit/shapio/components/hero.json +108 -0
  121. package/templates/sveltekit/shapio/models/article.json +150 -0
  122. package/templates/sveltekit/shapio/models/author.json +73 -0
  123. package/templates/sveltekit/shapio/models/page.json +93 -0
  124. package/templates/sveltekit/shapio/models/siteSettings.json +87 -0
  125. package/templates/sveltekit/src/app.html +12 -0
  126. package/templates/sveltekit/src/env.ts +16 -0
  127. package/templates/sveltekit/src/hooks.server.ts +9 -0
  128. package/templates/sveltekit/src/lib/articles.ts +5 -0
  129. package/templates/sveltekit/src/lib/components/ArticleCard.svelte +21 -0
  130. package/templates/sveltekit/src/lib/components/ArticleView.svelte +43 -0
  131. package/templates/sveltekit/src/lib/components/Byline.svelte +14 -0
  132. package/templates/sveltekit/src/lib/components/CallToAction.svelte +15 -0
  133. package/templates/sveltekit/src/lib/components/FeatureGrid.svelte +22 -0
  134. package/templates/sveltekit/src/lib/components/Gallery.svelte +25 -0
  135. package/templates/sveltekit/src/lib/components/Hero.svelte +24 -0
  136. package/templates/sveltekit/src/lib/components/ResponsiveImage.svelte +30 -0
  137. package/templates/sveltekit/src/lib/components/RichText.svelte +13 -0
  138. package/templates/sveltekit/src/lib/components/Sections.svelte +30 -0
  139. package/templates/sveltekit/src/lib/preview.ts +56 -0
  140. package/templates/sveltekit/src/lib/server/config.ts +46 -0
  141. package/templates/sveltekit/src/lib/server/shapio.ts +93 -0
  142. package/templates/sveltekit/src/lib/site.css +199 -0
  143. package/templates/sveltekit/src/lib/site.ts +66 -0
  144. package/templates/sveltekit/src/lib/types.ts +96 -0
  145. package/templates/sveltekit/src/routes/+layout.ts +3 -0
  146. package/templates/sveltekit/src/routes/+page.ts +7 -0
  147. package/templates/sveltekit/src/routes/[locale]/+layout.server.ts +16 -0
  148. package/templates/sveltekit/src/routes/[locale]/+layout.svelte +32 -0
  149. package/templates/sveltekit/src/routes/[locale]/[...slug]/+page.server.ts +28 -0
  150. package/templates/sveltekit/src/routes/[locale]/[...slug]/+page.svelte +15 -0
  151. package/templates/sveltekit/src/routes/[locale]/articles/+page.server.ts +9 -0
  152. package/templates/sveltekit/src/routes/[locale]/articles/+page.svelte +17 -0
  153. package/templates/sveltekit/src/routes/[locale]/articles/[slug]/+page.server.ts +25 -0
  154. package/templates/sveltekit/src/routes/[locale]/articles/[slug]/+page.svelte +15 -0
  155. package/templates/sveltekit/src/routes/[locale]/colophon/+page.svelte +17 -0
  156. package/templates/sveltekit/src/routes/[locale]/colophon/+page.ts +5 -0
  157. package/templates/sveltekit/src/routes/preview/+page.server.ts +5 -0
  158. package/templates/sveltekit/src/routes/preview/+page.svelte +67 -0
  159. package/templates/sveltekit/static/favicon.svg +9 -0
  160. package/templates/sveltekit/tsconfig.json +14 -0
  161. package/templates/sveltekit/vite.config.ts +63 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Content as the Shapio delivery API returns it for the starter's models (shapio/). Delivery
3
+ * entries are flat: system attributes next to the fields, keyed by API key. Media fields are asset views,
4
+ * rich text carries the JSON document plus a sanitised `html` rendering, populated relations are entries.
5
+ */
6
+ export type MediaVariant = {
7
+ name: string;
8
+ width: number | null;
9
+ height: number | null;
10
+ format: string;
11
+ mimeType: string;
12
+ url: string;
13
+ };
14
+
15
+ export type Media = {
16
+ id: string;
17
+ filename: string;
18
+ mimeType: string;
19
+ width: number | null;
20
+ height: number | null;
21
+ alt: string;
22
+ caption: string;
23
+ url: string;
24
+ variants: MediaVariant[];
25
+ };
26
+
27
+ export type RichText = { format: 'shapio-richtext'; version: number; html: string };
28
+
29
+ type EntryBase = {
30
+ id: string;
31
+ /** The locale that served the entry (differs from the requested one on fallback). */
32
+ locale: string;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ publishedAt?: string;
36
+ };
37
+
38
+ export type Feature = { title: string; description: string | null };
39
+
40
+ export type HeroSection = {
41
+ __component: 'hero';
42
+ heading: string;
43
+ subheading: string | null;
44
+ image: Media | null;
45
+ ctaLabel: string | null;
46
+ ctaUrl: string | null;
47
+ };
48
+ export type FeatureGridSection = { __component: 'featureGrid'; heading: string | null; features: Feature[] };
49
+ export type GallerySection = { __component: 'gallery'; heading: string | null; images: Media[] };
50
+ export type CallToActionSection = {
51
+ __component: 'callToAction';
52
+ heading: string;
53
+ text: string | null;
54
+ buttonLabel: string;
55
+ buttonUrl: string;
56
+ };
57
+ export type Section = HeroSection | FeatureGridSection | GallerySection | CallToActionSection;
58
+
59
+ export type Page = EntryBase & {
60
+ title: string;
61
+ slug: string;
62
+ description: string | null;
63
+ sections: Section[];
64
+ };
65
+
66
+ export type Author = EntryBase & { name: string; bio: string | null; avatar: Media | null };
67
+
68
+ export type Article = EntryBase & {
69
+ title: string;
70
+ slug: string;
71
+ excerpt: string | null;
72
+ body: RichText | null;
73
+ cover: Media | null;
74
+ /** Populated (`populate=author`) or just the ID; null when the author is not published. */
75
+ author: Author | string | null;
76
+ publishedOn: string;
77
+ };
78
+
79
+ /** The `siteSettings` singleton: one entry per site, read by its API ID. */
80
+ export type SiteSettings = EntryBase & {
81
+ siteName: string;
82
+ tagline: string | null;
83
+ footer: string | null;
84
+ colophon: RichText | null;
85
+ };
86
+
87
+ export type DeliveryList<T> = {
88
+ data: T[];
89
+ meta: {
90
+ locale: string;
91
+ snapshot: number;
92
+ pagination: { page: number; pageSize: number; total: number; pageCount: number };
93
+ };
94
+ };
95
+
96
+ export type DeliveryItem<T> = { data: T; meta: { locale: string; snapshot: number } };
@@ -0,0 +1,3 @@
1
+ /** Every page is prerendered to static HTML at build time; URLs end with a slash, as in the other starters. */
2
+ export const prerender = true;
3
+ export const trailingSlash = 'always';
@@ -0,0 +1,7 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import { DEFAULT_LOCALE } from '#lib/site.ts';
3
+
4
+ /** The front page lives under its locale (`/en/`); prerendered as a redirect page. */
5
+ export const load = () => {
6
+ redirect(307, `/${DEFAULT_LOCALE}/`);
7
+ };
@@ -0,0 +1,16 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { getSiteSettings } from '#lib/server/shapio.ts';
3
+ import { isLocale, stringsFor } from '#lib/site.ts';
4
+ import type { LayoutServerLoad } from './$types';
5
+
6
+ /** The locale's site settings (the siteSettings singleton) and UI strings, for the header and footer. */
7
+ export const load: LayoutServerLoad = async ({ params }) => {
8
+ if (!isLocale(params.locale)) {
9
+ error(404, 'Not found');
10
+ }
11
+ return {
12
+ locale: params.locale,
13
+ strings: stringsFor(params.locale),
14
+ settings: await getSiteSettings(params.locale),
15
+ };
16
+ };
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import '#lib/site.css';
4
+ import { articlesPath, colophonPath, HOME_SLUG, otherLocale, pagePath, switchLocale } from '#lib/site.ts';
5
+ import type { LayoutProps } from './$types';
6
+
7
+ let { data, children }: LayoutProps = $props();
8
+ const other = $derived(otherLocale(data.locale));
9
+ // The name, tagline and footer come from the siteSettings singleton (built-in strings until it is published).
10
+ const siteName = $derived(data.settings?.siteName ?? data.strings.siteName);
11
+ </script>
12
+
13
+ <a class="skip" href="#content">{data.strings.skipToContent}</a>
14
+ <header class="site-header">
15
+ <a class="brand" href={pagePath(data.locale, HOME_SLUG)}>{siteName}</a>
16
+ {#if data.settings?.tagline}
17
+ <p class="tagline">{data.settings.tagline}</p>
18
+ {/if}
19
+ <nav aria-label="Main">
20
+ <a href={articlesPath(data.locale)}>{data.strings.articles}</a>
21
+ <a href={colophonPath(data.locale)}>{data.strings.colophon}</a>
22
+ <a href={switchLocale(page.url.pathname, other)} hreflang={other} lang={other}>{data.strings.switchTo}</a>
23
+ </nav>
24
+ </header>
25
+ <main id="content">
26
+ {@render children()}
27
+ </main>
28
+ <footer class="site-footer">
29
+ <p>{siteName} · {data.settings?.footer ?? data.strings.footer} · SvelteKit</p>
30
+ </footer>
31
+ <!-- Visual editing (@shapio/visual) plugs in here: its script and the data-shapio attributes on content arrive
32
+ with Shapio's visual-editing SDK, together with draft preview. -->
@@ -0,0 +1,28 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { listPages } from '#lib/server/shapio.ts';
3
+ import { HOME_SLUG, isLocale, LOCALES } from '#lib/site.ts';
4
+ import type { EntryGenerator, PageServerLoad } from './$types';
5
+
6
+ /** Pages from the `page` collection; `home` is the locale's front page (`/en/`), the others `/en/<slug>/`. */
7
+ export const entries: EntryGenerator = async () => {
8
+ const paths = [];
9
+ for (const locale of LOCALES) {
10
+ for (const page of await listPages(locale)) {
11
+ paths.push({ locale, slug: page.slug === HOME_SLUG ? '' : page.slug });
12
+ }
13
+ }
14
+ return paths;
15
+ };
16
+
17
+ export const load: PageServerLoad = async ({ params }) => {
18
+ // A rest parameter keeps the trailing slash (`about/`; trailingSlash is 'always').
19
+ const slug = params.slug.replace(/\/+$/, '');
20
+ const wanted = slug === '' ? HOME_SLUG : slug;
21
+ const page = isLocale(params.locale)
22
+ ? (await listPages(params.locale)).find((candidate) => candidate.slug === wanted)
23
+ : undefined;
24
+ if (!page) {
25
+ error(404, 'Not found');
26
+ }
27
+ return { page };
28
+ };
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import Sections from '#lib/components/Sections.svelte';
3
+ import type { PageProps } from './$types';
4
+
5
+ let { data }: PageProps = $props();
6
+ </script>
7
+
8
+ <svelte:head>
9
+ <title>{data.page.title} · {data.settings?.siteName ?? data.strings.siteName}</title>
10
+ {#if data.page.description}
11
+ <meta name="description" content={data.page.description} />
12
+ {/if}
13
+ </svelte:head>
14
+
15
+ <Sections page={data.page} />
@@ -0,0 +1,9 @@
1
+ import { listArticles } from '#lib/server/shapio.ts';
2
+ import { DEFAULT_LOCALE, isLocale, LOCALES } from '#lib/site.ts';
3
+ import type { EntryGenerator, PageServerLoad } from './$types';
4
+
5
+ export const entries: EntryGenerator = () => LOCALES.map((locale) => ({ locale }));
6
+
7
+ export const load: PageServerLoad = async ({ params }) => ({
8
+ articles: await listArticles(isLocale(params.locale) ? params.locale : DEFAULT_LOCALE),
9
+ });
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import ArticleCard from '#lib/components/ArticleCard.svelte';
3
+ import type { PageProps } from './$types';
4
+
5
+ let { data }: PageProps = $props();
6
+ </script>
7
+
8
+ <svelte:head>
9
+ <title>{data.strings.articles} · {data.settings?.siteName ?? data.strings.siteName}</title>
10
+ </svelte:head>
11
+
12
+ <h1 class="page-title">{data.strings.articles}</h1>
13
+ <ul class="cards">
14
+ {#each data.articles as article (article.id)}
15
+ <ArticleCard {article} locale={data.locale} strings={data.strings} />
16
+ {/each}
17
+ </ul>
@@ -0,0 +1,25 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { listArticles } from '#lib/server/shapio.ts';
3
+ import { isLocale, LOCALES } from '#lib/site.ts';
4
+ import type { EntryGenerator, PageServerLoad } from './$types';
5
+
6
+ /** Published articles only: a draft has no page, so its URL is a 404. */
7
+ export const entries: EntryGenerator = async () => {
8
+ const paths = [];
9
+ for (const locale of LOCALES) {
10
+ for (const article of await listArticles(locale)) {
11
+ paths.push({ locale, slug: article.slug });
12
+ }
13
+ }
14
+ return paths;
15
+ };
16
+
17
+ export const load: PageServerLoad = async ({ params }) => {
18
+ const article = isLocale(params.locale)
19
+ ? (await listArticles(params.locale)).find((candidate) => candidate.slug === params.slug)
20
+ : undefined;
21
+ if (!article) {
22
+ error(404, 'Not found');
23
+ }
24
+ return { article };
25
+ };
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import ArticleView from '#lib/components/ArticleView.svelte';
3
+ import type { PageProps } from './$types';
4
+
5
+ let { data }: PageProps = $props();
6
+ </script>
7
+
8
+ <svelte:head>
9
+ <title>{data.article.title} · {data.settings?.siteName ?? data.strings.siteName}</title>
10
+ {#if data.article.excerpt}
11
+ <meta name="description" content={data.article.excerpt} />
12
+ {/if}
13
+ </svelte:head>
14
+
15
+ <ArticleView article={data.article} locale={data.locale} strings={data.strings} />
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import RichText from '#lib/components/RichText.svelte';
3
+ import type { PageProps } from './$types';
4
+
5
+ /** The singleton page: the siteSettings entry's colophon, one per locale (loaded by the locale layout). */
6
+ let { data }: PageProps = $props();
7
+ </script>
8
+
9
+ <svelte:head>
10
+ <title>{data.strings.colophon} · {data.settings?.siteName ?? data.strings.siteName}</title>
11
+ </svelte:head>
12
+
13
+ <h1 class="page-title">{data.strings.colophon}</h1>
14
+ {#if data.settings?.tagline}
15
+ <p class="lead">{data.settings.tagline}</p>
16
+ {/if}
17
+ <RichText value={data.settings?.colophon ?? null} />
@@ -0,0 +1,5 @@
1
+ import { LOCALES } from '#lib/site.ts';
2
+ import type { EntryGenerator } from './$types';
3
+
4
+ /** The siteSettings singleton is loaded by the locale layout; this page only needs its locales listed. */
5
+ export const entries: EntryGenerator = () => LOCALES.map((locale) => ({ locale }));
@@ -0,0 +1,5 @@
1
+ import { publicShapioUrl } from '#lib/server/config.ts';
2
+ import type { PageServerLoad } from './$types';
3
+
4
+ /** The preview page is prerendered like the rest; the draft itself is read in the browser (see +page.svelte). */
5
+ export const load: PageServerLoad = () => ({ shapioUrl: publicShapioUrl() });
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { initVisualEditing } from '@shapio/visual';
3
+ import { onMount } from 'svelte';
4
+ import { replaceState } from '$app/navigation';
5
+ import ArticleView from '#lib/components/ArticleView.svelte';
6
+ import Sections from '#lib/components/Sections.svelte';
7
+ import '#lib/site.css';
8
+ import { loadDraft, parsePreviewUrl, type PreviewDraft } from '#lib/preview.ts';
9
+ import { DEFAULT_LOCALE, stringsFor, type Locale } from '#lib/site.ts';
10
+ import type { PageProps } from './$types';
11
+
12
+ /**
13
+ * `/preview/?model=…&id=…&locale=…#token=…`: the draft Shapio's preview URL points at, read in the browser
14
+ * with the preview token (src/lib/preview.ts) and rendered with the published pages' components. Inside
15
+ * Shapio's preview pane, @shapio/visual turns on visual editing and re-loads the draft after each save.
16
+ */
17
+ let { data }: PageProps = $props();
18
+ let locale: Locale = $state(DEFAULT_LOCALE);
19
+ let status: 'loading' | 'incomplete' | 'failed' | 'ready' = $state('loading');
20
+ let message = $state('');
21
+ let draft: PreviewDraft | undefined = $state();
22
+ const strings = $derived(stringsFor(locale));
23
+
24
+ onMount(() => {
25
+ const request = parsePreviewUrl(new URL(window.location.href));
26
+ // The token must not linger in the address bar, history or a shared screenshot.
27
+ replaceState(`${window.location.pathname}${window.location.search}`, {});
28
+ if (!request) {
29
+ status = 'incomplete';
30
+ return undefined;
31
+ }
32
+ locale = request.locale;
33
+ document.documentElement.lang = request.locale;
34
+ const load = async () => {
35
+ try {
36
+ draft = await loadDraft(data.shapioUrl, request);
37
+ status = 'ready';
38
+ } catch (error) {
39
+ message = error instanceof Error ? error.message : String(error);
40
+ status = 'failed';
41
+ }
42
+ };
43
+ void load();
44
+ return initVisualEditing({ origin: data.shapioUrl, onRefresh: load });
45
+ });
46
+ </script>
47
+
48
+ <svelte:head>
49
+ <title>Preview</title>
50
+ <meta name="robots" content="noindex" />
51
+ <meta name="referrer" content="no-referrer" />
52
+ </svelte:head>
53
+
54
+ <div class="preview-banner" role="status">{strings.previewBanner}</div>
55
+ <main id="content" lang={locale}>
56
+ {#if status === 'loading'}
57
+ <p>{strings.previewLoading}</p>
58
+ {:else if status === 'incomplete'}
59
+ <p>{strings.previewIncomplete}</p>
60
+ {:else if status === 'failed'}
61
+ <p>{strings.previewFailed}: {message}</p>
62
+ {:else if draft?.modelKey === 'pages'}
63
+ <Sections page={draft.entry} />
64
+ {:else if draft?.modelKey === 'articles'}
65
+ <ArticleView article={draft.entry} locale={draft.locale} {strings} />
66
+ {/if}
67
+ </main>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Shapio">
2
+ <title>Shapio</title>
3
+ <rect width="64" height="64" rx="14" fill="#2563EB"/>
4
+ <g transform="translate(32 32) scale(0.72) translate(-32 -32)">
5
+ <path fill="#FAF9F6" d="M43.39 8.16L51.31 12.73A8.25 8.25 0 0 1 55.43 19.88L55.43 21.08A4.72 4.72 0 0 1 48.36 25.17L33.51 16.59ZM20.61 55.84L12.69 51.27A8.25 8.25 0 0 1 8.57 44.12L8.57 42.92A4.72 4.72 0 0 1 15.64 38.83L30.49 47.41Z"/>
6
+ <path fill="#FAF9F6" d="M30.23 5.67A8.84 8.84 0 0 1 39.07 5.67L47.52 10.54L26.62 22.61A3.24 3.24 0 0 0 25 25.42L25 26.92A2.65 2.65 0 0 0 26.33 29.21L29.37 30.97A1.47 1.47 0 0 1 29.37 33.52L24.24 36.48A3.24 3.24 0 0 1 21 36.48L15.27 33.17A10.61 10.61 0 0 1 9.97 23.98L9.97 23.49A10.61 10.61 0 0 1 15.27 14.3ZM33.77 58.33A8.84 8.84 0 0 1 24.93 58.33L16.48 53.46L37.38 41.39A3.24 3.24 0 0 0 39 38.58L39 37.08A2.65 2.65 0 0 0 37.67 34.79L34.63 33.03A1.47 1.47 0 0 1 34.63 30.48L39.76 27.52A3.24 3.24 0 0 1 43 27.52L48.73 30.83A10.61 10.61 0 0 1 54.03 40.02L54.03 40.51A10.61 10.61 0 0 1 48.73 49.7Z"/>
7
+ <path fill="none" stroke="#2563EB" stroke-width="1.1" d="M35.28 17.61L47.52 10.54M28.72 46.39L16.48 53.46"/>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "$app/tsconfig",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "noUncheckedIndexedAccess": true,
6
+ "skipLibCheck": true,
7
+ "resolveJsonModule": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "verbatimModuleSyntax": true,
11
+ "types": ["$app/types", "node"]
12
+ },
13
+ "include": ["src", "*"]
14
+ }
@@ -0,0 +1,63 @@
1
+ import adapter from '@sveltejs/adapter-static';
2
+ import { sveltekit } from '@sveltejs/kit/vite';
3
+ import { defineConfig, loadEnv, type Connect, type Plugin } from 'vite';
4
+
5
+ const originOf = (url: string) => {
6
+ try {
7
+ return new URL(url).origin;
8
+ } catch {
9
+ return undefined;
10
+ }
11
+ };
12
+
13
+ /**
14
+ * Only this site and Shapio may frame its pages (Shapio's preview pane shows /preview/ beside the document):
15
+ * sent by `vite dev` and `vite preview`, and written to build/_headers for hosts that read it (Cloudflare
16
+ * Pages, Netlify); set the same header on any other host. The origin is PUBLIC_SHAPIO_URL, else SHAPIO_URL.
17
+ */
18
+ const frameAncestors = (env: Record<string, string>) => {
19
+ const shapio = originOf(env.PUBLIC_SHAPIO_URL || env.SHAPIO_URL || 'http://localhost:4300');
20
+ return `frame-ancestors 'self'${shapio ? ` ${shapio}` : ''}`;
21
+ };
22
+
23
+ /**
24
+ * Sets the policy on every response of `vite dev` and `vite preview` (registered before SvelteKit's own
25
+ * middleware, which answers prerendered pages itself), and emits `_headers` into the client output, which
26
+ * adapter-static copies into build/.
27
+ */
28
+ const frameAncestorsPlugin = (policy: string): Plugin => {
29
+ const setHeader = (server: { middlewares: Connect.Server }) => {
30
+ server.middlewares.use((_request, response, next) => {
31
+ response.setHeader('Content-Security-Policy', policy);
32
+ next();
33
+ });
34
+ };
35
+ return {
36
+ name: 'shapio-frame-ancestors',
37
+ configureServer: setHeader,
38
+ configurePreviewServer: setHeader,
39
+ generateBundle() {
40
+ if (this.environment.name === 'client') {
41
+ this.emitFile({
42
+ type: 'asset',
43
+ fileName: '_headers',
44
+ source: `/*\n Content-Security-Policy: ${policy}\n`,
45
+ });
46
+ }
47
+ },
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Static output: `npm run build` prerenders every page to plain HTML in build/ (any static host: Cloudflare
53
+ * Pages, Netlify, a bucket). 404.html is served for unknown paths by hosts that support it.
54
+ */
55
+ export default defineConfig(({ mode }) => {
56
+ const policy = frameAncestors({ ...loadEnv(mode, process.cwd(), ''), ...process.env } as Record<
57
+ string,
58
+ string
59
+ >);
60
+ return {
61
+ plugins: [frameAncestorsPlugin(policy), sveltekit({ adapter: adapter({ fallback: '404.html' }) })],
62
+ };
63
+ });