ferst-core 0.8.1 → 0.8.2

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.
@@ -150,7 +150,11 @@ function otherMapUse(provider: string): CookieUse {
150
150
  export interface CookieScanInput {
151
151
  /** Every block on the site's pages. Container children (`section` / `grid` / `stack`)
152
152
  * are walked, so an embed nested in a grid counts. */
153
- blocks: Iterable<BlockLike>;
153
+ blocks?: Iterable<BlockLike>;
154
+ /** @deprecated Block types alone, as before 0.8.1: judged by type, not by source (any
155
+ * video block counts as YouTube and Vimeo). Kept so a site's own older policy page
156
+ * still builds; pass `blocks`. */
157
+ blockTypes?: Iterable<string>;
154
158
  /** True when the site has a Google Calendar configured (dedicated calendar page). */
155
159
  calendarConfigured?: boolean;
156
160
  }
@@ -171,9 +175,25 @@ function allBlocks(blocks: Iterable<BlockLike>, acc: BlockLike[] = []): BlockLik
171
175
  return acc;
172
176
  }
173
177
 
178
+ /** How a provider was detected before 0.8.1, from block types alone. */
179
+ const BY_TYPE: Record<string, string[]> = {
180
+ 'google-maps': ['locations', 'mapEmbed'],
181
+ 'google-calendar': ['calendar'],
182
+ youtube: ['video'],
183
+ vimeo: ['video'],
184
+ };
185
+
174
186
  /** Report the cookie uses actually active on a site, grouped by category. */
