Skip to content

Commit b459c91

Browse files
authored
Make malformed blog post error at compile time (rescript-lang#365)
* Decode blog post upstream * Dedupe postData payload Additionally, the validation should be done as early as possible. Don't let it trickle down. All the bad data info should be gathered at compile time, not runtime dev time * Remove dead module * Remove paragraph in blogpost-guide that's no longer needed Fewer instructions for writing a post
1 parent 6ee6c61 commit b459c91

File tree

10 files changed

+91
-238
lines changed

10 files changed

+91
-238
lines changed

pages/blogpost-guide.mdx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ Refresh your browser within [/blog](/blog). You should now see a warning for som
3232
Each blogpost requires a certain set of meta information (so called
3333
"frontmatter"), otherwise it can't be displayed in the blog.
3434

35-
In
36-
development mode, you will see error messages for invalid frontmatter on the
37-
`localhost:3000/blog` url. Blogposts with invalid frontmatter will not be
38-
rendered and displayed in production, so make sure to fix those errors.
39-
4035
Frontmatter is put **on the top of your file**. Here is a fully working example
4136
of all available attributes:
4237

src/Blog.mjs

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import * as DateStr from "./common/DateStr.mjs";
1414
import * as Markdown from "./components/Markdown.mjs";
1515
import * as Belt_Array from "rescript/lib/es6/belt_Array.js";
1616
import * as Navigation from "./components/Navigation.mjs";
17-
import * as ProcessEnv from "./common/ProcessEnv.mjs";
1817
import * as Caml_option from "rescript/lib/es6/caml_option.js";
1918
import * as BlogFrontmatter from "./common/BlogFrontmatter.mjs";
2019

@@ -169,32 +168,13 @@ function Blog$FeatureCard(Props) {
169168
})));
170169
}
171170

172-
var Post = {};
173-
174-
var Malformed = {};
175-
176171
function $$default(props) {
177-
var malformed = props.malformed;
178172
var posts = props.posts;
179173
var match = React.useState(function () {
180174
return /* All */0;
181175
});
182176
var setSelection = match[1];
183177
var currentSelection = match[0];
184-
var errorBox = ProcessEnv.env === ProcessEnv.development && malformed.length !== 0 ? React.createElement("div", {
185-
className: "mb-12"
186-
}, React.createElement(Markdown.Warn.make, {
187-
children: null
188-
}, React.createElement("h2", {
189-
className: "font-bold text-gray-95 text-32 mb-2"
190-
}, "Some Blog Posts are Malformed!"), React.createElement("p", undefined, "Any blog post with invalid data will not be displayed in production."), React.createElement("div", undefined, React.createElement("p", {
191-
className: "font-bold mt-4"
192-
}, "Errors:"), React.createElement("ul", undefined, Belt_Array.mapWithIndex(malformed, (function (i, m) {
193-
return React.createElement("li", {
194-
key: String(i),
195-
className: "list-disc ml-5"
196-
}, "pages/blog/" + (m.id + (".mdx: " + m.message)));
197-
})))))) : null;
198178
var content;
199179
if (posts.length === 0) {
200180
content = React.createElement("div", {
@@ -215,7 +195,7 @@ function $$default(props) {
215195
title: first.frontmatter.title,
216196
author: first.frontmatter.author,
217197
date: DateStr.toDate(first.frontmatter.date),
218-
slug: first.id
198+
slug: BlogApi.blogPathToSlug(first.path)
219199
};
220200
var tmp$1 = Caml_option.null_to_opt(first.frontmatter.previewImg);
221201
if (tmp$1 !== undefined) {
@@ -240,8 +220,8 @@ function $$default(props) {
240220
title: post.frontmatter.title,
241221
author: post.frontmatter.author,
242222
date: DateStr.toDate(post.frontmatter.date),
243-
slug: post.id,
244-
key: post.id
223+
slug: BlogApi.blogPathToSlug(post.path),
224+
key: post.path
245225
};
246226
var tmp$1 = Caml_option.null_to_opt(post.frontmatter.previewImg);
247227
if (tmp$1 !== undefined) {
@@ -298,68 +278,39 @@ function $$default(props) {
298278
style: {
299279
maxWidth: "66.625rem"
300280
}
301-
}, errorBox, content))
281+
}, content))
302282
}))), React.createElement(Footer.make, {}))));
303283
}
304284

