diff --git a/package.json b/package.json index c14176d..e3ca823 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "dotenv": "^16.3.1", "feather-icons": "^4.29.1", "findup-sync": "^5.0.0", - "gray-matter": "^4.0.3", "happy-dom": "^12.0.1", "levenshtein": "^1.0.5", "marked": "^9.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e49c1dc..2fe587a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,6 @@ dependencies: findup-sync: specifier: ^5.0.0 version: 5.0.0 - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 happy-dom: specifier: ^12.0.1 version: 12.0.1 diff --git a/posts.json b/posts.json index 13d06ac..5bb34e8 100644 --- a/posts.json +++ b/posts.json @@ -7037,15 +7037,33 @@ },{ "slug": "astroblog", "source": "doc.bmndr.co/astroblog", -"redirects": [] +"redirects": [], +"disqus_id": "astroblog", +"date": "2023-09-06", +"author": "Nathan Arthur", +"tags": ["bee-all", "navel-gazing", "nerdery", "meta"], +"status": "publish", +"excerpt": "This is another X-Treme Nerd Interlude post. Last time we announced, mercifully briefly, our shiny new blog redesign and if you’re a normal human you should read that, nod thoughtfully, say “looks lovely”, and be on your merry way. The rest of you can frolic deep in the weeds here with" },{ "slug": "predict", "source": "doc.bmndr.co/predict", -"redirects": [] +"redirects": [], +"disqus_id": "predict", +"date": "2023-09-20", +"author": "dreeves", +"tags": ["bee-all", "rationality", "prediction markets", "nerdery", "history of beeminder", "startups", "commitment contracts"], +"status": "publish", +"excerpt": "A fun fact about predicting your own behavior, particularly publicly, is that the act of predicting it changes the prediction. “I’m 75% likely to maintain my Duolingo streak all year, but now that I’ve said so I’m actually 90% likely, but now that I’ve said that, …” Or what happens when the probability starts very low but you add a wager? It’s like this self-describing xkcd" },{ "slug": "readwise", "source": "doc.bmndr.co/readwise", -"redirects": [] +"redirects": [], +"disqus_id": "readwise", +"date": "2023-09-27", +"author": "Adam Wolf", +"tags": ["akrasia", "bee-all", "get everything done", "integrations", "PSA", "rationality", "startups", "reading"], +"status": "publish", +"excerpt": "Readwise Reader is a powerful tool for “power readers”. It’s like a supercharged read-it-later app, with first-class support for notes and highlights and tags. Now, you can keep track of your Readwise Reader items using Beeminder. You save things like web pages, PDFs, YouTube videos, Twitter threads, or aim it at an RSS feed or an" },{ "slug": "college", "source": "doc.bmndr.co/college", diff --git a/src/env.d.ts b/src/env.d.ts index f964fe0..e815b35 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1 +1,3 @@ +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// /// diff --git a/src/lib/__snapshots__/canonicalizeUrl.spec-snapshot.ts.snap b/src/lib/__snapshots__/canonicalizeUrl.spec-snapshot.ts.snap index b08b4fc..0253114 100644 --- a/src/lib/__snapshots__/canonicalizeUrl.spec-snapshot.ts.snap +++ b/src/lib/__snapshots__/canonicalizeUrl.spec-snapshot.ts.snap @@ -532,6 +532,8 @@ exports[`canonicalizeUrl > handles source 'doc.bmndr.co/qs' 1`] = `"https://the_ exports[`canonicalizeUrl > handles source 'doc.bmndr.co/qs2013' 1`] = `"https://the_source_domain/qs2013/export/txt"`; +exports[`canonicalizeUrl > handles source 'doc.bmndr.co/rainbowdash' 1`] = `"https://the_source_domain/rainbowdash/export/txt"`; + exports[`canonicalizeUrl > handles source 'doc.bmndr.co/ratchet' 1`] = `"https://the_source_domain/ratchet/export/txt"`; exports[`canonicalizeUrl > handles source 'doc.bmndr.co/reactions' 1`] = `"https://the_source_domain/reactions/export/txt"`; diff --git a/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap b/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap index 4542378..506b537 100644 --- a/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap +++ b/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap @@ -5247,7 +5247,7 @@ You can report those in the comments or " `; -exports[`body > post athletes hash 20c50fcee4500274200a6f7a548f4a9a 1`] = ` +exports[`body > post athletes hash a3590e7bf12a75f1a173a33fe890c412 1`] = ` "

post blogmorphosis hash 8e28c0cd4c82241ad375991ded12234c 1`] = ` +exports[`body > post blogmorphosis hash 4ed52deb9d78c831d6ae26d8f6dbd82e 1`] = ` "

post glossary hash 7f58cd7783ab93d2bc4a2f19949eb6cd 1`] = ` +exports[`body > post glossary hash d9f14e73b59814a412cab69f13c78c66 1`] = ` "

n - o + + o + p @@ -42342,7 +42344,7 @@ as we notice nomenclature that isn’t self-evident or terms change. Akrasia

- : Procrastination, impetuousness, failure of willpower, acting against one’s own better judgment. + Procrastination, impetuousness, failure of willpower, acting against one’s own better judgment. Doing something you regret even as you’re doing it. See the sidebar of this blog, or check out our .

