Compare commits

..

7 Commits

Author SHA1 Message Date
joeac 82d08e8aa8 tidying in gemlog posts 2026-03-24 22:07:31 +00:00
joeac 706f5913e6 moves blog posts to new position to be consistent with gemlog 2026-03-24 22:07:05 +00:00
joeac 4b720b8dd7 port gemlog to blog 2026-03-24 22:06:32 +00:00
joeac 46bc365021 description is optional 2026-03-24 22:04:06 +00:00
joeac 550d285a8b adds extended-glob loader 2026-03-24 22:02:07 +00:00
joeac 268eff17f1 upgrades astro-gemtext 2026-03-24 22:01:49 +00:00
joeac 579c31b7c7 redirects old blog posts to new 2026-03-24 22:01:38 +00:00
43 changed files with 435 additions and 84 deletions
+2 -2
View File
@@ -2,7 +2,8 @@
Mark Vernon got me thinking about how the Churchs teaching on sex may be evolving. Mark Vernon got me thinking about how the Churchs teaching on sex may be evolving.
Published on: 17 Dec 2024 updatedDate: 2024-12-17 Published on: 17 Dec 2024
Updated on: 17 Dec 2024
I just listened back to Mark Vernon talking about sexual desire and Christian spirituality. I just listened back to Mark Vernon talking about sexual desire and Christian spirituality.
@@ -23,4 +24,3 @@ But so far (Im about a third of the way through and up to about the 5th centu
So I want to know: what changed between Marks experience and mine? How is Christian teaching about sex changing right now? Was my experience typical of other Christians growing up today, in evangelical churches, across the denominational spectrum, across Britain, across the global Church? What *is* the Christian consensus on sex now, and how old is it, and where did it come from? So I want to know: what changed between Marks experience and mine? How is Christian teaching about sex changing right now? Was my experience typical of other Christians growing up today, in evangelical churches, across the denominational spectrum, across Britain, across the global Church? What *is* the Christian consensus on sex now, and how old is it, and where did it come from?
I guess Ive got two-thirds of a gigantic tome to work through, first of all. Thatll be a start. Any answers? Postcards please. I guess Ive got two-thirds of a gigantic tome to work through, first of all. Thatll be a start. Any answers? Postcards please.
+2 -2
View File
@@ -2,7 +2,8 @@
I'm summarising a few of the big stories about the Reformation I've been studying recently. I'm summarising a few of the big stories about the Reformation I've been studying recently.
Published on: 16 Feb 2026 updatedDate: 2026-02-27 Published on: 16 Feb 2026
Updated on: 27 Feb 2026
1. Moral corruption in public office 1. Moral corruption in public office
@@ -45,4 +46,3 @@ The medieval Latin church had a thing for councils. Councils functioned as a way
10. Persecution 10. Persecution
In 1520, Martin Luther was declared an heretic in a papal bull issued by Pope Leo X. In response, Martin Luther burned the bull in public and announced that Leo X was the Antichrist. Various players in various quarters tried various strategies for resolving the schism, and it seems that few were willing to give up on violent coercion. In both Catholic and Protestant domains, magistrates burned books and burned people in an attempt to quell heresy. In 1520, Martin Luther was declared an heretic in a papal bull issued by Pope Leo X. In response, Martin Luther burned the bull in public and announced that Leo X was the Antichrist. Various players in various quarters tried various strategies for resolving the schism, and it seems that few were willing to give up on violent coercion. In both Catholic and Protestant domains, magistrates burned books and burned people in an attempt to quell heresy.
+2
View File
@@ -1,5 +1,7 @@
# The structure of Genesis # The structure of Genesis
Published on: 21 Mar 2026
I've been reading the Book of Genesis recently. Last night, I tried a favourite exercise of mine. It's very simple. I wrote down a list, in order, of all the 'episodes' in Genesis. That is, I wrote down a list that went: I've been reading the Book of Genesis recently. Last night, I tried a favourite exercise of mine. It's very simple. I wrote down a list, in order, of all the 'episodes' in Genesis. That is, I wrote down a list that went:
* Creation * Creation
+3 -1
View File
@@ -3,9 +3,10 @@ import db from "@astrojs/db";
import gemtext from "astro-gemtext"; import gemtext from "astro-gemtext";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import node from "@astrojs/node"; import node from "@astrojs/node";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import redirects from "./redirects.mjs";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
adapter: node({ adapter: node({
@@ -34,6 +35,7 @@ export default defineConfig({
image: { image: {
service: passthroughImageService(), service: passthroughImageService(),
}, },
redirects,
site: "https://joeac.net", site: "https://joeac.net",
integrations: [db(), gemtext(), mdx(), sitemap()], integrations: [db(), gemtext(), mdx(), sitemap()],
}); });
+2 -2
View File
@@ -12,7 +12,7 @@
"@astrojs/sitemap": "^3.7.1", "@astrojs/sitemap": "^3.7.1",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"astro": "^6.0.8", "astro": "^6.0.8",
"astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git", "astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git#ab32140ae28ff451447e7b25e24de0a4d46d264d",
"gemtext": "^0.2.2", "gemtext": "^0.2.2",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"nodemailer": "^7.0.11", "nodemailer": "^7.0.11",
@@ -3676,7 +3676,7 @@
}, },
"node_modules/astro-gemtext": { "node_modules/astro-gemtext": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "git+https://git.joeac.net/joeac/astro-gemtext.git#1248542cc6b509a1b070ed5812ca5c327823a018", "resolved": "git+https://git.joeac.net/joeac/astro-gemtext.git#ab32140ae28ff451447e7b25e24de0a4d46d264d",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"gemtext": "^0.2.2", "gemtext": "^0.2.2",
+1 -1
View File
@@ -18,7 +18,7 @@
"@astrojs/sitemap": "^3.7.1", "@astrojs/sitemap": "^3.7.1",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"astro": "^6.0.8", "astro": "^6.0.8",
"astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git", "astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git#ab32140ae28ff451447e7b25e24de0a4d46d264d",
"gemtext": "^0.2.2", "gemtext": "^0.2.2",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"nodemailer": "^7.0.11", "nodemailer": "^7.0.11",
+34
View File
@@ -0,0 +1,34 @@
export default {
"/blog/2024/10/tracking_pixels": "/blog/2024-04-10",
"/blog/2024/01/14/sapiens_on_religion": "/blog/2024-01-14",
"/blog/2024/01/29/euhwc_toast_to_the_lasses_2024": "/blog/2024-01-29",
"/blog/2024/03/30/easter": "/blog/2024-03-30",
"/blog/2024/04/10/tracking_pixels": "/blog/2024-04-10",
"/blog/2024/04/11/who_consecrates_the_temple": "/blog/2024-04-11",
"/blog/2024/04/14/god_is_not_great": "/blog/2024-04-14",
"/blog/2024/05/02/no_more_youtube": "/blog/2024-05-02",
"/blog/2024/06/13/llms_do_not_understand_anything": "/blog/2024-06-13",
"/blog/2024/07/08/doctor_who_gayness_church": "/blog/2024-07-08",
"/blog/2024/07/16/word_hallucination_with_reference_to_llms":
"/blog/2024-07-16",
"/blog/2024/12/17/open_questions_about_sex": "/blog/2024-12-17",
"/blog/2025/01/19/my_feed_and_reading_list": "/blog/2025-01-19",
"/blog/2025/01/19/newsboat-post.webp": "/blog/2025-01-19_newsboat-post.webp",
"/blog/2025/01/19/newsboat.webp": "/blog/2025-01-19_newsboat.webp",
"/blog/2025/01/19/ranger.webp": "/blog/2025-01-19_ranger.webp",
"/blog/2025/01/19/w3m.webp": "/blog/2025-01-19_w3m.webp",
"/blog/2025/01/24/science_and_philosophy": "/blog/2025-01-24",
"/blog/2025/01/28/paradox": "/blog/2025-01-28",
"/blog/2025/05/02/surprised_by_hope": "/blog/2025-05-02",
"/blog/2025/05/04/does_resurrection_ground_works": "/blog/2025-05-04",
"/blog/2025/06/23/work": "/blog/2025-06-23",
"/blog/2025/07/03/ps118": "/blog/2025-07-03",
"/blog/2025/09/18/starting_msc": "/blog/2025-09-18",
"/blog/2025/09/24/creeds": "/blog/2025-09-24",
"/blog/2025/10/05/creeds": "/blog/2025-10-05",
"/blog/2025/10/09/arius": "/blog/2025-10-09",
"/blog/2025/12/11/persecution": "/blog/2025-12-11",
"/blog/2026/02/16/short_reformation_stories": "/blog/2026-02-16",
"/blog/2026/03/04/agnostic": "/blog/2026-03-04",
"/blog/2026/03/21/genesis": "/blog/2026-03-21",
};
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
interface Props { interface Props {
title: string; title: string;
description: string; description?: string;
image?: string; image?: string;
} }
+36 -3
View File
@@ -1,16 +1,49 @@
import { glob } from "astro/loaders";
import { defineCollection } from "astro:content"; import { defineCollection } from "astro:content";
import { z } from "astro/zod"; import { z } from "astro/zod";
import { extendedGlob } from "./loaders/extended-glob";
const blog = defineCollection({ const blog = defineCollection({
loader: glob({ loader: extendedGlob({
pattern: "**/*.(md|mdx|gmi|html)", pattern: "**/*.(md|mdx|gmi|html)",
base: "./src/content/blog", base: "./src/content/blog",
postprocessSlug: (slug: string) => slug.replaceAll(/^gemlog\//g, ""),
ignore: [
"gemlog/index.gmi",
/* Some blog posts were ported to gemtext. Don't port them back again:
* that would lead to duplicates! The originals are likely to be better
* anyway, as MD/MDX are richer languages. */
"**/2024-01-14.gmi",
"**/2024-01-29.gmi",
"**/2024-03-30.gmi",
"**/2024-04-10.gmi",
"**/2024-04-11.gmi",
"**/2024-04-14.gmi",
"**/2024-05-02.gmi",
"**/2024-06-13.gmi",
"**/2024-07-08.gmi",
"**/2024-07-16.gmi",
"**/2024-12-17.gmi",
"**/2025-01-19.gmi",
"**/2025-01-24.gmi",
"**/2025-01-28.gmi",
"**/2025-05-02.gmi",
"**/2025-05-04.gmi",
"**/2025-06-23.gmi",
"**/2025-07-03.gmi",
"**/2025-09-18.gmi",
"**/2025-09-24.gmi",
"**/2025-10-05.gmi",
"**/2025-10-09.gmi",
"**/2025-12-11.gmi",
"**/2026-02-16.gmi",
"**/2026-03-04.gmi",
],
}), }),
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
hidden: z.optional(z.boolean()), hidden: z.optional(z.boolean()),
description: z.string(), description: z.optional(z.string()),
pubDate: z.date(), pubDate: z.date(),
updatedDate: z.optional(z.date()), updatedDate: z.optional(z.date()),
}), }),
@@ -1,9 +1,11 @@
--- ---
title: How I read things on the Internet now (no, of course I don't leave the terminal!) title:
How I read things on the Internet now (no, of course I don't leave the
terminal!)
description: >- description: >-
I've been sprucing up how I follow what's happening on the Internet. I can I've been sprucing up how I follow what's happening on the Internet. I can now
now read all the things I want to read pretty efficiently, and enjoy doing read all the things I want to read pretty efficiently, and enjoy doing it,
it, which is exciting. which is exciting.
pubDate: 2025-01-19 pubDate: 2025-01-19
--- ---
@@ -16,15 +18,14 @@ Some interesting people write blog posts. Some beautiful people make music. Some
silly people make comedy sketches. Sometimes my friends are sharing stuff with silly people make comedy sketches. Sometimes my friends are sharing stuff with
their friends, which means me (WIP on that front: I'll get back to that). their friends, which means me (WIP on that front: I'll get back to that).
I want to know about it. I want to read interesting things that will educate I want to know about it. I want to read interesting things that will educate me.
me. I want to hear beautiful music. I want to hear about what my friends are up I want to hear beautiful music. I want to hear about what my friends are up to.
to.
I can't read everything as it comes in, it's too much: I'll need to sift a lot I can't read everything as it comes in, it's too much: I'll need to sift a lot
of it out quickly. I'll need to sift through it regularly to stay on top of it: of it out quickly. I'll need to sift through it regularly to stay on top of it:
like, several times a week, if not daily. And sometimes, while I'm doing my like, several times a week, if not daily. And sometimes, while I'm doing my
daily sifting, I'll find something I really want to read, but I haven't got daily sifting, I'll find something I really want to read, but I haven't got time
time right now: I'll save this for the weekend. right now: I'll save this for the weekend.
For a while, I've been hosting [CommaFeed][commafeed] on [PikaPods][pikapods]. For a while, I've been hosting [CommaFeed][commafeed] on [PikaPods][pikapods].
This has been OK ([OMG, RSS is cool][rss]), but the interface is just clunky This has been OK ([OMG, RSS is cool][rss]), but the interface is just clunky
@@ -32,15 +33,15 @@ enough to make it a chore to use. That means I don't sift through stuff
regularly, and that means my feed piles hundreds of unread posts high. regularly, and that means my feed piles hundreds of unread posts high.
It also hasn't been any good for distinguishing between stuff I don't want to It also hasn't been any good for distinguishing between stuff I don't want to
read *ever*, and stuff I don't want to read *right now* but will get round to read _ever_, and stuff I don't want to read _right now_ but will get round to
later. I could in theory use the bookmarking feature built-in to my browser, later. I could in theory use the bookmarking feature built-in to my browser, but
but removing things after I've read them is too clunky, so I don't do it. removing things after I've read them is too clunky, so I don't do it.
## The solution ## The solution
Every morning, I open my terminal and run [newsboat][newsboat]. Every morning, I open my terminal and run [newsboat][newsboat].
![newsboat showing how many unread posts I have at a glance in the opening view](./newsboat.webp) ![newsboat showing how many unread posts I have at a glance in the opening view](./2025-01-19_newsboat.webp)
I know the unread count is pretty fresh, because I've set up a systemd service I know the unread count is pretty fresh, because I've set up a systemd service
to run newsboat at startup to fetch the feeds. to run newsboat at startup to fetch the feeds.
@@ -48,7 +49,7 @@ to run newsboat at startup to fetch the feeds.
I press `l` twice to open a post. Then I press `n` to navigate to the next I press `l` twice to open a post. Then I press `n` to navigate to the next
unread post until I run out of unread posts. unread post until I run out of unread posts.
![newsboat displaying a post](./newsboat-post.webp) ![newsboat displaying a post](./2025-01-19_newsboat-post.webp)
If I encounter something I want to read later, but don't have time right now, I If I encounter something I want to read later, but don't have time right now, I
press `b`, which runs a home-made bookmarking script. Here it is: press `b`, which runs a home-made bookmarking script. Here it is:
@@ -85,11 +86,11 @@ curl "$url" > "$HOME/readlist/unread/$filename"
I can also run this script manually and pass it a URL of my choice at any time, I can also run this script manually and pass it a URL of my choice at any time,
say, if I find an interesting article while browsing the Web. say, if I find an interesting article while browsing the Web.
When I want to read from my reading list, I run `readnow.sh`, which simply When I want to read from my reading list, I run `readnow.sh`, which simply opens
opens my reading list folder, `~/readlist/unread`, in my terminal file browser my reading list folder, `~/readlist/unread`, in my terminal file browser of
of choice: namely, [ranger][ranger]. choice: namely, [ranger][ranger].
![ranger showing the contents of my reading list with a preview](./ranger.webp) ![ranger showing the contents of my reading list with a preview](./2025-01-19_ranger.webp)
Although ranger has a preview, I'll typically open the file up in my terminal Although ranger has a preview, I'll typically open the file up in my terminal
web browser of choice, which is [w3m][w3m] (plus a couple of custom web browser of choice, which is [w3m][w3m] (plus a couple of custom
@@ -108,18 +109,18 @@ ext x?html?, ...
... ...
``` ```
Having configured my default web browser in my ranger config, all Having configured my default web browser in my ranger config, all I need to do
I need to do is press `l`. is press `l`.
![A post displaying in w3m](./w3m.webp) ![A post displaying in w3m](./2025-01-19_w3m.webp)
No ads, no cookie popups, no giant banner images taking 2 seconds to load and No ads, no cookie popups, no giant banner images taking 2 seconds to load and
shifting the content all over the place: just the text I want to read. Isn't it shifting the content all over the place: just the text I want to read. Isn't it
beautiful? beautiful?
Once I'm finished reading the post, I'll press `q` to quit w3m and return to Once I'm finished reading the post, I'll press `q` to quit w3m and return to
ranger. Assuming I don't need to read it again, I'll press `dm` to move the ranger. Assuming I don't need to read it again, I'll press `dm` to move the post
post to `~/readlist/read` - my way of marking a post as 'read'. to `~/readlist/read` - my way of marking a post as 'read'.
I've done this by writing a super simple script, `markread.sh`: I've done this by writing a super simple script, `markread.sh`:
@@ -155,8 +156,7 @@ map dm shell markread.sh %f
I can now keep up to date, and I enjoy doing it. I can now keep up to date, and I enjoy doing it.
I get not everyone likes to live in the terminal. I think the key takeaways I get not everyone likes to live in the terminal. I think the key takeaways are:
are:
- Make it really easy to sift through new posts - Make it really easy to sift through new posts
- When you sift, sift through every post, and for each one, either read it - When you sift, sift through every post, and for each one, either read it

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

@@ -1,45 +0,0 @@
---
title: The structure of Genesis
pubDate: 2026-03-21
description: I've been reading the Book of Genesis recently.
---
Last night, I tried a favourite exercise of mine. It's very simple. I wrote down a list, in order, of all the 'episodes' in Genesis. That is, I wrote down a list that went:
* Creation
* Adam and Eve
* Cain and Abel
* Genealogy of Noah
* Noah and the Flood
* …
…and so on, all the way to Joseph.
Once I'd finished this, I was surprised at the result. I realised that, without consciously thinking it, I had been expecting to find a single, clear structure, and in particular, one that went: patriarch, genealogy, patriarch, genealogy, and so on. I didn't find that at all.
For sure, patriarchs and genealogies are major structures in Genesis, and you'll get a long way thinking about Genesis as a story of six patriarchs and so many genealogies (though the genealogies are rather more difficult to count). Indeed, you *can*, if you really want, slice up Genesis into six neat patriarch cycles and glue them back together again with genealogies: Adam (1-5), Noah (5-11), Abram (11-25), Isaac (25-27), Jacob (27-36), Joseph (36-50). But this way of thinking cannot comprehensively account for the whole book.
For one thing, there are a good few pieces in Genesis which are difficult to fit into a patriarch jigsaw. Why is there so much material about Cain, Hagar, Ishmael, Esau, and Dinah, including genealogies? Why is the Tower of Babel in there? If Genesis is all about the patriarchs, these are all lengthy side-alleys. (May I say pends? Closes?)
For two things, this neat six-part series is more messy than it might at first appear. On this model, the genealogies function to glue together the patriarch narratives. Sometimes, this model works well: the genealogy in chapter 5 does a good job of connecting Adam to Noah, and the one in chapter 10 gets us from Noah to Abram. But from Abram to Jacob to Joseph, there's no need for genealogies to stitch things together, because we're only taking one step on the genealogical ladder at a time, from father to son to grandson.
So, in chapter 25, instead of a 'genealogy' from Abram to his son Isaac, we get genealogies of Keturah and Esau: genealogical 'dead ends' from a patriarchal point of view. In chapter 36, where we're 'supposed' to be linking Jacob to his son Joseph, we get another genealogy of Esau. And in chapter 27, at the join between the supposed 'Isaac cycle' and the supposed 'Jacob cycle,' there's no genealogy at all.
I'm not for a moment trying to tell you that this model is *useless* for understanding Genesis. Far from it! I don't think you can understand Genesis as a coherent whole without considering it as a story of six patriarchs. I'm just saying this model is *inadequate*. It seems that to account for the Book of Genesis, we're going to have to structure it in multiple ways. To me, that's wonderfully exciting.
In particular, I'm considering all these to be worthwhile ways to look at Genesis as a whole:
* Genealogies! (Without assuming they are centred on the six patriarchs!)
* 'These are the generations of…' (how many occurrences can you find?)
* Yahweh speaking to people! (Always one person alone: did you notice that?)
* Wives! (How many can you think of? Will you include Potiphar's wife in this analysis? What about Dinah?)
* Anti-patriarchs and foreigners! (Does Adam get as much column space as Abimelech, or Esau?)
* Wells! (Some of them appear twice, or even thrice…)
* Names and naming! (Do you remember why Jacob is called 'Jacob'?)
* Deaths and burials! (How did Abraham end up buried in that cave?)
None of these structural lenses is adequate for seeing the whole thing in focus. But each one adds a new dimension. They all pile on top of one another. It's difficult to think of a physical analogy, because in my experience, things don't tend to occupy the same physical space at the same time, and I don't want to resort to the 'kaleidoscope' cliché. I guess it's a bit like quantum super-position, but instead of something being both 'up' and 'down' at the same time, it's multiple Persian rugs. Each one has its own colour scheme, its own patterns, its own delicately balanced symmetries. And yet, just as each rug is intricately harmonious within itself, it is no less intricately arranged into complex symphony with every other rug. And yet, somehow, all the rugs are simultaneously woven out of the same yarn. Picture that!
Of course, you can also read Genesis without attempting to comprehend the entire thing at once. You can also study a single episode, like the Tower of Babel, without trying to squeeze it into a grand narrative of the whole of Genesis. I suspect lots, if not all, of the stories in Genesis were originally told orally and independently of each other, and I think it's perfectly valid to read them independently once again.
Nevertheless, the author or authors of Genesis, and, more importantly, the Holy Ghost who moved them, once decided that it would be a good idea to put these stories and genealogies in an orderly sequence in the same scroll, and deliberately and explicitly relate them to one another. By considering structural lenses, we're joining the human authors, and generations of readers, in the faith that these stories mean something to each other, that they have something to tell us not only independently but also together.
+1
View File
@@ -0,0 +1 @@
../../../../capsule/content/gemlog/
+18
View File
@@ -0,0 +1,18 @@
/**
* This file is edited from astro/loaders/glob.d.ts
*/
import type { glob, Loader } from "astro/loaders";
type GlobOptions = Parameters<typeof glob>[0];
/**
* Loads multiple entries, using a glob pattern to match files.
* @param pattern A glob pattern to match files, relative to the content directory.
* @param ignore Glob patterns to exclude from the matched results.
*/
export declare function extendedGlob(
globOptions: GlobOptions & {
postprocessSlug?: (slug: string) => string;
ignore?: string[];
},
): Loader;
export {};
+306
View File
@@ -0,0 +1,306 @@
/**
* This file is edited from astro/loaders/glob.js
*/
import { existsSync, promises as fs } from "node:fs";
import { relative } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import pLimit from "p-limit";
import colors from "piccolore";
import picomatch from "picomatch";
import { glob as tinyglobby } from "tinyglobby";
import {
getContentEntryIdAndSlug,
posixRelative,
} from "../../node_modules/astro/dist/content/utils.js";
function generateIdDefault({ entry, base, data }, postprocessSlug, isLegacy) {
if (data.slug) {
return data.slug;
}
const entryURL = new URL(encodeURI(entry), base);
if (isLegacy) {
const { id } = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
collection: "",
});
return id;
}
const { slug } = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
collection: "",
});
if (postprocessSlug) {
return postprocessSlug(slug);
}
return slug;
}
function checkPrefix(pattern, prefix) {
if (Array.isArray(pattern)) {
return pattern.some((p) => p.startsWith(prefix));
}
return pattern.startsWith(prefix);
}
const secretLegacyFlag = /* @__PURE__ */ Symbol("astro.legacy-glob");
function extendedGlob(globOptions) {
if (checkPrefix(globOptions.pattern, "../")) {
throw new Error(
"Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead.",
);
}
if (checkPrefix(globOptions.pattern, "/")) {
throw new Error(
"Glob patterns cannot start with `/`. Set the `base` option to a parent directory or use a relative path instead.",
);
}
const isLegacy = !!globOptions[secretLegacyFlag];
const generateId =
globOptions?.generateId ??
((opts) => generateIdDefault(opts, globOptions?.postprocessSlug, isLegacy));
const fileToIdMap = /* @__PURE__ */ new Map();
const ignore = globOptions.ignore || [];
return {
name: "extended-glob-loader",
load: async ({
config,
collection,
logger,
watcher,
parseData,
store,
generateDigest,
entryTypes,
}) => {
const renderFunctionByContentType = /* @__PURE__ */ new WeakMap();
const untouchedEntries = new Set(store.keys());
async function syncData(entry, base, entryType, oldId) {
if (!entryType) {
logger.warn(`No entry type found for ${entry}`);
return;
}
const fileUrl = new URL(encodeURI(entry), base);
const contents = await fs.readFile(fileUrl, "utf-8").catch((err) => {
logger.error(`Error reading ${entry}: ${err.message}`);
return;
});
if (!contents && contents !== "") {
logger.warn(`No contents found for ${entry}`);
return;
}
const { body, data } = await entryType.getEntryInfo({
contents,
fileUrl,
});
const id = generateId({ entry, base, data });
if (oldId && oldId !== id) {
store.delete(oldId);
}
untouchedEntries.delete(id);
const existingEntry = store.get(id);
const digest = generateDigest(contents);
const filePath2 = fileURLToPath(fileUrl);
if (
existingEntry &&
existingEntry.digest === digest &&
existingEntry.filePath
) {
if (existingEntry.deferredRender) {
store.addModuleImport(existingEntry.filePath);
}
if (existingEntry.assetImports?.length) {
store.addAssetImports(
existingEntry.assetImports,
existingEntry.filePath,
);
}
fileToIdMap.set(filePath2, id);
return;
}
const relativePath2 = posixRelative(
fileURLToPath(config.root),
filePath2,
);
const parsedData = await parseData({
id,
data,
filePath: filePath2,
});
if (
existingEntry &&
existingEntry.filePath &&
existingEntry.filePath !== relativePath2
) {
const oldFilePath = new URL(existingEntry.filePath, config.root);
if (existsSync(oldFilePath)) {
logger.warn(
`Duplicate id "${id}" found in ${filePath2}. Later items with the same id will overwrite earlier ones.`,
);
}
}
if (entryType.getRenderFunction) {
let render = renderFunctionByContentType.get(entryType);
if (!render) {
render = await entryType.getRenderFunction(config);
renderFunctionByContentType.set(entryType, render);
}
let rendered = void 0;
try {
rendered = await render?.({
id,
data,
body,
filePath: filePath2,
digest,
});
} catch (error) {
logger.error(`Error rendering ${entry}: ${error.message}`);
}
store.set({
id,
data: parsedData,
body: globOptions.retainBody === false ? void 0 : body,
filePath: relativePath2,
digest,
rendered,
assetImports: rendered?.metadata?.imagePaths,
});
} else if ("contentModuleTypes" in entryType) {
store.set({
id,
data: parsedData,
body: globOptions.retainBody === false ? void 0 : body,
filePath: relativePath2,
digest,
deferredRender: true,
});
} else {
store.set({
id,
data: parsedData,
body: globOptions.retainBody === false ? void 0 : body,
filePath: relativePath2,
digest,
});
}
fileToIdMap.set(filePath2, id);
}
let baseDir;
if (isLegacy && !globOptions.base) {
baseDir = new URL(`./src/content/${collection}`, config.root);
} else {
baseDir = globOptions.base
? new URL(globOptions.base, config.root)
: config.root;
}
if (!baseDir.pathname.endsWith("/")) {
baseDir.pathname = `${baseDir.pathname}/`;
}
const filePath = fileURLToPath(baseDir);
const relativePath = relative(fileURLToPath(config.root), filePath);
const exists = existsSync(baseDir);
if (!exists) {
logger.warn(
`The base directory "${fileURLToPath(baseDir)}" does not exist.`,
);
}
const files = await tinyglobby(globOptions.pattern, {
cwd: fileURLToPath(baseDir),
expandDirectories: false,
ignore,
});
if (exists && files.length === 0) {
logger.warn(
`No files found matching "${globOptions.pattern}" in directory "${relativePath}"`,
);
return;
}
function configForFile(file) {
const ext = file.split(".").at(-1);
if (!ext) {
logger.warn(`No extension found for ${file}`);
return;
}
return entryTypes.get(`.${ext}`);
}
const limit = pLimit(10);
const skippedFiles = [];
const contentDir = new URL("content/", config.srcDir);
const configFiles = new Set(
["config.js", "config.ts", "config.mjs"].map(
(file) => new URL(file, contentDir).href,
),
);
function isConfigFile(file) {
const fileUrl = new URL(file, baseDir);
return configFiles.has(fileUrl.href);
}
await Promise.all(
files.map((entry) => {
if (isConfigFile(entry)) {
return;
}
return limit(async () => {
const entryType = configForFile(entry);
await syncData(entry, baseDir, entryType);
});
}),
);
const skipCount = skippedFiles.length;
if (skipCount > 0) {
const patternList = Array.isArray(globOptions.pattern)
? globOptions.pattern.join(", ")
: globOptions.pattern;
logger.warn(
`The glob() loader cannot be used for files in ${colors.bold("src/content")} when legacy mode is enabled.`,
);
if (skipCount > 10) {
logger.warn(
`Skipped ${colors.green(skippedFiles.length)} files that matched ${colors.green(patternList)}.`,
);
} else {
logger.warn(
`Skipped the following files that matched ${colors.green(patternList)}:`,
);
skippedFiles.forEach((file) =>
logger.warn(`\u2022 ${colors.green(file)}`),
);
}
}
untouchedEntries.forEach((id) => store.delete(id));
if (!watcher) {
return;
}
watcher.add(filePath);
const matchesGlob = (entry) =>
!entry.startsWith("../") &&
picomatch.isMatch(entry, globOptions.pattern);
const basePath = fileURLToPath(baseDir);
async function onChange(changedPath) {
const entry = posixRelative(basePath, changedPath);
if (!matchesGlob(entry)) {
return;
}
const entryType = configForFile(changedPath);
const baseUrl = pathToFileURL(basePath);
const oldId = fileToIdMap.get(changedPath);
await syncData(entry, baseUrl, entryType, oldId);
logger.info(`Reloaded data from ${colors.green(entry)}`);
}
watcher.on("change", onChange);
watcher.on("add", onChange);
watcher.on("unlink", async (deletedPath) => {
const entry = posixRelative(basePath, deletedPath);
if (!matchesGlob(entry)) {
return;
}
const id = fileToIdMap.get(deletedPath);
if (id) {
store.delete(id);
fileToIdMap.delete(deletedPath);
}
});
},
};
}
export { extendedGlob, secretLegacyFlag };
+1 -1
View File
@@ -49,7 +49,7 @@ const GemtextHTMLRenderer: Gemtext.Renderer<string> = {
const post = Astro.props; const post = Astro.props;
const Content = post.filePath?.endsWith('.gmi') const Content = post.filePath?.endsWith('.gmi')
? Gemtext.parse(post.body ?? '').generate(GemtextHTMLRenderer) ? Gemtext.parse((post.body ?? '').replace(`# ${post.data.title}`, '')).generate(GemtextHTMLRenderer)
: (await render(post)).Content; : (await render(post)).Content;
--- ---