305285
function getStaticProps(_ctx) {
306286
var match = Belt_Array.reduce(BlogApi.getAllPosts(undefined), [
307-
[],
308287
[],
309288
[]
310289
], (function (acc, postData) {
311-
var archived = acc[2];
312-
var malformed = acc[1];
290+
var archived = acc[1];
313291
var posts = acc[0];
314-
var id = BlogApi.blogPathToSlug(postData.path);
315-
var decoded = BlogFrontmatter.decode(postData.frontmatter);
316-
if (decoded.TAG === /* Ok */0) {
317-
var frontmatter = decoded._0;
318-
if (postData.archived) {
319-
archived.push({
320-
id: id,
321-
frontmatter: frontmatter
322-
});
323-
} else {
324-
posts.push({
325-
id: id,
326-
frontmatter: frontmatter
327-
});
328-
}
329-
return [
330-
posts,
331-
malformed,
332-
archived
333-
];
292+
if (postData.archived) {
293+
archived.push(postData);
294+
} else {
295+
posts.push(postData);
334296
}
335-
var m_message = decoded._0;
336-
var m = {
337-
id: id,
338-
message: m_message
339-
};
340-
var malformed$1 = Belt_Array.concat(malformed, [m]);
341297
return [
342298
posts,
343-
malformed$1,
344299
archived
345300
];
346301
}));
347302
var props_posts = match[0];
348-
var props_archived = match[2];
349-
var props_malformed = match[1];
303+
var props_archived = match[1];
350304
var props = {
351305
posts: props_posts,
352-
archived: props_archived,
353-
malformed: props_malformed
306+
archived: props_archived
354307
};
355308
return Promise.resolve({
356309
props: props
357310
});
358311
}
359312

360313
export {
361-
Post ,
362-
Malformed ,
363314
defaultPreviewImg ,
364315
$$default ,
365316
$$default as default,

src/Blog.res

Lines changed: 19 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -207,56 +207,16 @@ module FeatureCard = {
207207

208208
type params = {slug: string}
209209

210-
module Post = {
211-
type t = {
212-
id: string,
213-
frontmatter: BlogFrontmatter.t,
214-
}
215-
}
216-
217-
module Malformed = {
218-
type t = {
219-
id: string,
220-
message: string,
221-
}
222-
}
223-
224210
type props = {
225-
posts: array<Post.t>,
226-
archived: array<Post.t>,
227-
malformed: array<Malformed.t>,
211+
posts: array<BlogApi.post>,
212+
archived: array<BlogApi.post>,
228213
}
229214

230215
let default = (props: props): React.element => {
231-
let {posts, malformed, archived} = props
216+
let {posts, archived} = props
232217

233218
let (currentSelection, setSelection) = React.useState(() => CategorySelector.All)
234219

235-
let errorBox = if ProcessEnv.env === ProcessEnv.development && Belt.Array.length(malformed) > 0 {
236-
<div className="mb-12">
237-
<Markdown.Warn>
238-
<h2 className="font-bold text-gray-95 text-32 mb-2">
239-
{React.string("Some Blog Posts are Malformed!")}
240-
</h2>
241-
<p>
242-
{React.string("Any blog post with invalid data will not be displayed in production.")}
243-
</p>
244-
<div>
245-
<p className="font-bold mt-4"> {React.string("Errors:")} </p>
246-
<ul>
247-
{Belt.Array.mapWithIndex(malformed, (i, m) =>
248-
<li key={i->Belt.Int.toString} className="list-disc ml-5">
249-
{React.string("pages/blog/" ++ (m.id ++ (".mdx: " ++ m.message)))}
250-
</li>
251-
)->React.array}
252-
</ul>
253-
</div>
254-
</Markdown.Warn>
255-
</div>
256-
} else {
257-
React.null
258-
}
259-
260220
let content = if Belt.Array.length(posts) === 0 {
261221
/* <div> {React.string("Currently no posts available")} </div>; */
262222
<div className="mt-8">
@@ -284,7 +244,7 @@ let default = (props: props): React.element => {
284244
author=first.frontmatter.author
285245
firstParagraph=?{first.frontmatter.description->Js.Null.toOption}
286246
date={first.frontmatter.date->DateStr.toDate}
287-
slug=first.id
247+
slug=BlogApi.blogPathToSlug(first.path)
288248
/>
289249
</div>
290250

@@ -297,13 +257,13 @@ let default = (props: props): React.element => {
297257
let badge = post.frontmatter.badge->Js.Null.toOption
298258

299259
<BlogCard
300-
key={post.id}
260+
key={post.path}
301261
previewImg=?{post.frontmatter.previewImg->Js.Null.toOption}
302262
title=post.frontmatter.title
303263
author=post.frontmatter.author
304264
?badge
305265
date={post.frontmatter.date->DateStr.toDate}
306-
slug=post.id
266+
slug=BlogApi.blogPathToSlug(post.path)
307267
/>
308268
})->React.array}
309269
</div>
@@ -339,7 +299,7 @@ let default = (props: props): React.element => {
339299
<Mdx.Provider components=Markdown.default>
340300
<div className="flex justify-center">
341301
<div className="w-full" style={ReactDOMStyle.make(~maxWidth="66.625rem", ())}>
342-
errorBox content
302+
content
343303
</div>
344304
</div>
345305
</Mdx.Provider>
@@ -352,32 +312,21 @@ let default = (props: props): React.element => {
352312
}
353313

354314
let getStaticProps: Next.GetStaticProps.t<props, params> = _ctx => {
355-
let (posts, malformed, archived) =
356-
BlogApi.getAllPosts()
357-
->Belt.Array.reduce(([], [], []), (acc, postData) => {
358-
let (posts, malformed, archived) = acc
359-
let id = BlogApi.blogPathToSlug(postData.path)
360-
361-
let decoded = BlogFrontmatter.decode(postData.frontmatter)
362-
363-
switch decoded {
364-
| Error(message) =>
365-
let m = {Malformed.id: id, message: message}
366-
let malformed = Belt.Array.concat(malformed, [m])
367-
(posts, malformed, archived)
368-
| Ok(frontmatter) =>
369-
if postData.archived {
370-
Js.Array2.push(archived, {Post.id: id, frontmatter: frontmatter})->ignore
371-
} else {
372-
Js.Array2.push(posts, {Post.id: id, frontmatter: frontmatter})->ignore
373-
}
374-
(posts, malformed, archived)
375-
}
376-
})
315+
let (posts, archived) = BlogApi.getAllPosts()->Belt.Array.reduce(([], []), (
316+
acc,
317+
postData,
318+
) => {
319+
let (posts, archived) = acc
320+
if postData.archived {
321+
Js.Array2.push(archived, postData)->ignore
322+
} else {
323+
Js.Array2.push(posts, postData)->ignore
324+
}
325+
(posts, archived)
326+
})
377327

378328
let props = {
379329
posts: posts,
380-
malformed: malformed,
381330
archived: archived,
382331
}
383332

src/Blog.resi

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
module Post: {
2-
type t = {
3-
id: string,
4-
frontmatter: BlogFrontmatter.t,
5-
}
6-
}
7-
8-
module Malformed: {
9-
type t = {
10-
id: string,
11-
message: string,
12-
}
13-
}
14-
151
let defaultPreviewImg: string
162

17-
type params = {slug: string}
18-
type props = {
19-
posts: array<Post.t>,
20-
archived: array<Post.t>,
21-
malformed: array<Malformed.t>,
22-
}
3+
type params
4+
type props
235

246
let default: props => React.element
257

src/common/BlogApi.mjs

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as Fs from "fs";
44
import * as Path from "path";
5+
import * as Js_exn from "rescript/lib/es6/js_exn.js";
56
import * as $$String from "rescript/lib/es6/string.js";
67
import * as DateStr from "./DateStr.mjs";
78
import * as Process from "process";
@@ -25,19 +26,29 @@ function getAllPosts(param) {
2526
};
2627
var nonArchivedPosts = mdxFiles(postsDirectory).map(function (path) {
2728
var match = GrayMatter(Fs.readFileSync(Path.join(postsDirectory, path), "utf8"));
28-
return {
29-
path: path,
30-
archived: false,
31-
frontmatter: match.data
32-
};
29+
var msg = BlogFrontmatter.decode(match.data);
30+
if (msg.TAG === /* Ok */0) {
31+
return {
32+
path: path,
33+
archived: false,
34+
frontmatter: msg._0
35+
};
36+
} else {
37+
return Js_exn.raiseError(msg._0);
38+
}
3339
});
3440
var archivedPosts = mdxFiles(archivedPostsDirectory).map(function (path) {
3541
var match = GrayMatter(Fs.readFileSync(Path.join(archivedPostsDirectory, path), "utf8"));
36-
return {
37-
path: Path.join("archive", path),
38-
archived: true,
39-
frontmatter: match.data
40-
};
42+
var msg = BlogFrontmatter.decode(match.data);
43+
if (msg.TAG === /* Ok */0) {
44+
return {
45+
path: Path.join("archive", path),
46+
archived: true,
47+
frontmatter: msg._0
48+
};
49+
} else {
50+
return Js_exn.raiseError(msg._0);
51+
}
4152
});
4253
return nonArchivedPosts.concat(archivedPosts).sort(function (a, b) {
4354
return $$String.compare(Path.basename(b.path), Path.basename(a.path));
@@ -52,24 +63,16 @@ function dateToUTCString(date) {
5263
function getLatest(maxOpt, baseUrlOpt, param) {
5364
var max = maxOpt !== undefined ? maxOpt : 10;
5465
var baseUrl = baseUrlOpt !== undefined ? baseUrlOpt : "https://rescript-lang.org";
55-
return Belt_Array.reduce(getAllPosts(undefined), [], (function (acc, next) {
56-
var fm = BlogFrontmatter.decode(next.frontmatter);
57-
if (fm.TAG !== /* Ok */0) {
58-
return acc;
59-
}
60-
var fm$1 = fm._0;
61-
var description = Belt_Option.getWithDefault(Caml_option.null_to_opt(fm$1.description), "");
62-
var item_title = fm$1.title;
63-
var item_href = baseUrl + "/blog/" + blogPathToSlug(next.path);
64-
var item_pubDate = DateStr.toDate(fm$1.date);
65-
var item = {
66-
title: item_title,
67-
href: item_href,
68-
description: description,
69-
pubDate: item_pubDate
70-
};
71-
return Belt_Array.concat(acc, [item]);
72-
})).slice(0, max);
66+
return getAllPosts(undefined).map(function (post) {
67+
var fm = post.frontmatter;
68+
var description = Belt_Option.getWithDefault(Caml_option.null_to_opt(fm.description), "");
69+
return {
70+
title: fm.title,
71+
href: baseUrl + "/blog/" + blogPathToSlug(post.path),
72+
description: description,
73+
pubDate: DateStr.toDate(fm.date)
74+
};
75+
}).slice(0, max);
7376
}
7477

7578
function toXmlString(siteTitleOpt, siteDescriptionOpt, items) {

0 commit comments

Comments
 (0)