-Procrastination, impetuousness, failure of willpower, acting against one’s own better judgment. -Doing something you regret even as you’re doing it. -See the sidebar of this blog, or check out our - - Anti-Akrasia Manifesto - -. - -—>

Akrasia Horizon

- : Akrasia @@ -42379,6 +42368,7 @@ See the sidebar of this blog, or check out our quantifies “immediate”. It’s the timeframe within which your short-term impulses outweigh your better judgment — taken by Beeminder to always be one week in the future. + You can make arbitrary changes to your goal — the steepness of your bright red line @@ -42410,7 +42400,7 @@ See also Akrasia-proofing

- : The delay Beeminder imposes on making changes to your goal so that you’re always making decisions beyond you + The delay Beeminder imposes on making changes to your goal so that you’re always making decisions beyond you akrasia horizon @@ -42425,7 +42415,7 @@ See also Akratic

- : The adjectival form of + The adjectival form of akrasia @@ -42453,7 +42443,7 @@ UPDATE: Akratics Anonymous has been subsumed by the amazing All-you-can-eat-buffet-hopping vacation

- : A hypothetical reason to make a weight-loss + A hypothetical reason to make a weight-loss bright red line @@ -42468,7 +42458,7 @@ For most goals it’s sufficient to add a Archive

- : To get a goal out of your gallery — used to cry uncle on a goal. + To get a goal out of your gallery — used to cry uncle on a goal. The goal will be archived after one week, i.e., subject to the akrasia horizon @@ -42481,7 +42471,7 @@ Newly created goals that you archive will also give you the option to blow them Autodata goal

- : A goal that has an automatic data source, like + A goal that has an automatic data source, like

- : A premium feature to automatically + A premium feature to automatically ratchet @@ -42559,7 +42549,7 @@ The full list of autodata sources is on the Beeminder Auto-summing

- : Goals that are + Goals that are auto-summing @@ -42571,7 +42561,7 @@ Exceptions include weight loss and odometer goals and whittle-down goals. Auto-widening

- : This + This

- : This isn’t a thing anymore! + This isn’t a thing anymore!

Bad side of the line

- : The + The bright red line @@ -42608,7 +42598,7 @@ If you end the day (whenever you set your deadline to) on the wrong side, you The beehive

- : Beeminder headquarters. + Beeminder headquarters.

- : Daily, weekly, monthly, or yearly emails that we (the founders) send to keep everyone in the loop on what’s new with Beeminder. + Daily, weekly, monthly, or yearly emails that we (the founders) send to keep everyone in the loop on what’s new with Beeminder. By default you’ll get them monthly but you can change that by going to Your Account → Settings → @@ -42641,7 +42631,7 @@ If you’re reading our glossary then you’ll probably enjoy getting da Beemergency

- : A day when your + A day when your datapoint @@ -42666,7 +42656,7 @@ by midnight at the end of this day. To beemind

- : To use Beeminder to track and commit to a goal. + To use Beeminder to track and commit to a goal. Often used transitively, as in “I’m beeminding my donut consumption.” Think of “beeminding X” as an intensified version of “minding X”.

@@ -42674,7 +42664,7 @@ Think of “beeminding X” as an intensified version of “minding Bot

- : The program you interact with by email or SMS. + The program you interact with by email or SMS. It reminds you to add data, you reply to it with new data, and it warns you if you’re about to derail @@ -42685,7 +42675,7 @@ It reminds you to add data, you reply to it with new data, and it warns you if y Bright red line

- : The line demarcating the + The line demarcating the good side @@ -42703,7 +42693,7 @@ If your progress perfectly followed the bright red line, you would reach your go Centerline

- : Not a thing anymore! + Not a thing anymore! See bright red line @@ -42714,7 +42704,7 @@ See Commitment contract (also: commitment device)

- : A means by which you constrain your own future behavior for the purposes of thwarting + A means by which you constrain your own future behavior for the purposes of thwarting akrasia @@ -42743,7 +42733,7 @@ See also Commitment dial

- : The three parameters, shown beneath your graph, that determine your goal and the steepness of the + The three parameters, shown beneath your graph, that determine your goal and the steepness of the bright red line @@ -42778,7 +42768,7 @@ See Cumulative

- : See + See auto-summing @@ -42791,7 +42781,7 @@ See Datapoints

- : The values you enter into Beeminder, or that Beeminder enters for you in the case of + The values you enter into Beeminder, or that Beeminder enters for you in the case of autodata goals @@ -42822,7 +42812,7 @@ red means you’re going to Derail

- : To allow your + To allow your datapoints @@ -42851,7 +42841,7 @@ If you had a Dial up/down

- : To make your + To make your bright red line @@ -42875,7 +42865,7 @@ Changes you make to your red line have a one-week delay before they take effect, Do Less goal

- : Formerly known as + Formerly known as “

- : This is the most common Beeminder goal type. + This is the most common Beeminder goal type. You enter a value every time you do something you want to do more of — minutes of exercise, number of workouts, servings of vegetables, hours of work — and Beeminder auto-sums @@ -42932,7 +42922,7 @@ Think of them as goals to do more (or less) than you would otherwise do if left Emergency day (eep day)

