@sevenfold/setto-client 0.33.1 → 0.34.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.
- package/dist/blog/contract.d.ts +149 -0
- package/dist/blog/index.d.ts +10 -0
- package/dist/blog/validate.d.ts +42 -0
- package/dist/blog.js +23950 -0
- package/dist/blog.js.map +1 -0
- package/package.json +16 -3
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The blog post contract, version 1.
|
|
3
|
+
*
|
|
4
|
+
* A post is an MDX file in the site's repo, at
|
|
5
|
+
* `src/content/blog/<slug>.<locale>.mdx`. The file is the record, so its shape
|
|
6
|
+
* is a contract between everything that touches it: Pixel, which writes it;
|
|
7
|
+
* setto-server, which validates and commits it; the click-to-edit path, which
|
|
8
|
+
* patches one paragraph or one image in it; and this library, which renders
|
|
9
|
+
* it. A redesign of the blog may change how a component looks, never its name,
|
|
10
|
+
* its props or where posts live — old posts must keep rendering.
|
|
11
|
+
*
|
|
12
|
+
* MDX compiles to JavaScript, and these files are written by a model and
|
|
13
|
+
* patched by owners. So the contract is closed: frontmatter with known keys,
|
|
14
|
+
* CommonMark (plus GFM tables and strikethrough), and the four components
|
|
15
|
+
* below with quoted string props. No `import`, no `export`, no `{…}`. A post
|
|
16
|
+
* is data that happens to use JSX syntax, and is rendered from its syntax
|
|
17
|
+
* tree — it is never compiled and never run.
|
|
18
|
+
*
|
|
19
|
+
* Changing anything here is a contract change: bump `BLOG_CONTRACT_VERSION`
|
|
20
|
+
* and migrate the posts that exist, in the same release.
|
|
21
|
+
*/
|
|
22
|
+
export declare const BLOG_CONTRACT_VERSION = 1;
|
|
23
|
+
/** Where published posts live, relative to the site repo root. */
|
|
24
|
+
export declare const BLOG_DIR = "src/content/blog";
|
|
25
|
+
/** The public route the blog is served under. */
|
|
26
|
+
export declare const BLOG_ROUTE = "/blogg";
|
|
27
|
+
/** Lowercase words joined by single hyphens: `ny-kaffemaskin-i-butikken`. */
|
|
28
|
+
export declare const BLOG_SLUG_PATTERN: RegExp;
|
|
29
|
+
export declare const BLOG_SLUG_MAX = 80;
|
|
30
|
+
/** A site language code, as the locale files use them: `no`, `en`, `sv`. */
|
|
31
|
+
export declare const BLOG_LOCALE_PATTERN: RegExp;
|
|
32
|
+
/** Hard ceiling on a post file, so a runaway generation cannot be committed. */
|
|
33
|
+
export declare const BLOG_MAX_SOURCE_LENGTH = 200000;
|
|
34
|
+
export interface BlogFrontmatter {
|
|
35
|
+
/** The post's only top-level heading; the body starts at `##`. */
|
|
36
|
+
title: string;
|
|
37
|
+
/** The lead under the title, the meta description and the share text. */
|
|
38
|
+
description: string;
|
|
39
|
+
/** Publication date, `YYYY-MM-DD`. */
|
|
40
|
+
date: string;
|
|
41
|
+
/** Must match the file name. */
|
|
42
|
+
slug: string;
|
|
43
|
+
/** Must match the file name. */
|
|
44
|
+
locale: string;
|
|
45
|
+
/** Cover image (https), also the share image. Optional: the site's own is the fallback. */
|
|
46
|
+
cover?: string;
|
|
47
|
+
tags?: string[];
|
|
48
|
+
}
|
|
49
|
+
export declare const BLOG_FRONTMATTER_KEYS: readonly ["title", "description", "date", "slug", "locale", "cover", "tags"];
|
|
50
|
+
export declare const BLOG_TITLE_MAX = 120;
|
|
51
|
+
export declare const BLOG_DESCRIPTION_MAX = 300;
|
|
52
|
+
export declare const BLOG_TAGS_MAX = 8;
|
|
53
|
+
export declare const BLOG_TAG_MAX = 40;
|
|
54
|
+
export type BlogPropKind = 'text' | 'image-url' | 'youtube-id' | 'enum';
|
|
55
|
+
export interface BlogPropSpec {
|
|
56
|
+
kind: BlogPropKind;
|
|
57
|
+
required: boolean;
|
|
58
|
+
/** For `text`: the longest value accepted. */
|
|
59
|
+
max?: number;
|
|
60
|
+
/** For `enum`: the accepted values. */
|
|
61
|
+
values?: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What a component may contain:
|
|
65
|
+
* - `none`: self-closing, no children;
|
|
66
|
+
* - `images`: only `<Image>` elements (a gallery);
|
|
67
|
+
* - `markdown`: paragraphs and lists with inline formatting, no headings and
|
|
68
|
+
* no components.
|
|
69
|
+
*/
|
|
70
|
+
export type BlogChildren = 'none' | 'images' | 'markdown';
|
|
71
|
+
export interface BlogComponentSpec {
|
|
72
|
+
props: Readonly<Record<string, BlogPropSpec>>;
|
|
73
|
+
children: BlogChildren;
|
|
74
|
+
}
|
|
75
|
+
export declare const BLOG_GALLERY_MIN = 2;
|
|
76
|
+
export declare const BLOG_GALLERY_MAX = 24;
|
|
77
|
+
export declare const BLOG_COMPONENTS: {
|
|
78
|
+
readonly Image: {
|
|
79
|
+
readonly props: {
|
|
80
|
+
readonly src: {
|
|
81
|
+
readonly kind: "image-url";
|
|
82
|
+
readonly required: true;
|
|
83
|
+
};
|
|
84
|
+
readonly alt: {
|
|
85
|
+
readonly kind: "text";
|
|
86
|
+
readonly required: true;
|
|
87
|
+
readonly max: 300;
|
|
88
|
+
};
|
|
89
|
+
readonly caption: {
|
|
90
|
+
readonly kind: "text";
|
|
91
|
+
readonly required: false;
|
|
92
|
+
readonly max: 300;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
readonly children: "none";
|
|
96
|
+
};
|
|
97
|
+
readonly Gallery: {
|
|
98
|
+
readonly props: {
|
|
99
|
+
readonly caption: {
|
|
100
|
+
readonly kind: "text";
|
|
101
|
+
readonly required: false;
|
|
102
|
+
readonly max: 300;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
readonly children: "images";
|
|
106
|
+
};
|
|
107
|
+
readonly YouTube: {
|
|
108
|
+
readonly props: {
|
|
109
|
+
/** The 11-character video id, never a URL: no other embed can be smuggled in. */
|
|
110
|
+
readonly id: {
|
|
111
|
+
readonly kind: "youtube-id";
|
|
112
|
+
readonly required: true;
|
|
113
|
+
};
|
|
114
|
+
readonly title: {
|
|
115
|
+
readonly kind: "text";
|
|
116
|
+
readonly required: false;
|
|
117
|
+
readonly max: 200;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
readonly children: "none";
|
|
121
|
+
};
|
|
122
|
+
readonly Callout: {
|
|
123
|
+
readonly props: {
|
|
124
|
+
readonly tone: {
|
|
125
|
+
readonly kind: "enum";
|
|
126
|
+
readonly required: false;
|
|
127
|
+
readonly values: readonly ["info", "tip", "warning"];
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
readonly children: "markdown";
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
export type BlogComponentName = keyof typeof BLOG_COMPONENTS;
|
|
134
|
+
export declare const BLOG_COMPONENT_NAMES: BlogComponentName[];
|
|
135
|
+
export declare function isBlogComponentName(name: string): name is BlogComponentName;
|
|
136
|
+
export declare const YOUTUBE_ID_PATTERN: RegExp;
|
|
137
|
+
/** Link targets a post may use. Anything else, `javascript:` included, is refused. */
|
|
138
|
+
export declare const BLOG_LINK_SCHEMES: readonly ["https:", "http:", "mailto:", "tel:"];
|
|
139
|
+
export declare function isValidBlogSlug(slug: string): boolean;
|
|
140
|
+
export declare function isValidBlogLocale(locale: string): boolean;
|
|
141
|
+
/** `src/content/blog/<slug>.<locale>.mdx`. Throws on a slug or locale the contract refuses. */
|
|
142
|
+
export declare function blogPostPath(slug: string, locale: string): string;
|
|
143
|
+
/** The slug and locale a post path names, or null if it is not a post path. */
|
|
144
|
+
export declare function parseBlogPostPath(path: string): {
|
|
145
|
+
slug: string;
|
|
146
|
+
locale: string;
|
|
147
|
+
} | null;
|
|
148
|
+
/** The public URL path of a post. */
|
|
149
|
+
export declare function blogPostRoute(slug: string): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sevenfold/setto-client/blog` — the blog post contract.
|
|
3
|
+
*
|
|
4
|
+
* A separate entry so the MDX parser it needs never reaches a site's main
|
|
5
|
+
* bundle: setto-server and setto-agent validate with it, and the site's build
|
|
6
|
+
* step uses it to read posts.
|
|
7
|
+
*/
|
|
8
|
+
export * from './contract';
|
|
9
|
+
export { validateBlogPost, parseBlogPost } from './validate';
|
|
10
|
+
export type { BlogIssue, BlogPost, BlogValidationResult, BlogValidateOptions } from './validate';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a blog post file against the contract in `contract.ts`.
|
|
3
|
+
*
|
|
4
|
+
* setto-server runs this before it commits a post, and before it stores a
|
|
5
|
+
* draft Pixel wrote; the site renders only what passes. Issues name the
|
|
6
|
+
* frontmatter key or the body line and say what to write instead, because the
|
|
7
|
+
* first reader of a failure is usually a model that has to fix its own output.
|
|
8
|
+
*/
|
|
9
|
+
import type { Root } from 'mdast';
|
|
10
|
+
import { type BlogFrontmatter } from './contract';
|
|
11
|
+
export interface BlogIssue {
|
|
12
|
+
/** `frontmatter`, `frontmatter.<key>`, `body` or `path`. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** 1-based line in the file, when the issue has one. */
|
|
15
|
+
line?: number;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
export interface BlogPost {
|
|
19
|
+
frontmatter: BlogFrontmatter;
|
|
20
|
+
/** The body's syntax tree, frontmatter removed. What the renderer walks. */
|
|
21
|
+
body: Root;
|
|
22
|
+
}
|
|
23
|
+
export type BlogValidationResult = {
|
|
24
|
+
ok: true;
|
|
25
|
+
post: BlogPost;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
issues: BlogIssue[];
|
|
29
|
+
};
|
|
30
|
+
export interface BlogValidateOptions {
|
|
31
|
+
/** The repo path the file is (or will be) committed at; slug and locale must match it. */
|
|
32
|
+
path?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Hosts images may come from (`blob.vercel-storage.com` also admits its
|
|
35
|
+
* subdomains). Omitted, any https URL is accepted — the server passes its
|
|
36
|
+
* media store so a post cannot hotlink.
|
|
37
|
+
*/
|
|
38
|
+
imageHosts?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function validateBlogPost(source: string, options?: BlogValidateOptions): BlogValidationResult;
|
|
41
|
+
/** Like `validateBlogPost`, but throws `Invalid blog post at <where>: <message>` on the first issue. */
|
|
42
|
+
export declare function parseBlogPost(source: string, options?: BlogValidateOptions): BlogPost;
|