create-shapio 0.5.0 → 0.5.1
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.
- package/package.json +1 -1
- package/templates/astro/.env.example +6 -0
- package/templates/astro/README.md +9 -0
- package/templates/astro/package.json +7 -4
- package/templates/astro/scripts/seed.ts +77 -24
- package/templates/astro/scripts/smokeDrafts.ts +109 -0
- package/templates/astro/src/layouts/Base.astro +8 -0
- package/templates/astro/src/lib/config.ts +13 -1
- package/templates/astro/src/lib/renderTime.test.ts +31 -0
- package/templates/astro/src/lib/renderTime.ts +24 -0
- package/templates/astro/src/lib/shapio.ts +88 -64
- package/templates/astro/src/lib/site.ts +4 -0
- package/templates/astro/src/lib/snapshotResolver.test.ts +64 -0
- package/templates/astro/src/lib/snapshotResolver.ts +52 -0
- package/templates/astro/src/lib/types.ts +0 -9
- package/templates/astro/src/pages/[locale]/[...slug].astro +12 -1
- package/templates/astro/src/pages/[locale]/articles/[slug].astro +12 -1
- package/templates/astro/src/pages/[locale]/articles/index.astro +6 -1
- package/templates/astro/src/pages/build.json.ts +9 -2
- package/templates/astro/src/styles/site.css +14 -0
- package/templates/next/.env.example +6 -0
- package/templates/next/README.md +9 -0
- package/templates/next/next.config.ts +5 -1
- package/templates/next/package.json +4 -3
- package/templates/next/scripts/seed.ts +77 -24
- package/templates/next/scripts/smokeDrafts.ts +109 -0
- package/templates/next/src/app/[locale]/layout.tsx +3 -0
- package/templates/next/src/app/site.css +14 -0
- package/templates/next/src/components/DraftsBadge/index.tsx +10 -0
- package/templates/next/src/lib/config.ts +13 -1
- package/templates/next/src/lib/revalidation.ts +4 -1
- package/templates/next/src/lib/shapio.ts +24 -4
- package/templates/next/src/lib/shapioClient.ts +6 -4
- package/templates/next/src/lib/site.ts +4 -0
- package/templates/next/src/lib/snapshotResolver.test.ts +64 -0
- package/templates/next/src/lib/snapshotResolver.ts +52 -0
- package/templates/sveltekit/.env.example +6 -0
- package/templates/sveltekit/README.md +9 -0
- package/templates/sveltekit/package.json +7 -4
- package/templates/sveltekit/scripts/seed.ts +77 -24
- package/templates/sveltekit/scripts/smokeDrafts.ts +109 -0
- package/templates/sveltekit/src/env.ts +2 -0
- package/templates/sveltekit/src/lib/server/config.ts +11 -1
- package/templates/sveltekit/src/lib/server/shapio.ts +28 -13
- package/templates/sveltekit/src/lib/server/snapshotResolver.test.ts +64 -0
- package/templates/sveltekit/src/lib/server/snapshotResolver.ts +52 -0
- package/templates/sveltekit/src/lib/site.css +14 -0
- package/templates/sveltekit/src/lib/site.ts +4 -0
- package/templates/sveltekit/src/routes/[locale]/+layout.server.ts +3 -2
- package/templates/sveltekit/src/routes/[locale]/+layout.svelte +3 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createClient, ShapioApiError, type DeliveryListQuery, type DeliverySite } from '@shapio/client';
|
|
2
|
+
import { dev } from '$app/env';
|
|
2
3
|
import type { Locale } from '../site';
|
|
3
4
|
import type { Article, Page, SiteSettings } from '../types';
|
|
4
|
-
import { configuredSnapshot, deliveryToken, shapioUrl, siteKey } from './config';
|
|
5
|
+
import { configuredSnapshot, deliveryToken, isDraftsMode, shapioUrl, siteKey } from './config';
|
|
6
|
+
import { cachedRead, cacheWindow, createSnapshotResolver } from './snapshotResolver';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* The site's read side: Shapio's delivery API through `@shapio/client`, pinned to one publication snapshot
|
|
8
10
|
* for the whole build. The snapshot is read once, when prerendering starts (or taken from SHAPIO_SNAPSHOT),
|
|
9
11
|
* and sent with every request, so a publish during the build never yields a site that mixes old and new.
|
|
12
|
+
* Under `vite dev` the snapshot and every read are reused for a second at most, so a publish shows on reload
|
|
13
|
+
* (src/lib/server/snapshotResolver.ts). Drafts mode (SHAPIO_DRAFTS=true) reads saved drafts, which have no
|
|
14
|
+
* snapshot: no read sends one.
|
|
10
15
|
*/
|
|
11
16
|
const PAGE_SIZE = 100;
|
|
12
17
|
|
|
@@ -20,26 +25,36 @@ const PAGE_FIELDS = ['title', 'slug', 'description', 'sections', 'seo'];
|
|
|
20
25
|
const ARTICLE_FIELDS = ['title', 'slug', 'excerpt', 'body', 'cover', 'author', 'publishedOn', 'seo'];
|
|
21
26
|
const SITE_SETTINGS_FIELDS = ['siteName', 'tagline', 'footer', 'colophon'];
|
|
22
27
|
|
|
23
|
-
const createShapio = () =>
|
|
28
|
+
const createShapio = () =>
|
|
29
|
+
createClient({ baseUrl: shapioUrl(), token: deliveryToken(), site: siteKey(), drafts: isDraftsMode() });
|
|
24
30
|
|
|
25
31
|
let client: ReturnType<typeof createShapio> | undefined;
|
|
26
32
|
const shapio = () => (client ??= createShapio());
|
|
27
33
|
|
|
28
|
-
let
|
|
34
|
+
let resolver: (() => Promise<number>) | undefined;
|
|
29
35
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The snapshot every request reads: one for the whole build; in dev, the current one (re-read after 1 s); in
|
|
38
|
+
* drafts mode, none (drafts cannot be pinned).
|
|
39
|
+
*/
|
|
40
|
+
const snapshot = async (): Promise<number | undefined> =>
|
|
41
|
+
isDraftsMode()
|
|
42
|
+
? undefined
|
|
43
|
+
: (resolver ??= createSnapshotResolver({
|
|
44
|
+
dev,
|
|
45
|
+
configured: configuredSnapshot(),
|
|
46
|
+
current: async () => (await shapio().snapshots.current()).snapshot,
|
|
47
|
+
}))();
|
|
33
48
|
|
|
34
|
-
/** Each collection is read once per locale
|
|
35
|
-
const memo = new Map<string, Promise<unknown>>();
|
|
49
|
+
/** Each collection is read once per locale however many pages render from it (in dev, at most 1 s apart). */
|
|
50
|
+
const memo = new Map<string, () => Promise<unknown>>();
|
|
36
51
|
const once = <T>(key: string, read: () => Promise<T>): Promise<T> => {
|
|
37
|
-
let
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
memo.set(key,
|
|
52
|
+
let cached = memo.get(key) as (() => Promise<T>) | undefined;
|
|
53
|
+
if (!cached) {
|
|
54
|
+
cached = cachedRead(read, cacheWindow(dev));
|
|
55
|
+
memo.set(key, cached);
|
|
41
56
|
}
|
|
42
|
-
return
|
|
57
|
+
return cached();
|
|
43
58
|
};
|
|
44
59
|
|
|
45
60
|
/** Every published entry of a collection in one locale, page by page, at the pinned snapshot. */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { cachedRead, createSnapshotResolver, DEV_WINDOW_MS } from './snapshotResolver';
|
|
3
|
+
|
|
4
|
+
const clock = (start = 0) => {
|
|
5
|
+
let time = start;
|
|
6
|
+
return { now: () => time, advance: (ms: number) => (time += ms) };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe('createSnapshotResolver', () => {
|
|
10
|
+
it('pins the first snapshot for the whole build', async () => {
|
|
11
|
+
const { now, advance } = clock();
|
|
12
|
+
const current = vi.fn().mockResolvedValueOnce(7).mockResolvedValue(8);
|
|
13
|
+
const snapshot = createSnapshotResolver({ dev: false, configured: undefined, current, now });
|
|
14
|
+
|
|
15
|
+
expect(await snapshot()).toBe(7);
|
|
16
|
+
advance(60 * 60 * 1000);
|
|
17
|
+
expect(await snapshot()).toBe(7);
|
|
18
|
+
expect(current).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('reuses a dev read within the window and reads the current snapshot again after it', async () => {
|
|
22
|
+
const { now, advance } = clock();
|
|
23
|
+
const current = vi.fn().mockResolvedValueOnce(7).mockResolvedValue(8);
|
|
24
|
+
const snapshot = createSnapshotResolver({ dev: true, configured: undefined, current, now });
|
|
25
|
+
|
|
26
|
+
expect(await snapshot()).toBe(7);
|
|
27
|
+
advance(DEV_WINDOW_MS - 1);
|
|
28
|
+
expect(await snapshot()).toBe(7);
|
|
29
|
+
expect(current).toHaveBeenCalledTimes(1);
|
|
30
|
+
|
|
31
|
+
advance(1);
|
|
32
|
+
expect(await snapshot()).toBe(8);
|
|
33
|
+
expect(current).toHaveBeenCalledTimes(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('keeps SHAPIO_SNAPSHOT in dev and in builds, without asking Shapio', async () => {
|
|
37
|
+
const current = vi.fn().mockResolvedValue(8);
|
|
38
|
+
for (const dev of [true, false]) {
|
|
39
|
+
const snapshot = createSnapshotResolver({ dev, configured: 3, current });
|
|
40
|
+
expect(await snapshot()).toBe(3);
|
|
41
|
+
}
|
|
42
|
+
expect(current).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('cachedRead', () => {
|
|
47
|
+
it('shares one read between calls that overlap it', async () => {
|
|
48
|
+
const read = vi.fn().mockResolvedValue('value');
|
|
49
|
+
const cached = cachedRead(read, DEV_WINDOW_MS, clock().now);
|
|
50
|
+
|
|
51
|
+
const [first, second] = await Promise.all([cached(), cached()]);
|
|
52
|
+
expect([first, second]).toEqual(['value', 'value']);
|
|
53
|
+
expect(read).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not keep a failed read', async () => {
|
|
57
|
+
const read = vi.fn().mockRejectedValueOnce(new Error('down')).mockResolvedValue('value');
|
|
58
|
+
const cached = cachedRead(read, Number.POSITIVE_INFINITY, clock().now);
|
|
59
|
+
|
|
60
|
+
await expect(cached()).rejects.toThrow('down');
|
|
61
|
+
expect(await cached()).toBe('value');
|
|
62
|
+
expect(read).toHaveBeenCalledTimes(2);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which publication snapshot the site reads, and for how long a read is reused. A build pins one snapshot
|
|
3
|
+
* for all its pages (SHAPIO_SNAPSHOT, else the current one when the build starts). The dev server reads the
|
|
4
|
+
* current snapshot again once the last read is older than DEV_WINDOW_MS, so a publish shows on reload while
|
|
5
|
+
* the reads of one page render still share a snapshot.
|
|
6
|
+
*/
|
|
7
|
+
export const DEV_WINDOW_MS = 1000;
|
|
8
|
+
|
|
9
|
+
/** How long a cached read is reused: for the whole process in a build, DEV_WINDOW_MS in dev. */
|
|
10
|
+
export const cacheWindow = (dev: boolean): number => (dev ? DEV_WINDOW_MS : Number.POSITIVE_INFINITY);
|
|
11
|
+
|
|
12
|
+
/** `read`, reused for `windowMs` after it starts. A read that fails is not kept: the next call tries again. */
|
|
13
|
+
export const cachedRead = <T>(
|
|
14
|
+
read: () => Promise<T>,
|
|
15
|
+
windowMs: number,
|
|
16
|
+
now: () => number = Date.now,
|
|
17
|
+
): (() => Promise<T>) => {
|
|
18
|
+
let cached: { value: Promise<T>; at: number } | undefined;
|
|
19
|
+
return () => {
|
|
20
|
+
const at = now();
|
|
21
|
+
if (cached && at - cached.at < windowMs) {
|
|
22
|
+
return cached.value;
|
|
23
|
+
}
|
|
24
|
+
const entry = { value: read(), at };
|
|
25
|
+
cached = entry;
|
|
26
|
+
entry.value.catch(() => {
|
|
27
|
+
if (cached === entry) {
|
|
28
|
+
cached = undefined;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return entry.value;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SnapshotResolverOptions = {
|
|
36
|
+
/** True under the dev server. */
|
|
37
|
+
dev: boolean;
|
|
38
|
+
/** SHAPIO_SNAPSHOT: when set, the site shows exactly this snapshot, in dev too. */
|
|
39
|
+
configured: number | undefined;
|
|
40
|
+
/** Reads Shapio's current publication snapshot. */
|
|
41
|
+
current: () => Promise<number>;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The snapshot every delivery read sends. */
|
|
46
|
+
export const createSnapshotResolver = ({
|
|
47
|
+
dev,
|
|
48
|
+
configured,
|
|
49
|
+
current,
|
|
50
|
+
now,
|
|
51
|
+
}: SnapshotResolverOptions): (() => Promise<number>) =>
|
|
52
|
+
configured === undefined ? cachedRead(current, cacheWindow(dev), now) : () => Promise.resolve(configured);
|
|
@@ -197,3 +197,17 @@ main {
|
|
|
197
197
|
text-align: center;
|
|
198
198
|
font-weight: 600;
|
|
199
199
|
}
|
|
200
|
+
/* Drafts mode (SHAPIO_DRAFTS=true): a corner badge, so a drafts build is never mistaken for the real site. */
|
|
201
|
+
.drafts-badge {
|
|
202
|
+
position: fixed;
|
|
203
|
+
right: 1rem;
|
|
204
|
+
bottom: 1rem;
|
|
205
|
+
z-index: 10;
|
|
206
|
+
margin: 0;
|
|
207
|
+
background: #78350f;
|
|
208
|
+
color: #fef3c7;
|
|
209
|
+
padding: 0.25rem 0.75rem;
|
|
210
|
+
border-radius: 999px;
|
|
211
|
+
font-size: 0.875rem;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
}
|
|
@@ -19,6 +19,8 @@ const STRINGS = {
|
|
|
19
19
|
by: 'By',
|
|
20
20
|
switchTo: 'Français',
|
|
21
21
|
skipToContent: 'Skip to content',
|
|
22
|
+
drafts: 'Drafts',
|
|
23
|
+
draftsNote: 'Drafts mode: this server shows saved drafts, not the published site.',
|
|
22
24
|
footer: 'Built with Shapio',
|
|
23
25
|
previewBanner: 'Preview: this is a draft, not the published page.',
|
|
24
26
|
previewLoading: 'Loading the draft…',
|
|
@@ -32,6 +34,8 @@ const STRINGS = {
|
|
|
32
34
|
by: 'Par',
|
|
33
35
|
switchTo: 'English',
|
|
34
36
|
skipToContent: 'Aller au contenu',
|
|
37
|
+
drafts: 'Brouillons',
|
|
38
|
+
draftsNote: 'Mode brouillons : ce serveur montre les brouillons enregistrés, pas le site publié.',
|
|
35
39
|
footer: 'Construit avec Shapio',
|
|
36
40
|
previewBanner: 'Aperçu : ceci est un brouillon, pas la page publiée.',
|
|
37
41
|
previewLoading: 'Chargement du brouillon…',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { error } from '@sveltejs/kit';
|
|
2
|
-
import { siteUrl } from '#lib/server/config.ts';
|
|
2
|
+
import { isDraftsMode, siteUrl } from '#lib/server/config.ts';
|
|
3
3
|
import { getSite, getSiteSettings } from '#lib/server/shapio.ts';
|
|
4
4
|
import { isLocale, stringsFor } from '#lib/site.ts';
|
|
5
5
|
import type { LayoutServerLoad } from './$types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The locale's site settings (the siteSettings singleton) and UI strings, for the header and footer; the site's
|
|
9
|
-
* SEO defaults and public URL, for every page's <head
|
|
9
|
+
* SEO defaults and public URL, for every page's <head>; whether drafts mode is on, for its badge.
|
|
10
10
|
*/
|
|
11
11
|
export const load: LayoutServerLoad = async ({ params }) => {
|
|
12
12
|
if (!isLocale(params.locale)) {
|
|
@@ -18,5 +18,6 @@ export const load: LayoutServerLoad = async ({ params }) => {
|
|
|
18
18
|
settings: await getSiteSettings(params.locale),
|
|
19
19
|
site: await getSite(),
|
|
20
20
|
siteUrl: siteUrl() ?? null,
|
|
21
|
+
drafts: isDraftsMode(),
|
|
21
22
|
};
|
|
22
23
|
};
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
<footer class="site-footer">
|
|
29
29
|
<p>{siteName} · {data.settings?.footer ?? data.strings.footer} · SvelteKit</p>
|
|
30
30
|
</footer>
|
|
31
|
+
{#if data.drafts}
|
|
32
|
+
<p class="drafts-badge" title={data.strings.draftsNote} data-shapio-drafts="">{data.strings.drafts}</p>
|
|
33
|
+
{/if}
|
|
31
34
|
<!-- Visual editing (@shapio/visual) plugs in here: its script and the data-shapio attributes on content arrive
|
|
32
35
|
with Shapio's visual-editing SDK, together with draft preview. -->
|