- : See + See beemergency @@ -42945,7 +42935,7 @@ Think of them as goals to do more (or less) than you would otherwise do if left Flatline / flatlining

- : When you report no data, Beeminder assumes that you did nothing (or stayed the same weight). + When you report no data, Beeminder assumes that you did nothing (or stayed the same weight). We add a placeholder datapoint @@ -42966,7 +42956,7 @@ Exception: Flat spot

- : A period in which your + A period in which your bright red line @@ -42995,7 +42985,7 @@ When you start a new goal or Freebee

- : A freebee is a goal with a $0 pledge. + A freebee is a goal with a $0 pledge. (Historical note: Freebees used to refer to

- : If you + If you derail @@ -43039,11 +43029,11 @@ You can still add data to a frozen goal but the won’t remind you to do so and the graph won’t update when you do.

-

- Frontburner (above the fold) {.def} +

+ Frontburner (above the fold)

- : See + See backburner @@ -43052,11 +43042,11 @@ You can still add data to a frozen goal but the

G

-

- Good side of the line/road {.def} +

+ Good side of the line/road

- : See + See bad side @@ -43069,7 +43059,7 @@ You can still add data to a frozen goal but the Legit check

- : An email that the + An email that the bot @@ -43090,7 +43080,7 @@ It’s slightly cumbersome, by design, but if the derailment was in any way Lifehacking

- : The use of clever or perhaps unorthodox tricks to improve one’s life, particularly one’s own behavior. + The use of clever or perhaps unorthodox tricks to improve one’s life, particularly one’s own behavior. Beeminder is a hardcore lifehack.

@@ -43100,7 +43090,7 @@ Beeminder is a hardcore lifehack. Mercy

- : Old name for post-derail + Old name for post-derail respite @@ -43110,7 +43100,7 @@ Beeminder is a hardcore lifehack. Motivation point

- : The minimum + The minimum pledge @@ -43144,7 +43134,7 @@ Also known as the psychological bite point (thanks to Noah Wilde for the term). No-Excuses Mode

- : If you check this box (press this button) in your goal settings then you’re precommitting to accept all derailments as final, regardless of any extenuating circumstances. + If you check this box (press this button) in your goal settings then you’re precommitting to accept all derailments as final, regardless of any extenuating circumstances. In other words, if you cry foul in response to a legit check @@ -43172,7 +43162,7 @@ Details in the No-mercy rerail

- : Normally you automatically + Normally you automatically rerail @@ -43191,11 +43181,14 @@ Check the no-mercy checkbox in your goal settings if you want to be immediately .

+

+ O +

Odometer goal

- : A goal that is not + A goal that is not auto-summing @@ -43218,7 +43211,7 @@ If you report a zero then Beeminder treats that as the odometer getting reset (o Panic threshold

- : Not a thing anymore! We got rid of this! + Not a thing anymore! We got rid of this! Instead you can now directly set for each goal the time of day that Beeminder starts sending reminders. One less glossary term to worry about!

@@ -43226,7 +43219,7 @@ One less glossary term to worry about! Pessimistic presumptive reports

- : This applies to + This applies to Do Less @@ -43260,7 +43253,7 @@ See Pledge

- : The amount of money you have committed to pay if you + The amount of money you have committed to pay if you derail @@ -43300,7 +43293,7 @@ You can lower your pledge for free but it’s subject to the Precommit to recommit

- : This is the fundamental tenet of Beeminder that, unless you’ve explicitly hit + This is the fundamental tenet of Beeminder that, unless you’ve explicitly hit archive @@ -43319,7 +43312,7 @@ It has a name because, historically, it was something you had to specifically op Premium plans

- : We intend for Beeminder to always be a totally free awesomeness-inducer (free if you don’t cross the + We intend for Beeminder to always be a totally free awesomeness-inducer (free if you don’t cross the bright red line @@ -43345,7 +43338,7 @@ you’ll get Programmable self

- : The next stage of evolution of + The next stage of evolution of quantified self @@ -43366,7 +43359,7 @@ you’ll get Quantified self

- : “Self knowledge through numbers”. + “Self knowledge through numbers”. (See

- : Changing the + Changing the bright red line @@ -43410,7 +43403,7 @@ Ratcheting adjusts your bright red line to be closer to your current Rate/steepness

- : The number of goal actions or units you are commmitted to completing per week (or per day). + The number of goal actions or units you are commmitted to completing per week (or per day). A rate of zero means a flat spot @@ -43434,7 +43427,7 @@ The rate can be adjusted subject to the Rerail

- : To recommit to a goal after you’ve + To recommit to a goal after you’ve derailed @@ -43463,7 +43456,7 @@ The Respite

- : Previously known as “mercy” and now as + Previously known as “mercy” and now as post-derail respite @@ -43494,17 +43487,17 @@ For a Retroratchet

- : Old name for + Old name for ratchet .

-

- Right lane {.def} +

+ Right lane

- : Not a thing anymore! + Not a thing anymore! There’s just a single bright red line @@ -43515,17 +43508,17 @@ There’s just a single Road

- : See + See yellow brick road .

-

- Road dial {.def} +

+ Road dial

- : See + See commitment dial @@ -43538,7 +43531,7 @@ There’s just a single Safety buffer, safe days

- : The number of days you can go without reporting data or doing what you committed to before you + The number of days you can go without reporting data or doing what you committed to before you derail @@ -43557,7 +43550,7 @@ If you are at zero safe days then the next day is a Set-A-Limit goal

- : Former name for + Former name for Do Less @@ -43567,7 +43560,7 @@ If you are at zero safe days then the next day is a Short-circuit

- : To increase your + To increase your pledge @@ -43591,7 +43584,7 @@ For example, if you’re jumping from a $10 pledge to a $30 pledge, you must SOS clause

- : If something truly unexpected happens, such as physical injury, that prevents you from staying on the right side of the + If something truly unexpected happens, such as physical injury, that prevents you from staying on the right side of the bright red line @@ -43618,7 +43611,7 @@ Email (Getting) stung

- : See + See derail @@ -43628,7 +43621,7 @@ Email Supporters

- : Supporters are friends/family/enemies who will be cc’d on any + Supporters are friends/family/enemies who will be cc’d on any legit check @@ -43654,7 +43647,7 @@ See Urgency load

- : The idea of Urgency Load is to construct a single number that captures how edge-skatey you are across all your goals. + The idea of Urgency Load is to construct a single number that captures how edge-skatey you are across all your goals. Every day of safety buffer @@ -43680,7 +43673,7 @@ And adding new goals can at best leave your urgency load unchanged, if you keep User-Visible Improvement (UVI)

- : Early on, before we even publicly launched, the Beeminder founders, being dogfood maniacs, hard-committed to averaging one user-visible improvement to Beeminder every day. + Early on, before we even publicly launched, the Beeminder founders, being dogfood maniacs, hard-committed to averaging one user-visible improvement to Beeminder every day. We sometimes describe the day’s UVI @@ -43724,7 +43717,7 @@ We sometimes describe the day’s Uservoice

- : The old home of our feedback forum, now superceded by the amazing + The old home of our feedback forum, now superceded by the amazing

- : Weaseling @@ -43767,10 +43759,10 @@ We sometimes describe the day’s weasel-proofing - ) {#weaselproof .def} + ) {#weaselproof}

- : See + See No-Excuses Mode @@ -43779,7 +43771,7 @@ We sometimes describe the day’s Whittle-down goal

- : Like an + Like an odometer @@ -43790,7 +43782,7 @@ Like if you want to whittle down the number of messages in your inbox. Wrong lane

- : Not a thing anymore! + Not a thing anymore! There’s just a single bright red line @@ -43804,7 +43796,7 @@ There’s just a single Yellow brick road

- : The path to your goal. + The path to your goal. This is/was drawn as a literal yellow brick road on your graph and you’re committing to keep your datapoints @@ -43831,7 +43823,7 @@ UPDATE: We’ve since Yellow guiding lines

- : These appear above or below your goal to indicate which side of the + These appear above or below your goal to indicate which side of the bright red line @@ -43873,7 +43865,7 @@ It will take effect after the akrasia horizon (7 days) but since you have that m Zeno polling

- : If Zeno polling is enabled, you’ll get reminded ever more persistently on + If Zeno polling is enabled, you’ll get reminded ever more persistently on beemergency days @@ -58303,7 +58295,7 @@ Check out " `; -exports[`body > post mirabai hash 3f1c77fa939fec1aa0bfc398445861b4 1`] = ` +exports[`body > post mirabai hash 1a624aa6f1eb83810a40dec7f7159219 1`] = ` "

Business -

+

Reply Zero -: Answer all emails tagged with a @reply label. +
+

+ Answer all emails tagged with a @reply label. Beeminder has cured me of Inbox Sprawl by training me to achieve Inbox Zero several times a day, so my only remaining challenge is to reply to everything requiring an answer in a timely manner.

-

+

Blog -: I get a point every time I make an entry on +
+

+ I get a point every time I make an entry on Household -

+

Food Money -: I’ve been using +
+

+ I’ve been using -

+

Cheaper Groceries -: I love expensive grocery stores. It’s a terrible habit. +
+

+ I love expensive grocery stores. It’s a terrible habit. This goal gives me a point whenever I go to an affordable grocery store, and takes away a point whenever I go to one of the absurdly pricey fancypants stores that abound in New York City.

“[Habitica] is a perfect complement to Beeminder”

-

+

HabitRPG -: Beeminder only tracks to-dos, which I reserve for apartment cleaning tasks, but I’ve been using HabitRPG +
+

+ Beeminder only tracks to-dos, which I reserve for apartment cleaning tasks, but I’ve been using HabitRPG Health -

+

Swim or Gym -: I get a point every time I go swimming at the pool downtown or to the cardio room at the gym across the street. -

+

+ I get a point every time I go swimming at the pool downtown or to the cardio room at the gym across the street. +

+
RunKeeper -: Straightforward mile-based cycling goal. +
+

+ Straightforward mile-based cycling goal. Just bought my own bike last month, so I don’t always have to use Citibike anymore! Instead of only biking on my daily commute, I can actually do it for fun, in my own neighborhood. Beeminder’s @@ -58483,15 +58489,19 @@ Beeminder’s makes it simple.

-

+

Junk -: Every day I give myself three points. +
+

+ Every day I give myself three points. If I choose to consume red meat, crunchy fried things, sugary stuff, caffeine, or alcohol, I have to give back a point. I need 16 points a week to stay in the green.

-

+

Sleep as Android -: Another great +
+

+ Another great post rainbowdash hash b9c7d7101c6e3560133663cbd90073e1 1`] = ` +" +

+ \\"Rainbow +

+

+ You know what we heard people like? +Listicles! +Here are the top 5 new Beeminder features and updates we randomly want to tell you about! +If your favorite isn’t here, check the + + full list of 4,812 of them + + . +(You read that right — Beeminder’s over-a-decade-long changelog has close to 5,000 entries and counting.) +

+

+ 1. The help docs! +

+

+ Support Czar Nicky diligently keeps them impeccably up to date. +If you have a question about a Beeminder thing, it’s probably in there! +Head to + + help.beeminder.com + + or just google something like “Beeminder akrasia horizon” and generally one of our articles is the first hit. +

+

+ 2. The rainbow dashboard +

+

+ It’s a little garish maybe, but we took a + + poll in the forum + + and almost everyone is either into it or expects to get used to it. +

+

+ \\"Screenshot +

+

+ We’ll probably keep fussing with it. +Chime in in the forum if you have opinions! +Next we need to decide the color of the shed where we keep our bikes (just kidding). +

+

+ (Shoutout to Beeminder superuser Michael Hanson for building his own custom Beeminder dashboard — + + which anyone can use by plugging in their API key + + — arguably prettier than ours.) +

+

+ 3. Goals can be “dark green” now +

+

+ The eagle-eyed among you may have noticed a subtle hue shift in the green there. +Here’s a review of the colors: +

+
    +
  • + Red means you’re within 24 hours of derailing — aka a beemergency +
  • +
  • + Orange means you have 1 day of safety buffer +
  • +
  • + Blue means 2 days of safety buffer +
  • +
  • + Green means 3 or more safe days +
  • +
+

+ And now the slightly darker green means 7+ days of safety buffer. +The nice thing about being dark green, as we explained in + + another old listicle + + , is that you’re immune to the akrasia horizon. +If something comes up or you decide you hate your Beeminder goal (it happens), you can click Archive or schedule a flat section of the bright red line. +It takes a week for such changes to take effect, but, lucky you, you have that much safety buffer! +So instead of clicking Archive or scheduling your break and then toughing it out for another week, you can do that and then immediately check out. +By the time your safety buffer runs out, your break will begin. +Your friends will be dark green with envy. +

+

+ (Special thanks to our friend and Beeminder superuser Grayson Bray Morris for suggesting something like this years ago.) +

+

+ 4. A new status page +

+

+ We’re pretty sure our last unscheduled downtime last month or whenever that was was the last time that will happen, but just in case, we made a new + + status.beeminder.com + + page. +Until now, that URL just redirected to a Twitter feed where we’d give updates on how engulfed in flames our servers were. +But now you have to be logged in to Twitter (and maybe refer to it as X even) to see things there so that obviously doesn’t work as a status page anymore. +So, ok, if you ever can’t reach Beeminder (or, in fact many/most websites) stick “status.” in front of the URL and see what’s up. +

+

+ 5. Weight loss goal headers are more sane +

+

+ Have you ever tried a Beeminder weight goal? +If so you might have noticed the insanity that was the goal page header, and generally any time we tried to tell you things about how much weight loss was due when. +I’m sure you’ve been suffering nightmares ever since. +

+

+ It is tempting to put a lot of words into trying to describe how wrong-headed it was, but sometimes a picture is worth a thousand cliches, eh? +

+

+ before +

+

+ \\"Before +

+

+ Well the night terrors can end now! +The goal page now tells you simply what your hard cap is today. +You can get that both in terms of the delta (e.g. +0.25 — you can’t gain more than 0.25 stone and still be below the bright red line today), +or in terms of the absolute amount (e.g. 14.25 stone — that’s the actual value of the bright red line today). +

+

+ (If you see one and not the other, try clicking on the black button and it will toggle to the other version. +This highly undiscoverable click-to-toggle business is another of our Long National Nightmares. We’re working on it!) +

+

+ after +

+

+ \\"After +

+

+ The header also tells you how long you can coast along not weighing in before you’re in trouble, but that’s not new. +

+

+
+   +
+

+

+ Alright, that rounds out the list for now. +(Again, see + + the changelog + + for more.) +Keep telling us what you think we should do next! +

+" +`; + exports[`body > post ratchet hash 1570aa49e7a0b862699efaef14f7be7b 1`] = ` "

diff --git a/src/lib/getPosts.spec.ts b/src/lib/getPosts.spec.ts index cb043b2..8fb7e66 100644 --- a/src/lib/getPosts.spec.ts +++ b/src/lib/getPosts.spec.ts @@ -35,14 +35,13 @@ describe("getPosts", () => { }); it("includes excerpts", async () => { - vi.mocked(readSources).mockReturnValue([meta({ excerpt: undefined })]); + vi.mocked(readSources).mockReturnValue([ + meta({ excerpt: "MAGIC_AUTO_EXTRACT" }), + ]); vi.mocked(fetchPost).mockResolvedValue( ether({ content: "word", - frontmatter: { - excerpt: "MAGIC_AUTO_EXTRACT", - }, }), ); @@ -101,104 +100,6 @@ describe("getPosts", () => { expect(result?.image?.src).toEqual("https://example.com/image.png"); }); - it("uses frontmatter title", async () => { - vi.mocked(readSources).mockReturnValue([meta({ title: undefined })]); - - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - title: "Hello", - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.title).toEqual("Hello"); - }); - - it("uses frontmatter author", async () => { - vi.mocked(readSources).mockReturnValue([meta({ author: undefined })]); - - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - author: "Alice", - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.author).toEqual("Alice"); - }); - - it("uses frontmatter excerpt", async () => { - vi.mocked(readSources).mockReturnValue([meta({ excerpt: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - excerpt: "Hello", - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.excerpt).toEqual("Hello"); - }); - - it("uses frontmatter tags", async () => { - vi.mocked(readSources).mockReturnValue([meta({ tags: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - tags: ["a", "b", "c"], - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.tags).toEqual(expect.arrayContaining(["a", "b", "c"])); - }); - - it("uses frontmatter date", async () => { - vi.mocked(readSources).mockReturnValue([meta({ date: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - date: new Date("2021-09-02"), - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.date_string).toEqual("2021-09-02"); - }); - - it("uses frontmatter slug", async () => { - vi.mocked(readSources).mockReturnValue([meta({ slug: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - slug: "hello", - }, - }), - ); - - const posts = await getPosts(); - const result = posts.find((p) => p.slug === "hello"); - - expect(result?.slug).toEqual("hello"); - }); - it("uses legacy status", async () => { const posts = await getPosts(); const result = posts[0]; @@ -206,22 +107,6 @@ describe("getPosts", () => { expect(result?.status).toEqual("publish"); }); - it("uses frontmatter status", async () => { - vi.mocked(readSources).mockReturnValue([meta({ status: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - status: "publish", - }, - }), - ); - - const posts = await getPosts(); - const result = posts[0]; - - expect(result?.status).toEqual("publish"); - }); - it("returns html", async () => { vi.mocked(fetchPost).mockResolvedValue( ether({ @@ -389,22 +274,6 @@ https://blog.beeminder.com/depunish expect(content).toContain("2"); }); - it("parses frontmatter", async () => { - vi.mocked(readSources).mockReturnValue([meta({ slug: undefined })]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: { - slug: "val", - }, - }), - ); - - const posts = await getPosts(); - const { slug } = posts.find((p) => p.slug === "val") || {}; - - expect(slug).toEqual("val"); - }); - it("uses wordpress excerpt", async () => { vi.mocked(readSources).mockReturnValue([meta({ excerpt: "wp excerpt" })]); @@ -429,45 +298,26 @@ https://blog.beeminder.com/depunish it("throws on duplicate slugs", async () => { vi.mocked(readSources).mockReturnValue([ - { source: "doc.bmndr.co/a" }, - { source: "doc.bmndr.co/b" }, + meta({ source: "doc.bmndr.co/a", slug: "the_slug", date: "2020-01-01" }), + meta({ source: "doc.bmndr.co/b", slug: "the_slug", date: "2020-01-01" }), ]); - vi.mocked(fetchPost).mockResolvedValue( - ether({ - frontmatter: meta({ - slug: "the_slug", - date: new Date(), - }), - }), - ); - await expect(getPosts()).rejects.toThrow(/Duplicate slug/); }); it("throws on duplicate disqus IDs", async () => { vi.mocked(readSources).mockReturnValue([ - { source: "doc.bmndr.co/a" }, - { source: "doc.bmndr.co/b" }, - ]); - - vi.mocked(fetchPost).mockResolvedValueOnce( - ether({ - frontmatter: meta({ - disqus_id: "the_disqus_id", - date: new Date(), - }), + meta({ + source: "doc.bmndr.co/a", + date: "2020-01-01", + disqus_id: "the_disqus_id", }), - ); - - vi.mocked(fetchPost).mockResolvedValueOnce( - ether({ - frontmatter: meta({ - disqus_id: "the_disqus_id", - date: new Date(), - }), + meta({ + source: "doc.bmndr.co/b", + date: "2020-01-01", + disqus_id: "the_disqus_id", }), - ); + ]); await expect(getPosts()).rejects.toThrow(/Duplicate disqus/); }); diff --git a/src/lib/parseTitle.spec.ts b/src/lib/parseTitle.spec.ts new file mode 100644 index 0000000..8d99cab --- /dev/null +++ b/src/lib/parseTitle.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; + +import parseTitle from "./parseTitle"; + +describe("parseTitle", () => { + it("parses title", () => { + const content = "BEGIN_MAGIC[title]"; + expect(parseTitle(content)).toBe("title"); + }); + + it("returns undefined if no title", () => { + const content = `BEGIN_MAGIC +helle world`; + expect(parseTitle(content)).toBeUndefined(); + }); + + it("returns blogmorphosis title", () => { + const content = `BEGIN_MAGIC[Ditching WordPress and a Shiny Blog Redesign]`; + expect(parseTitle(content)).toBe( + "Ditching WordPress and a Shiny Blog Redesign", + ); + }); +}); diff --git a/src/lib/test/ether.ts b/src/lib/test/ether.ts index be7796a..7a5b26f 100644 --- a/src/lib/test/ether.ts +++ b/src/lib/test/ether.ts @@ -1,7 +1,4 @@ -import matter from "gray-matter"; - export default function ether({ - frontmatter = {}, before = "", content = "", after = "", @@ -14,14 +11,9 @@ export default function ether({ title?: string; redirects?: string[]; } = {}): string { - return matter.stringify( - ` -${before} + return `${before} BEGIN_MAGIC${title ? `[${title}]` : ""} ${content} END_MAGIC -${after} -`, - frontmatter, - ); +${after}`; } diff --git a/src/schemas/frontmatter.ts b/src/schemas/frontmatter.ts deleted file mode 100644 index bc033e6..0000000 --- a/src/schemas/frontmatter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -import { status } from "./status"; - -export const frontmatter = z - .object({ - title: z.string(), - excerpt: z.string(), - date: z.date(), - slug: z.string(), - author: z.string(), - tags: z.array(z.string()), - disqus_id: z.string(), - status, - redirects: z.array(z.string()), - }) - .partial(); - -export type Frontmatter = z.infer; diff --git a/src/schemas/post.spec.ts b/src/schemas/post.spec.ts index d10de1a..5021f60 100644 --- a/src/schemas/post.spec.ts +++ b/src/schemas/post.spec.ts @@ -13,25 +13,39 @@ describe("post", () => { content: "body", }); - expect(() => - post.parse({ - url: "the_url", - md, - }), - ).toThrowError(); + const data = { + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "", + md, + }; + + expect(() => post.parse(data)).toThrowError(); }); it("uses disqus id", () => { const md = ether({ - frontmatter: meta({ - disqus_id: "test-post", - date: new Date(), - }), content: "body", }); const p = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "test-post", md, }); @@ -40,15 +54,21 @@ describe("post", () => { it("does not include private notes in excerpts", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - }), before: "private notes", content: "content", }); const p = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -57,15 +77,20 @@ describe("post", () => { it("does not include raw markdown in excerpts", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - excerpt: "MAGIC_AUTO_EXTRACT", - }), content: "[link](#)", }); const p = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -74,15 +99,21 @@ describe("post", () => { it("does not use image from private notes", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - }), before: "", content: "content", }); const p = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -91,51 +122,21 @@ describe("post", () => { it("requires title", async () => { const md = ether({ - frontmatter: meta({ - title: undefined, - date: new Date(), - }), - }); - - const result = post.safeParse({ - source: "the_url", - md, - }); - - expect(result.success).toEqual(false); - }); - - it("requires slug be declared one time", async () => { - const md = ether({ - frontmatter: meta({ - title: "the_title", - slug: "the_slug", - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", - md, + date: "2020-01-01", + excerpt: "the_excerpt", slug: "the_slug", - }); - - expect(result.success).toEqual(false); - }); - - it("requires status be declared one time", async () => { - const md = ether({ - frontmatter: meta({ - title: "the_title", - status: "publish", - date: new Date(), - }), - }); - - const result = post.safeParse({ - source: "the_url", - md, + title: "", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", status: "publish", + disqus_id: "the_disqus_id", + md, }); expect(result.success).toEqual(false); @@ -143,14 +144,20 @@ describe("post", () => { it("requires slug", async () => { const md = ether({ - frontmatter: meta({ - slug: "", - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -159,14 +166,20 @@ describe("post", () => { it("requires author", async () => { const md = ether({ - frontmatter: meta({ - author: "", - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -175,14 +188,20 @@ describe("post", () => { it("requires disqus_id", async () => { const md = ether({ - frontmatter: meta({ - disqus_id: "", - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "", md, }); @@ -191,14 +210,20 @@ describe("post", () => { it("requires redirects", async () => { const md = ether({ - frontmatter: meta({ - redirects: undefined, - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: undefined, + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -207,13 +232,20 @@ describe("post", () => { it("requires date", async () => { const md = ether({ - frontmatter: meta({ - date: undefined, - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -222,14 +254,20 @@ describe("post", () => { it("requires tags", async () => { const md = ether({ - frontmatter: meta({ - tags: undefined, - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: undefined, + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -238,14 +276,20 @@ describe("post", () => { it("requires status", async () => { const md = ether({ - frontmatter: meta({ - status: undefined, - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "", + disqus_id: "the_disqus_id", md, }); @@ -254,12 +298,20 @@ describe("post", () => { it("requires source", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ + source: undefined, + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -268,15 +320,21 @@ describe("post", () => { it("requires new line preceeding HTML comments", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - }), content: `This is the paragraph in question More text`, }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -285,15 +343,21 @@ describe("post", () => { it("specifies error reason", async () => { const md = ether({ - frontmatter: meta({ - date: new Date(), - }), content: `This is the paragraph in question More text`, }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -307,14 +371,20 @@ describe("post", () => { it("requires excerpt", async () => { const md = ether({ - frontmatter: meta({ - excerpt: undefined, - date: new Date(), - }), + content: "content", }); const result = post.safeParse({ source: "the_url", + date: "2020-01-01", + excerpt: undefined, + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -324,14 +394,19 @@ describe("post", () => { it("expects excerpt from MAGIC_AUTO_EXTRACT to be Generated", async () => { const md = ether({ content: "words", - frontmatter: meta({ - excerpt: "MAGIC_AUTO_EXTRACT", - date: new Date(), - }), }); const result = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "MAGIC_AUTO_EXTRACT", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -341,14 +416,19 @@ describe("post", () => { it("expects custom excerpt to return unchanged", async () => { const md = ether({ content: "words", - frontmatter: meta({ - excerpt: "the excerpt", - date: new Date(), - }), }); const result = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -358,13 +438,19 @@ describe("post", () => { it("extracts image title", async () => { const md = ether({ content: ``, - frontmatter: meta({ - date: new Date(), - }), }); const result = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -374,13 +460,19 @@ describe("post", () => { it("extracts image alt", async () => { const md = ether({ content: `the_alt`, - frontmatter: meta({ - date: new Date(), - }), }); const result = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -390,13 +482,19 @@ describe("post", () => { it("extracts image alt and title", async () => { const md = ether({ content: `the_alt`, - frontmatter: meta({ - date: new Date(), - }), }); const result = post.parse({ source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", md, }); @@ -404,4 +502,207 @@ describe("post", () => { expect.objectContaining({ title: "the_title", alt: "the_alt" }), ); }); + + it("does not accept date from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + date: "2020-01-01", + }), + }); + + const result = post.safeParse({ + source: "the_url", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + expect(result.success).toEqual(false); + }); + + it("does not accept date_string from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + date_string: "2020-01-01", + }), + }); + + const result = post.safeParse({ + source: "the_url", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + expect(result.success).toEqual(false); + }); + + it("does not accept excerpt from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + excerpt: "the_excerpt", + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + expect(result.success).toEqual(false); + }); + + it("does not accept slug from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + slug: "the_slug", + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + expect(result.success).toEqual(false); + }); + + it("does not accept author from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + author: "the_author", + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + + expect(result.success).toEqual(false); + }); + + it("does not accept tags from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + tags: ["the_tag"], + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + + expect(result.success).toEqual(false); + }); + + it("does not accept disqus_id from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + disqus_id: "the_disqus_id", + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + status: "publish", + md, + }); + + expect(result.success).toEqual(false); + }); + + it("does not accept status from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + status: "publish", + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + redirects: ["the_redirect"], + author: "the_author", + disqus_id: "the_disqus_id", + md, + }); + + expect(result.success).toEqual(false); + }); + + it("does not accept redirects from frontmatter", async () => { + const md = ether({ + frontmatter: meta({ + redirects: ["the_redirect"], + }), + }); + + const result = post.safeParse({ + source: "the_url", + date: "2020-01-01", + excerpt: "the_excerpt", + slug: "the_slug", + title: "the_title", + tags: ["the_tag"], + author: "the_author", + status: "publish", + disqus_id: "the_disqus_id", + md, + }); + + expect(result.success).toEqual(false); + }); }); diff --git a/src/schemas/post.ts b/src/schemas/post.ts index f4aec15..5ff34f2 100644 --- a/src/schemas/post.ts +++ b/src/schemas/post.ts @@ -4,48 +4,25 @@ import parseTitle from "../lib/parseTitle"; import getExcerpt from "../lib/getExcerpt"; import { image } from "./image"; import extractImage from "../lib/extractImage"; -import matter from "gray-matter"; -import { frontmatter } from "./frontmatter"; import { body } from "./body"; import { dateString } from "./dateString"; -const intersect = ( - o1: Record, - o2: Record, -) => { - return Object.keys(o1).filter((k) => k in o2); -}; - export const post = z .object({ source: z.string(), title: z.string().optional(), - slug: z.string().optional(), - date: z.string().optional(), - author: z.string().optional(), - tags: z.array(z.string()).optional(), - status: z.string().optional(), - disqus_id: z.string().optional(), - excerpt: z.string().optional(), - redirects: z.array(z.string()).optional(), + slug: z.string(), + date: z.string(), + author: z.string(), + tags: z.array(z.string()), + status: z.string(), + disqus_id: z.string(), + excerpt: z.string(), + redirects: z.array(z.string()), md: z.string(), }) .transform(({ source: url, md, ...rest }, ctx) => { - const { data, content } = matter(md); - const fm = frontmatter.parse(data); - const intersecting = intersect(fm, rest); - - if (intersecting.length > 0) { - intersecting.forEach((key) => { - ctx.addIssue({ - message: `Cannot declare ${key} in two places ${url}`, - code: ZodIssueCode.custom, - }); - }); - return z.NEVER; - } - - const meta = { ...rest, ...fm }; + const content = md; const c = body.safeParse(content); if (!c.success) { @@ -60,15 +37,15 @@ export const post = z return z.NEVER; } - const date = meta.date && new Date(meta.date); + const date = rest.date && new Date(rest.date); const dateStringResult = dateString.safeParse(date); return { - ...meta, - tags: meta.tags?.filter(Boolean), - excerpt: getExcerpt(meta.excerpt, c.data), + ...rest, + tags: rest.tags?.filter(Boolean), + excerpt: getExcerpt(rest.excerpt, c.data), image: extractImage(c.data), - title: meta.title || parseTitle(md), + title: rest.title || parseTitle(md), date, date_string: dateStringResult.success && dateStringResult.data, content: c.data,