175
187
  export function scanCookieUses(input: CookieScanInput): CookiePolicyModel {
176
- const blocks = allBlocks(input.blocks);
188
+ const necessary = NECESSARY.map((u) => ({ ...u }));
189
+ if (!input.blocks && input.blockTypes) {
190
+ const types = new Set(input.blockTypes);
191
+ const additional = ADDITIONAL.filter(
192
+ (u) => (BY_TYPE[u.id] ?? []).some((t) => types.has(t)) || (u.calendar === true && !!input.calendarConfigured),
193
+ ).map(({ loads, calendar, ...use }) => use);
194
+ return { necessary, additional };
195
+ }
196
+ const blocks = allBlocks(input.blocks ?? []);
177
197
  const additional: CookieUse[] = ADDITIONAL.filter(
178
198
  (u) => (u.loads ? blocks.some(u.loads) : false) || (u.calendar === true && !!input.calendarConfigured),
179
199
  ).map(({ loads, calendar, ...use }) => use); // drop detection fields from the view model
@@ -181,7 +201,7 @@ export function scanCookieUses(input: CookieScanInput): CookiePolicyModel {
181
201
  const otherMaps = new Set(blocks.map(mapProviderOf).filter((p): p is string => p !== undefined && !isGoogleMaps(p)));
182
202
  const afterMaps = additional.findIndex((u) => u.id !== 'google-maps');
183
203
  additional.splice(afterMaps === -1 ? additional.length : afterMaps, 0, ...[...otherMaps].map(otherMapUse));
184
- return { necessary: NECESSARY.map((u) => ({ ...u })), additional };
204
+ return { necessary, additional };
185
205
  }
186
206
 
187
207
  /** Every block `type` used in a list of blocks, walking container children
package/lib/pages.ts CHANGED
@@ -29,12 +29,21 @@ export async function getPageEntries() {
29
29
  return all.filter((entry) => !entry.data.draft);
30
30
  }
31
31
 
32
+ /** The reserved entry a site may design as its own "page not found" page. */
33
+ export const NOT_FOUND_ID = '404';
34
+
35
+ /** The site's own "page not found" page, if it has designed one (a `404` entry). The
36
+ * injected /404 route shows it at every missing address; it is never a page of its own. */
37
+ export async function getNotFoundEntry() {
38
+ return (await getPageEntries()).find((entry) => entry.id === NOT_FOUND_ID);
39
+ }
40
+
32
41
  /**
33
42
  * A drop-in `getStaticPaths` for a client's `[...slug].astro`: one route per
34
43
  * non-draft page, keyed by the file's slug, with the entry passed as a prop.
35
44
  */
36
45
  export async function getPageRoutes() {
37
- const entries = await getPageEntries();
46
+ const entries = (await getPageEntries()).filter((entry) => entry.id !== NOT_FOUND_ID);
38
47
  return entries.map((entry) => ({
39
48
  params: { slug: entry.id },
40
49
  props: { entry },
@@ -58,7 +67,9 @@ export function pageRouteParams(id: string): { slug: string | undefined } {
58
67
  * AND can pick its home layout in the builder like any other page.
59
68
  */
60
69
  export async function getSiteRoutes() {
61
- const entries = await getPageEntries();
70
+ // The `404` entry is the site's "page not found" page, shown by the /404 route at every
71
+ // missing address; it must not also be a page at /404/ (or listed in the sitemap).
72
+ const entries = (await getPageEntries()).filter((entry) => entry.id !== NOT_FOUND_ID);
62
73
  return entries.map((entry) => ({
63
74
  params: pageRouteParams(entry.id),
64
75
  props: { entry },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
6
6
  "license": "BUSL-1.1",
package/routes/404.astro CHANGED
@@ -5,11 +5,16 @@
5
5
  * a `404.html` it serves the HOME PAGE with a 200 instead (a "soft 404"): search engines
6
6
  * would see the home page at every mistyped or retired address, and a broken link would
7
7
  * look fine to visitors and to the migration's redirect check. A site with its own
8
- * `src/pages/404.astro` keeps it (the integration skips this route then).
8
+ * `src/pages/404.astro` keeps it (the integration skips this route then). A site that has
9
+ * designed its "page not found" page as content (a `404` entry in `pages`) gets that page.
9
10
  */
10
11
  import Base from '../layouts/Base.astro';
11
12
  import BlockRenderer from '../components/BlockRenderer.astro';
12
13
  import { pageSchema } from '../content/blocks';
14
+ import { getNotFoundEntry } from '../lib/pages';
15
+
16
+ const own = await getNotFoundEntry();
17
+ const ownPage = own ? pageSchema.parse(own.data) : null;
13
18
 
14
19
  const blog = import.meta.env.FERST_ROUTES_BLOG !== false;
15
20
  const page = pageSchema.parse({
@@ -31,6 +36,12 @@ const page = pageSchema.parse({
31
36
  });
32
37
  ---
33
38
 
34
- <Base title={page.title} description="This page isn't here." indexable={false}>
35
- <BlockRenderer blocks={page.blocks} />
36
- </Base>
39
+ {ownPage ? (
40
+ <Base title={ownPage.title} description={ownPage.description ?? "This page isn't here."} align={ownPage.align} indexable={false}>
41
+ <BlockRenderer blocks={ownPage.blocks} />
42
+ </Base>
43
+ ) : (
44
+ <Base title={page.title} description="This page isn't here." indexable={false}>
45
+ <BlockRenderer blocks={page.blocks} />
46
+ </Base>
47
+ )}
@@ -3,7 +3,7 @@
3
3
  // and each tag page in use, and the calendar when it's set up. Drafts, hidden tags, later
4
4
  // listing pages (/posts/page/2…) and the cookie policy stay out.
5
5
  import type { APIRoute } from 'astro';
6
- import { getPageEntries, pageRouteParams } from '../lib/pages';
6
+ import { getSiteRoutes } from '../lib/pages';
7
7
  import { canonicalUrl, sitemapXml } from '../lib/seo.mjs';
8
8
 
9
9
  export const prerender = true;
@@ -12,9 +12,9 @@ export const GET: APIRoute = async () => {
12
12
  const site = String(import.meta.env.FERST_SITE_URL || '');
13
13
  const entries: Array<{ loc: string; lastmod?: Date }> = [];
14
14
 
15
- for (const page of await getPageEntries()) {
16
- const { slug } = pageRouteParams(page.id);
17
- entries.push({ loc: canonicalUrl(site, slug ? `/${slug}` : '/') });
15
+ // Exactly the pages the site routes (so never its "page not found" page).
16
+ for (const { params } of await getSiteRoutes()) {
17
+ entries.push({ loc: canonicalUrl(site, params.slug ? `/${params.slug}` : '/') });
18
18
  }
19
19
 
20
20
  if (import.meta.env.FERST_ROUTES_BLOG !== false) {