Compare commits

...

64 Commits

Author SHA1 Message Date
joeac 29db3ce86a first draft of http.Dockerfile 2026-06-18 20:16:22 +01:00
joeac 2d89475f0b renames capsule -> gemini 2026-06-18 20:01:32 +01:00
joeac f71fbdafac updates readme: swaps website for http 2026-06-18 19:58:41 +01:00
joeac 90b16dad40 remove website service from compose.yml 2026-06-18 19:36:13 +01:00
joeac 05b9308c1b adds redirect from links/rss.xml -> microlog/rss.xml 2026-06-18 19:22:18 +01:00
joeac ca8e6824ea remove Astro website 2026-06-18 19:21:59 +01:00
joeac a0f2505728 updates subscribe.upphtml: no /rss.xml 2026-06-18 19:21:01 +01:00
joeac 3651679c86 adds microlog/rss.uppxml 2026-06-18 19:18:01 +01:00
joeac b0bffe5d72 rss.xml resilient in case multiple blog posts on the same day 2026-06-18 19:15:56 +01:00
joeac 86446de9a8 meta.upphtml accepts NO_META flag 2026-06-18 19:11:02 +01:00
joeac a63b8c6a0b replaces <hr /> with --- in 2024-07-16.gmi 2026-06-18 19:09:42 +01:00
joeac ef681069c7 adds component_files to blog_post_rule, and adds component_files
variable
2026-06-18 19:08:42 +01:00
joeac 5797bc85bf adds blog_rss_feed_rule to http/Makefile 2026-06-18 19:08:17 +01:00
joeac 4c829133a3 adds component files to out_rule deps 2026-06-18 19:07:56 +01:00
joeac 2328446520 more sophisticated escape_upphtml in gmi2upphtml 2026-06-18 19:07:45 +01:00
joeac af4adc0fc8 remove random ; in rss.uppxml 2026-06-18 19:04:34 +01:00
joeac f61a00d292 rss.uppxml uses ' instead of " for version 2026-06-18 19:04:23 +01:00
joeac 7d4ce6f073 rss.xml uses description instead of content:encoded 2026-06-18 19:04:11 +01:00
joeac db5b029f78 blog/rss.xml has no meta tags in content 2026-06-18 19:03:39 +01:00
joeac e8754afca2 blog/rss.xml escapes HTML 2026-06-18 19:03:24 +01:00
joeac c179adccb2 replace <hr> with <hr/> 2026-06-18 18:25:28 +01:00
joeac caed7bbfdf http: adds blog/rss/xml 2026-06-18 10:13:54 +01:00
joeac ff9df11a15 symlinks http/share/man/man1/pp.1 -> http/pp/pp.1 2026-06-18 09:49:51 +01:00
joeac 5396812290 blog/microlog posts have link to index 2026-06-18 09:47:46 +01:00
joeac 9996a47d0c fixes missing post titles in microlog 2026-06-18 09:43:52 +01:00
joeac abd249030b http: adds post.css to blog/microlog posts 2026-06-18 09:43:32 +01:00
joeac 3c132ca4dc http/Makefile doesn't prune non-post files in
http/src/blog,http/src/microlog
2026-06-18 09:25:04 +01:00
joeac f219908d67 prune_blog takes optional BLOG_POST_PATTERN argument 2026-06-18 09:22:49 +01:00
joeac 5e371cf734 prune_blog also removes argfiles 2026-06-18 09:20:50 +01:00
joeac 76901304c4 Makefile clean doesn't clean non-post files under blog,microlog 2026-06-18 09:18:03 +01:00
joeac 83c25e328f only gitignores blog/microlog posts, not everything under
http/src/blog,http/src/microlog
2026-06-18 08:58:32 +01:00
joeac ccd7140d6d http: redirects blog/subscribe to /subscribe 2026-06-18 08:58:02 +01:00
joeac 0db52b89f0 adds trivial html layout 2026-06-18 08:57:48 +01:00
joeac 93cda87bbd http: adds microlog index 2026-06-18 08:46:45 +01:00
joeac 35ef330df8 fix css bug in http microlog: ul inside microlog posts not getting list
markers
2026-06-18 08:45:51 +01:00
joeac 348b76aa04 http/Makefile: fix bug, only upphtml files being built because no
layouts exist for other formats
2026-06-18 08:44:29 +01:00
joeac df748183ab http: adds /subscribe 2026-06-18 08:19:45 +01:00
joeac e470dfc258 http/Makefile includes layout files in deps 2026-06-18 08:17:08 +01:00
joeac 56e2c037b5 moves <main> to default.layout 2026-06-18 08:16:51 +01:00
joeac c5c4167361 remove 2025-10-05 blog post 2026-06-17 22:08:18 +01:00
joeac 2f70f87f41 reverses blog post order 2026-06-17 22:06:05 +01:00
joeac 0519d155fa fixes blockquote block margins 2026-06-17 22:04:36 +01:00
joeac 6d801d0f0e symlinks http/bin/pp to http/pp/pp 2026-06-17 21:54:36 +01:00
joeac fd21d77526 adds pp build to http/Makefile 2026-06-17 21:54:16 +01:00
joeac 5b927900ae increases pp max buffer size 64,000 chars 2026-06-17 21:49:02 +01:00
joeac 5fcb2bcc83 gitignores pp build output 2026-06-17 21:48:24 +01:00
joeac 6dd4caf947 adds pp sources 2026-06-17 21:48:24 +01:00
joeac 506d1adfcc fix bug in Makefile out_rule 2026-06-17 18:35:39 +01:00
joeac e506f7c2e6 reorganises Makefile, adds headings 2026-06-17 18:31:46 +01:00
joeac 574b2d6646 Makefile: named params in out_rule macro 2026-06-17 18:27:13 +01:00
joeac b0adbd4419 parameterises blogs in all rule 2026-06-17 18:23:27 +01:00
joeac 8125081177 parameterises blogs in clean rule 2026-06-17 18:23:11 +01:00
joeac 229339dc17 macroises prune_blog rules 2026-06-17 18:22:43 +01:00
joeac 99b54df287 fix bug in prune_blog: rm '' 2026-06-17 18:21:28 +01:00
joeac 70689047ca macroises blog_rule 2026-06-17 18:19:09 +01:00
joeac b4be3ed2a6 re-orders Makefile and renames some vars 2026-06-17 18:18:51 +01:00
joeac 8e814917b6 Makefile: blogs are configurable 2026-06-17 17:59:18 +01:00
joeac a8686d9169 re-orders Makefile 2026-06-17 09:56:48 +01:00
joeac 830f63641b factors out prune_blog to bin 2026-06-17 09:56:48 +01:00
joeac 3e0def4305 remove unnecessary continue indices in prune_out 2026-06-17 09:34:05 +01:00
joeac 722a304023 factors out prune_out to bin 2026-06-17 09:23:42 +01:00
joeac 423fa1ed26 Makefile: macroises blog_rule 2026-06-17 09:12:09 +01:00
joeac 6cdc0ad1e6 Makefile: renames blog_rule macro 2026-06-17 09:10:37 +01:00
joeac 65b3169d98 fix bug in blog script: set layout_name to default 2026-06-17 09:08:39 +01:00
147 changed files with 1179 additions and 16518 deletions
+2 -2
View File
@@ -6,9 +6,9 @@ Structure:
``` ```
/ /
├── capsule My Gemini capsule ├── gemini My Gemini capsule
├── smtp A local SMTP server ├── smtp A local SMTP server
└── website My Website └── http My Website
``` ```
## Running with Podman ## Running with Podman
+1 -1
View File
@@ -167,7 +167,7 @@ Some authors have managed to recognise the ambiguity of the term. Both King et a
So it seems that although many academics have treated the term hallucination as jargon, in actual fact, there is no widely agreed specific meaning of the word. So it seems that although many academics have treated the term hallucination as jargon, in actual fact, there is no widely agreed specific meaning of the word.
<hr> ---
I will slide in here with a quick side note. While some authors treated hallucination as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word hallucination at all. Here are some examples: I will slide in here with a quick side note. While some authors treated hallucination as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word hallucination at all. Here are some examples:
-146
View File
@@ -1,146 +0,0 @@
# "381: how the church as we know it was made"
The church which defines our world now is in a significant way the one which emerged out of sixty years of controversy from the Council of Constantinople in 381. I've been charting what happened, why, and the ongoing legacy.
Published on: 5 Oct 2025
Athanasius defined the fourth century. Not that he was a god, or even a king, or that he always got his way. But he wrote the history books. His tale of an epic battle fought tooth-and-nail between Arian heretics and him and his loyal allies has come to be the standard account of how, over the course of the fourth century, the Church redefined what orthodoxy means and how it is declared and identified.
The result was the Nicene Creed. It had been first written for a very particular polemical purpose in 325, but later found itself the centre of a strange theological revival, and was finally revised in a council at Constantinople in 381. In so doing, the bishops assembled a recognisable 'Nicene' tradition which is still one of the defining features of planet Earth.
For better and for worse, the church as we know it has a capacity both for great humility, faith and submission to the mystery of God, but it also has a capacity for great intolerance. This is the church created in 381.
To understand the church as we know it today, then, we need to understand the complex, confusing journey from 325 to 381.
Athanasius' chronicle of that journey is temptingly simple. The only problem with it is that it isn't true. Indeed, his 'history' was never meant to function as an all-encompassing narrative of Church history, to be read for centuries ever after. His accounts function as polemics, meant to cajole, condemn and persuade his readers in his own time of his vision for their future.
Nevertheless, whatever Athanasius' real significance in how his times unfolded, his witness is important. He fully inhabited his times, often in the middle of the fray. Whether or not we buy Athanasius' portrayal of himself as fighting the good fight, he was certainly a fighter. By looking through his eyes, then, we can get a perspective on how the Church as we know it came to be.
So it makes sense to start with him. As a young priest in his native Alexandria, he became tangled up in a controversy which would come to define his career. A strong-minded and fearless young priest had begun to preach. His name was Arius.
---
According to the Egyptian tradition, Alexander, the bishop of Alexandria, was the nineteenth in a direct line of succession from Mark the Evangelist himself. With a great deal of justice, he would have regarded himself as one of the most important Christian leaders in the world, and at least the equal of the bishop of Rome.
Small wonder, then, that the insubordination that plagued his diocese bothered him. First Erescentius had started a schism, disputing the rule he used for calculating the date of Easter.
Then there was Meletius. During the persecution under the Roman emperor Diocletian, Meletius had already rubbed a few people the wrong way: while other bishops were in hiding or in prison, he took the initiative to resolve problems and ordain priests without properly consulting the absent bishops' representatives. Perhaps it was intended kindly: it was seen as meddling. Now Meletius accused Alexander of being too soft on Christians who had caved into the threat of torture and made sacrifices to the pagan cults. When he decided Alexander was never going to match his high rigorist standards, he broke away, too.
Alexander must have longed for the relatively good order of the Greek and Roman churches, where bickering subordinates were generally willing to let their bishop have the last say. The throne of St Mark was in trouble. If Christ's body wasn't to get chopped up any more than it already was, he needed to establish his personal authority.
This was the context in which Arius, a young firebrand priest, steps onto stage right. He surely knew his own bishop's teaching: God is one substance and one essence, unchangeable, indivisible. Christ his Son is in every way God: God from God, light from light, true God from true God, eternally begotten of the Father before all ages. How else could Christ, by adopting human flesh, mediate the transcendent God to fallen humanity?
But Arius didn't like this one bit. If God is unchangeable, how could he adopt flesh? That suggests he was not flesh, and then became flesh. And in any case, if the martyrs were right to give up their lives to know God, he must have the perfect, uncompromising transcendence which the martyrs so admired. But how can God adopt flesh, never mind suffer and die on a cross, without compromising that transcendence? Something had to give. For Arius, the solution was to modify the relationship between the Father and the Son.
Arius accepted that Christ had to be in some sense divine, in order to mediate God to humanity. But he denied that he was quite as much God as God is. He has something like his Father's essence, not in a co-equal way, but rather in a derivative way. This makes sense of Father-Son language, which suggests the Father came first, and the Son came next, a derivative of the Father. So the Son is God from God, but not true God from true God. The Son was begotten in time, and is not eternal: only God the Father himself is eternal.
At another time in another place, Arius might have passed for a creative, independent thinker without much notice. But Arius was directly contradicting Alexander just as the latter was desperate to assert his authority. It got ugly.
Alexander called a council of local bishops in about 320. The council condemned Arius and removed him from his post as priest. In response, Arius went on the campaign trail, visiting bishops in Palestine and Asia Minor who he thought would be sympathetic to his theology. Shortly afterwards, he returned to Alexandria, triumphantly brandishing vindications from two councils, one in Jerusalem and one in Bithynia. He wasn't going to make it easy for Alexander.
Luckily for Alexander, the Emperor Constantine had just united the eastern and western halves of the Empire. He had famously converted to Christianity after seeing the sign of the cross at the Battle of Milvian Bridge in 312, and saw the bishops as means towards his mission of uniting the Empire under one government and one God. Constantine had been made aware of the dispute between Arius and Alexander, and he didn't want schisms in the church any more than Alexander did.
He called a council in his own palace in Nicaea, paying the travel expenses and hotel bills of all the bishops in attendance. For those bishops, many carrying the scars of torture they had endured under Diocletian, it must have been a bewildering experience. Alexander was in attendance. His secretary was Athanasius.
In 325, the council condemned Arius. To avoid anyone else following in his path, they produced a statement of faith, designed to exclude Arius' teaching, no matter who taught it. This statement of faith is now known as the Nicene Creed.
The council also fixed the date of Easter to boot. Alexander must have been relieved.
You might have thought that would have been the end for Arius. In fact, Constantine engineered his re-admittance into the church as soon as 328. Arius died in peace in 336. Constantine's mission wasn't to purge the church, but to unite the church. As long as all sides worshipped God and could live in peace, he wanted as many people as possible included. His mission was unity, not uniformity.
Bishops like Eusebius of Caesarea in Syria got this. He had been provisionally excommunicated on suspicion of Arianism in 325, but was reconciled at Nicaea given the chance to explain himself and sign up to the Nicene Creed. No sooner had he done this, however, than he had started explaining to the faithful back home how they could carry on believing that the Son was not really eternal, even as the Creed was designed to exclude exactly such a claim. While Eusebius might seem duplicitous, at the time, this was exactly the kind of tolerant pragmatism that Constantine asked of the bishops: as long as they didn't cause more out-and-out conflict.
Alexander didn't have long to enjoy the peace of Nicaea. He died just a few years afterward in 328. The throne of St Mark passed to Athanasius.
---
The peace didn't last long. Just as Athanasius was donning his mitre, Eusebius was plotting against Eustathius the bishop of Antioch, and engineered his deposition. In his defence, Eusebius accused Eustathius of the long-condemned heresy, Sabellianism. Then in 335, he followed up by deposing Marcellus, the bishop of Ancyra, at a council in Tyre.
To defend his action, he wrote _Against Marcellus_, in which he accused Marcellus of being a Sabellian, too. Sabellius' heresy was (to borrow a modern term) modalism, the view that 'Father', 'Son' and 'Spirit' are mere titles, aspects, 'modes' of God, not in any real way distinct. He also accused Marcellus of adoptionism, another agreed heresy. Marcellus taught that the Son only became an aspect of the divine nature at the Incarnation, and that in the last day, Christ would hand over his kingdom to his Father.
This action would cast a long shadow over the next half-century. Time and again, bishops allied to Eusebius' way of thinking, or 'Eusebians', would re-affirm their opposition to that 'heretic' Marcellus and his 'Sabellianism'. This is a crucial dynamic for understanding where theological factions drew up their battle lines, and for what compromises were needed in order to get to 381.
Even the bishop of Alexandria wasn't immune from Eusebius' purge. Athanasius had vigorously defended his ally, Marcellus, at the council of Tyre in 335. Eusebius set about plotting his downfall. He dug up dirt. He accused Athanasius of using threats and bribes to get himself elected, and sending goons to beat up his political opponents. Once he'd found evidence of Athanasius meddling with the crucial Egyptian grain export that kept Rome fed, he had the emperor on side. Constantine convened a meeting in 336 and exiled him to the German frontier.
---
Or at least, that's how Athanasius tells it. Athanasius loves a plot: at the time, alleging a conspiracy was a classic rhetorical technique for painting your enemies as heretics.
Eusebius was no stranger to rhetoric himself, and it's to his 337 best-seller, the _Life of Constantine_, that we owe our standard account of Constantine's reign. He regarded Empire and Church as allies in a joint mission, to unite the world under one government and one faith. To him, someone like Athanasius, constitutionally incapable of tolerating anyone who disagreed with him and willing to use gangster tactics to get his way, was a threat to this divine mission.
It's worth remembering that after Constantine died, Athanasius would be re-exiled by four more Roman emperors. In his lifetime, only Julian failed to exile Athanasius, and him only perhaps because he didn't have time in his whirlwind twenty-month reign. We also can't be sure how much influence Eusebius actually had in the expulsion of Athanasius and his allies: it coheres well enough with the emperor's anti-sectarian agenda that it might have happened with or without Eusebius' involvement.
Perhaps Athanasius was a brute. Still, the Roman Catholic Church manages to venerate both Eusebius and Athanasius as saints. This may seem like a contradiction. But perhaps an ability to tolerate contradiction is precisely the legacy of 381.
But we're not there yet. By 335, Eusebius had engineered the exile of Eustathius, Marcellus, and Athanasius. After Constantine died, he had to do it all over again, but by 339, he had persuaded his successor, Constantius, to re-assert his father's exiles of the three men. With the Empire once again split, Athanasius and Marcellus headed to Rome to re-group and re-think.
---
From Rome, Athanasius and Marcellus were safe for now from Eusebius' clutches, but also relatively impotent. In this period of exile in the 340s, in an effort to claw back his reputation, Athanasius developed the polemic which still defines the standard history of the fourth century. He invented a cunning label for Eusebius and his cronies: he called them 'Arians'.
Eusebius rejected the label as ridiculous. Arius had been reconciled, and more to the point, had died in 336. For that matter, why would a bishop follow the teaching of a mere priest? Not only that, but the label ignored significant differences between Arius' and Eusebius' teaching. His verdict was clear: the label 'Arian' is a baseless slur, with no other purpose than to tar his reputation as a heretic.
He was right, of course. But like it or not, Athanasius' theory of an Arian conspiracy began to win adherents, not least Julian, the bishop of Rome. Julian called a council to exonerate Athanasius and Marcellus. When the Greeks refused to turn up, he called a local council anyway and vindicated the two men. In the face of Greek obstinacy, Julian wrote east, pleading the bishops to take the 'Arian' threat seriously.
In response, the easterners held a council in Antioch in 341, agreeing four creeds which powerfully condemned Marcellus' teaching, including the influential Dedication Creed. This includes assertions that Father, Son and Spirit are 'three in subsistence, one in agreement', that the Son was generated before time began, against Marcellus' teaching that the Father, Son and Spirit are aspects of God without division in subsistence, and that there only came to be a divine Son at his incarnation. They explicitly condemned Arius, Sabellius and Marcellus.
So the divisions grew deeper. Without an emperor to compel the bishops to come together, there may not have been much chance of a rapprochement. But even if there were to be such an emperor, who's to say that their settlement would have satisfied the bishops?
---
Meanwhile, in the 340s and through the 350s, two further theological movements gathered steam: the homoians and the heterousians.
The homoians, perhaps tired of the squabbles between the Athanasian and Eusebian factions, determined to sidestep their petty debates altogether.
A key term of the theological disagreement was 'essence' or 'ousia'. Athanasius, in his lifelong battle to make sure Arius stayed dead, insisted that Father, Son and Spirit shared the same ousia. In contrast, Eusebius, with his anti-Sabellian polemic, needed to assert the real distinction between Father, Son and Spirit, and so asserted that each had a separate ousia. So the difference can be summed up as a counting problem. How many divine ousias are there? One or three?
The homoians claimed that both sides were mistaken, simply because they used the word 'ousia'. There is no mention of ousia in Scripture, so, they claimed, we have no basis for asserting it of God one way or the other. All we can truly say is that Father, Son and Spirit are distinct but somehow alike. Whereof we cannot speak, there must we remain silent.
This might have worked as a way forward, except that the heterousians provoked such a strong reaction that 'ousia'-talk was needed to refute them. Aetius, and his followed Eunomius, argued that since God is simple, and all generate things are divided, it follows that God is ingenerate. But the Son is generate: therefore Father and Son must be altogether unalike. They expressed this by saying that Father and Son are unlike in ousia. This teaching was swiftly branded 'neo-Arian', provoking a strong reaction. To counter the heterousian teaching, their opponents were forced to fight on their terms, and that meant using 'ousia'-talk.
Thus enters Basil of Caesarea. He argued that if we abandon 'ousia'-talk, we will have no way of saying that the Father and Son have anything in common at all, which makes a nonsense of the idea that the Son brings humanity knowledge of his Father. Without like essence, they might as well be two completely different Gods. Therefore we have to say at least that they have like essence -- 'homoiousia'. But without direct access to perfect knowledge of the invisible God, we're not in a position to judge that they have exactly the same essence, so he stopped short of agreeing with the 'homoousia' of the Nicene Creed which Athanasius so treasured.
Seeing the opportunity to make common cause against the homoians, Athanasius started to soften. He wrote an extremely charitable commentary on Basil's theology which emphasised their similarities and papered over their differences. Athanasius recognised that both he and Basil wanted to assert the unity of God while still preserving distinctions between Father, Son and Spirit. The two began to campaign against the homoian movement.
But Basil got there too late. In 359, the emperor Constantine II called a council in Constantinople, and in 360 it issued a homoian creed with full imperial backing. Any campaign against the homoians would have to take place sub rosa.
---
In Athanasius' and Basil's long, slow campaign against homoianism, their weapon of choice was surprising: they dusted off the Nicene Creed of 325. Athanasius argued, against the homoians, that 'ousia'-talk, although not directly Scriptural, was essential in order to draw out the consequences of Scripture while ruling out Arian mis-interpretations.
Thus Nicaea, conceived as a one-off meant to clean up the Arian controversy, found a new life as the anti-homoian movement -- or perhaps you could call it the Nicene revival? -- rallied around it.
As the movement progressed, the formerly disagreeing bishops found ways to come together. An essential move was that made in Athanasius' _Antiochene Tome_ of 362. In it, he relented on his long opposition to there being three 'hypostases' or 'substances' in the Godhead.
'Hypostasis' had for a long time been used interchangeably with 'ousia'. However, Athanasius claimed that perhaps God could have three hypostases, but only one ousia, at the same time. In so doing, he wedged apart a sharp technical distinction between 'hypostasis' and 'ousia' which previously wouldn't have made sense. Logical or not, it enabled the Nicene revival to have its cake and eat it. God is both one in ousia, protecting against Arianism, and three in hypostasis, protecting against Sabellianism.
So the Nicene revival gained a new superpower: the power to use formerly synonymous terms to assert contradictions without blushing. This power to accept apparent contradiction as part of the unknowable mystery of God is perhaps the most important legacy of the period. Arguably, the church has been at its best when it has put aside the need to know everything, and embraced this spirit of tolerance, humility and faith.
---
For much of the 360s and 370s, the homoian emperor Valens had ruled over the eastern part of the Empire, while his big brother, Valentinian, ruled the west. In the late 370s, Valentinian and then Valens died within quick succession of each other. Valentinian's twenty-year-old son, Gratian, was left to clear up the mess. In 379, Gratian delegated rule of the east to Theodosius, who was to implement a decisively different religious policy than his predecessor, Valens.
In 380, Theodosius issued an edict, saying that only those who agreed to the homoousios clause of the Nicene Creed could be considered 'catholic' Christians. The message was clear: the homoians were out, and the Nicenes were in.
In 381, he called a council to Constantinople, and it (probably) issued the revision of the 325 creed which is still used in various versions in all the world's largest Christian denominations. There would be no more revisions, and it would become, then as now, compulsory reading for all those preparing to don vestments.
One question is, why did the 381 creed differ in the ways it did from 325? Many of the differences, including the much-enlarged section on the Son, seem to have little controversial content: nobody was disputing that Jesus was born of the Virgin Mary, for example, though she makes her first appearance in the Creed in the 381 version. Some historians think this suggests that the 381 was based on a similar, but distinct creed from 325. This seems unlikely to me, given that about half the creed is in verbatim agreement with 325.
However, a couple of edits stand out. There are some clear signs of anti-Marcellianism: 'his \[the Son's] kingdom shall have no end', the Son is begotten of the Father 'before all ages'. Perhaps a clear emphasis on the eternal relationship between the Son and the Father was part of the diplomacy needed to get the Eusebian faction on-side.
The new details on the Holy Spirit are interesting too. They suggest a delicate compromise. Some bishops were reluctant to suppose that the Father and the Spirit have the same essence. On the other hand, others reckoned that they must share the same essence, given that they are equally deserving of worship. Thus the creed does not have a 'homoousios' clause for the Spirit, but does assert that the Spirit 'together with the Father and with the Son is worshipped and glorified'. With a spoonful of humility, both sides can be satisfied with that.
The revised Nicene Creed was the focus point, the distillation of a growing theological movement, formed by the various anti-homoian bishops finding a way to keep true to their own convictions while respecting each other's red lines.
As a result of the context of 325, Athanasius' relentless anti-Arian polemic which kept that movement alive, and the 'neo-Arian' heterousian movement, the new Nicene tradition insisted on the full co-equal divinity of Father, Son and Holy Spirit. This doctrine ensures Nicenes can affirm that Christ mediates true knowledge of the transcendent Godhead to humanity: the one who was born of Mary, suffered and died on the cross, was raised from the dead and ascended into heaven was true God from true God, of the same essence as his Father.
To satisfy the Eusebian strain, which defined itself by opposition to Marcellus, the Nicene tradition included a commitment to a robust distinction between Father, Son and Spirit, and to the eternity of the Son: begotten of the Father before all ages, his kingdom shall have no end. As a result, Nicenes inherited a way of thinking about God's action in the world, as instrinsically co-operative without being divided.
The biggest change between 325 and 381 was not the text, but what the text is used for. In 325, the Creed functioned to condemn Arius in order to heal the divisions his teachings had caused. In its second life, the Creed found an altogether new purpose: to serve as a common statement of orthodox faith. It started life as a way to define who was out. It ended up defining who was in.
Where was Athanasius? Consider that when Athanasius was appointed bishop in 328, he was relatively young for a bishop at thirty-five. That means that in 381, he would have been the ripe old age of eighty-eight. In fact, he didn't make it that far: he died in peace in the countryside outside his native Alexandria in 373. If he had seen the outcome of 381, he might have regarded his life project complete. Perhaps he knew that with the new generation of bishops, the tide was turning for good, and died in peace. Perhaps not. Either way, his compromises, and his beloved homoousios, have left a permanent mark on the church.
This is the legacy of 381. It is two-faced: any common statement of faith can be used to exclude. Indeed, in the late fourth century, both non-Nicene Christians and pagans found themselves the victims of increasing state-backed sectarian violence.
However, 381 also bears witness to the power of humility and faith. Once we stop grasping at perfect knowledge we cannot attain, we can begin to appreciate the mystery of God. This is one legacy I hope we can carry forward into our century.
+7 -7
View File
@@ -1,17 +1,17 @@
services: services:
capsule: gemini:
image: git.joeac.net/joeac/joeac.net-capsule image: git.joeac.net/joeac/joeac.net-gemini
build: build:
context: . context: .
dockerfile: capsule.Dockerfile dockerfile: gemini.Dockerfile
ports: ports:
- "1965:1965" - "1965:1965"
website: http:
image: git.joeac.net/joeac/joeac.net-website image: git.joeac.net/joeac/joeac.net-http
build: build:
context: . context: .
dockerfile: website.Dockerfile dockerfile: http.Dockerfile
args: args:
LOCAL_SMTP_HOST: smtp LOCAL_SMTP_HOST: smtp
LOCAL_SMTP_PORT: 2500 LOCAL_SMTP_PORT: 2500
@@ -19,7 +19,7 @@ services:
env_file: env_file:
- .env - .env
ports: ports:
- "8000:4321" - "8080:8080"
smtp: smtp:
image: git.joeac.net/joeac/joeac.net-smtp image: git.joeac.net/joeac/joeac.net-smtp
+2 -2
View File
@@ -16,7 +16,7 @@ RUN wget -O - https://git.sr.ht/~nytpu/comitium/archive/v1.8.2.tar.gz | tar -xz
FROM alpine:3.23 AS final FROM alpine:3.23 AS final
RUN mkdir -p /var/app/content RUN mkdir -p /var/app/content
WORKDIR /var/app WORKDIR /var/app
COPY capsule/.certificates .certificates COPY gemini/.certificates .certificates
RUN crontab -l > crontab.tmp \ RUN crontab -l > crontab.tmp \
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \ && echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
&& crontab crontab.tmp \ && crontab crontab.tmp \
@@ -31,7 +31,7 @@ RUN rc-update add crond
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
COPY capsule . COPY gemini .
COPY common /var/common COPY common /var/common
RUN make RUN make
View File
+5
View File
@@ -0,0 +1,5 @@
FROM nginx:1.30.3-alpine3.23
RUN apk --update add build-base make
RUN make --directory=http --makefile=http/Makefile
COPY http/php /usr/share/nginx/php
COPY http/out /usr/share/nginx/html
+4 -2
View File
@@ -1,4 +1,6 @@
out out
vendor vendor
src/microlog src/microlog/*.microlog.upphtml
src/blog src/microlog/*.microlog.upphtml.args
src/blog/*.blog.upphtml
src/blog/*.blog.upphtml.args
+93 -77
View File
@@ -1,97 +1,100 @@
all: microlog blog public out include config.mk
.PHONY: prune
prune: prune_microlog prune_blog prune_out
.SILENT: prune_out #############
.PHONY: prune_out # VARIABLES #
prune_out: #############
for out in $$(find out -type f); do \
out_basename="$$(basename "$$out")"; \
out_ext="$${out_basename##*\.}"; \
rel_path="$$(echo "$${out}" | cut -c5-)"; \
if [ -f "public/$${rel_path}" ]; then \
continue 2; \
fi; \
base_rel_path="$${rel_path%%.$${out_ext}}"; \
src_path="src/$${base_rel_path}.upp$${out_ext}"; \
if [ -f "$$src_path" ]; then \
continue 2; \
fi; \
for layout in $$(find share/layouts -name "*.layout.upp$${out_ext}"); do \
layout="$$(basename "$$layout")"; \
layout="$${layout::$${#layout}-15}"; \
rel_path="$$(echo "$${out}" | cut -c5-)"; \
base_rel_path="$${rel_path%%.$${out_ext}}"; \
src_path="src/$${base_rel_path}.$${layout}.upp$${out_ext}"; \
if [ -f "$$src_path" ]; then \
continue 2; \
fi; \
done; \
echo "No source file found to match $${out}: removing it"; \
rm "$$out"; \
done
.SILENT: prune_microlog
.PHONY: prune_microlog
prune_microlog:
for post in $$(find src/microlog -name *.microlog.upphtml); do \
post_basename="$$(basename "$$post")"; \
post_src="blogs/microlog/$${post_basename::$${#post_basename}-17}.gmi"; \
if ! [ -f "$$post_src" ]; then \
echo "Microlog post source file $${post_src} has been deleted: removing $${post}"; \
rm -f "$$post" "$${post}.args"; \
fi; \
done
.SILENT: prune_blog
.PHONY: prune_blog
prune_blog:
for post in $$(find src/blog -name *.blog.upphtml); do \
post_basename="$$(basename "$$post")"; \
post_src="blogs/blog/$${post_basename::$${#post_basename}-13}.gmi"; \
if ! [ -f "$$post_src" ]; then \
echo "Blog post source file $${post_src} has been deleted: removing $${post}"; \
rm -f "$$post" "$${post}.args"; \
fi; \
done
src_files := $(filter-out %.args,$(wildcard src/*.upp*)) src_files := $(filter-out %.args,$(wildcard src/*.upp*))
src_files += $(filter-out %.args,$(wildcard src/**/*.upp*)) src_files += $(filter-out %.args,$(wildcard src/**/*.upp*))
src_files_ext := $(suffix $(src_files)) src_files_ext := $(suffix $(src_files))
out_files_ext := $(src_files_ext:.upp%=%) out_deps_ext := $(src_files_ext:.upp%=%)
component_files := $(shell find -L share/components -type f)
layout_files := $(wildcard share/layouts/*.layout.upp*) layout_files := $(wildcard share/layouts/*.layout.upp*)
layouts := $(notdir $(basename $(basename $(layout_files)))) layouts := $(notdir $(basename $(basename $(layout_files))))
out_files := $(addprefix out/,$(src_files:src/%=%))
remove_layout_from_out_files = out_files := $$(subst $(1).upp,.,$$(out_files))
$(foreach layout,$(layouts),$(eval $(call remove_layout_from_out_files,.$(layout))))
$(eval $(call remove_layout_from_out_files,))
define out_rule_for_ext_and_layout = out_deps := $(addprefix out/,$(src_files:src/%=%))
out/%.$(1): src/%$(2).upp$(1) remove_layout_from_out_deps = out_deps := $$(subst $(1).upp,.,$$(out_deps))
$(foreach layout,$(layouts),$(eval $(call remove_layout_from_out_deps,.$(layout))))
$(eval $(call remove_layout_from_out_deps,))
blogs := $(subst blogs/,,$(wildcard blogs/*))
#############
# FUNCTIONS #
#############
blog_post_target = src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
blog_post_src = blogs/$(blog)/%.$(BLOG_EXT_$(blog)) $$(wildcard bin/*)
blog_src = $(patsubst \
blogs/$(blog)/%.$(BLOG_EXT_$(blog)),\
src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml,\
$(shell find -L "blogs/$(blog)" -type f -and -name *.$(BLOG_EXT_$(blog))))
default_layout_file = $(if \
$(shell [ -f "share/layouts/default.layout.$(ext)" ] && echo 1),\
share/layouts/default.layout.$(ext))
layout_file = $(if $(layout),share/layouts/$(layout).layout.upp$(ext),$(default_layout_file))
define out_rule =
out/%.$(ext): src/%$(if $(layout),.$(layout)).upp$(ext) $(layout_file) $(component_files)
./bin/mkws https://joeac.net "$$$$(realpath $$<)" ./bin/mkws https://joeac.net "$$$$(realpath $$<)"
endef endef
$(foreach ext,$(sort $(out_files_ext)),\ define blog_rss_feed_rule =
$(eval $(call out_rule_for_ext_and_layout,$(ext),))) out/$(blog)/rss.xml: src/$(blog)/rss.uppxml $(blog_src) $(wildcard bin/*)
$(foreach layout,$(layouts),\ ./bin/mkws https://joeac.net "$$(realpath src/$(blog)/rss.uppxml)"
$(foreach ext,$(sort $(out_files_ext)),\ endef
$(eval $(call out_rule_for_ext_and_layout,$(ext),.$(layout)))))
define blog_rule =
.PHONY: $(blog)
$(blog): $(blog_src)
endef
define blog_post_rule =
$(blog_post_target): $(blog_post_src) $(let ext,html,$(layout_file) $(component_files))
./bin/blog \
-i $(blog) \
$(if $(BLOG_LAYOUT_$(blog)),-l $(BLOG_LAYOUT_$(blog))) \
$(BLOG_FLAGS_$(blog)) \
-f $$*.$(BLOG_EXT_$(blog))
endef
define prune_blog_rule =
.PHONY: prune_$(blog)
prune_$(blog):
./bin/prune_blog $(blog) *$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
endef
#########
# RULES #
#########
all: bin/pp $(blogs) public out
bin/pp:
cd pp && $(MAKE)
.PHONY: out .PHONY: out
out: $(out_files) out: $(out_deps)
.PHONY: microlog $(foreach ext,$(sort $(out_deps_ext)),\
microlog: $(patsubst blogs/microlog/%.gmi,src/microlog/%.microlog.upphtml,$(wildcard blogs/microlog/*.gmi)) $(eval $(out_rule)))
$(foreach layout,$(layouts),\
$(foreach ext,$(sort $(out_deps_ext)),\
$(eval $(out_rule))))
src/microlog/%.microlog.upphtml: blogs/microlog/%.gmi bin/blog bin/gmi2upphtml $(foreach blog,$(blogs),$(eval $(blog_rule)))
./bin/blog -i microlog -l microlog -t -p gmi2upphtml -f $*.gmi
.PHONY: blog $(foreach blog,$(blogs),\
blog: $(patsubst blogs/blog/%.gmi,src/blog/%.blog.upphtml,$(wildcard blogs/blog/*.gmi)) $(foreach layout,$(layouts),\
$(eval $(call blog_post_rule,$(blog),$(layout)))))
$(foreach blog,$(blogs),\
$(eval $(call blog_post_rule,$(blog),)))
src/blog/%.blog.upphtml: blogs/blog/%.gmi bin/blog bin/gmi2upphtml bin/cut_longlog_meta $(foreach blog,$(blogs),$(eval $(blog_rss_feed_rule)))
./bin/blog -i blog -l blog -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta -f $*.gmi
.PHONY: public .PHONY: public
public: $(subst public/,out/,$(shell find -L public/ -type f)) public: $(subst public/,out/,$(shell find -L public/ -type f))
@@ -99,6 +102,19 @@ public: $(subst public/,out/,$(shell find -L public/ -type f))
out/%: public/% out/%: public/%
mkdir -p $(dir $@) && cp $< $@ mkdir -p $(dir $@) && cp $< $@
.PHONY: prune
prune: $(addprefix prune_,$(blogs)) prune_out
.PHONY: prune_out
prune_out:
./bin/prune_out
$(foreach blog,$(blogs),$(eval $(prune_blog_rule)))
.PHONY: clean .PHONY: clean
clean: clean:
rm -rf out src/microlog src/blog rm -rf \
out \
$(foreach blog,$(blogs), \
$(foreach ext,upphtml upphtml.args,\
src/$(blog)/*$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).$(ext)))
+4 -5
View File
@@ -37,7 +37,7 @@ USAGE
extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\"" extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
file_pattern="*" file_pattern="*"
log_name="" log_name=""
layout="default" layout_name="default"
postprocess_cmd="echo" postprocess_cmd="echo"
use_filenames_for_titles=0 use_filenames_for_titles=0
@@ -99,11 +99,10 @@ do
dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml" dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
if [ $use_filenames_for_titles -eq 1 ] if [ $use_filenames_for_titles -eq 1 ]
then then
title="$name" export POST_TITLE="$name"
cat "$entry" | eval "POST_TITLE=\"${title}\" ${postprocess_cmd}" > "$dest" cat "$entry" | eval "${postprocess_cmd}" > "$dest"
else else
title="$(grep --regexp "^# " --max-count 1 "$entry" | cut -c3-)" export POST_TITLE="$(grep --regexp "^# " --max-count 1 "$entry" | cut -c3-)"
export POST_TITLE="$title"
cat "$entry" | grep --regexp "^# " --invert-match | eval "$postprocess_cmd" > "$dest" cat "$entry" | grep --regexp "^# " --invert-match | eval "$postprocess_cmd" > "$dest"
fi fi
cat "$entry" | eval "$extract_args_cmd" > "${dest}.args" cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
+8 -1
View File
@@ -53,7 +53,14 @@ close_all_open_blocks()
escape_upphtml() escape_upphtml()
{ {
echo "$1" | sed "s/\`/\\\\\`/g" | sed "s/\"/\\\\\"/g" | sed "s/\\\$/\\\\\$/g" echo "$1" \
| sed "s/&/\\&amp;/g" \
| sed "s/</\\&lt;/g" \
| sed "s/>/\\&gt;/g" \
| sed "s/\"/\\&quot;/g" \
| sed "s/'/\\&#x27;/g" \
| sed "s/\`/\\&#x60;/g" \
| sed "s/\\\$/\\\\\$/g"
} }
close_all_open_blocks_except() close_all_open_blocks_except()
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
/home/joeac/src/joeac.net/http/pp/pp
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh -e
BLOG="$1"
BLOG_POST_PATTERN="${2:-*}"
SRC="$(realpath src)"
BLOGS="$(realpath blogs)"
LAYOUTS="$(realpath share/layouts)"
BLOG_DIR_IN_SRC="${SRC}/${BLOG}"
BLOG_EXT=gmi
for post in $(find "$BLOG_DIR_IN_SRC" -type f -and -name "$BLOG_POST_PATTERN")
do
last_segment="${post##*\.}"
if [ "$last_segment" = "args" ]
then
continue
fi
post_rel_path="$(echo "$post" | cut -c${#BLOG_DIR_IN_SRC}- | cut -c3-)"
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-1}"
post_src="${BLOGS}/${BLOG}/${post_basename}.${BLOG_EXT}"
if [ -f "$post_src" ]
then
continue
fi
penultimate_segment="${post::${#post}-${#last_segment}-1}"
penultimate_segment="${penultimate_segment##*\.}"
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-${#penultimate_segment}-2}"
if [ -f "${LAYOUTS}/${penultimate_segment}.layout.${last_segment}" ]
then
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-${#penultimate_segment}-2}"
post_src="${BLOGS}/${BLOG}/${post_basename}.${BLOG_EXT}"
if [ -f "$post_src" ]
then
continue
fi
fi
echo "No source file found to match post ${post} in blog ${BLOG}: removing ${post}"
rm -f "$post" "${post}.args"
done
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh -e
for out in $(find out -type f)
do
out_basename="$(basename "$out")"
out_ext="${out_basename##*\.}"
rel_path="$(echo "${out}" | cut -c5-)"
if [ -f "public/${rel_path}" ]; then
continue
fi
base_rel_path="${rel_path%%.${out_ext}}"
src_path="src/${base_rel_path}.upp${out_ext}"
if [ -f "$src_path" ]; then
continue
fi
for layout in $(find share/layouts -name "*.layout.upp${out_ext}"); do
layout="$(basename "$layout")"
layout="${layout::${#layout}-15}"
rel_path="$(echo "${out}" | cut -c5-)"
base_rel_path="${rel_path%%.${out_ext}}"
src_path="src/${base_rel_path}.${layout}.upp${out_ext}"
if [ -f "$src_path" ]; then
continue 2
fi
done
echo "No source file found to match ${out}: removing it"
rm "$out"
done
+7
View File
@@ -0,0 +1,7 @@
BLOG_LAYOUT_blog := blog
BLOG_EXT_blog := gmi
BLOG_FLAGS_blog = -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta
BLOG_LAYOUT_microlog := microlog
BLOG_EXT_microlog := gmi
BLOG_FLAGS_microlog := -t -p gmi2upphtml
+1
View File
@@ -0,0 +1 @@
pp
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2021, Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
CC = cc
LD = ${CC}
PREFIX = /usr/local
MANPREFIX = ${PREFIX}/share/man
CFLAGS = -Wall -Wextra -pedantic
pp: pp.o
pp.o: pp.c
.c.o:
$(CC) $(CPPFLAGS) $< $(CFLAGS) -c -o $@
.o:
$(LD) $< $(LDFLAGS) -o $@
clean:
rm -rf pp *.o
install: pp pp.1
mkdir -p $(DESTDIR)$(PREFIX)/bin
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
cp pp $(DESTDIR)$(PREFIX)/bin/pp
cp pp.1 $(DESTDIR)$(MANPREFIX)/man1/
chmod 755 $(DESTDIR)$(PREFIX)/bin/pp
uninstall:
rm $(DESTDIR)$(PREFIX)/bin/pp
rm $(DESTDIR)$(MANPREFIX)/man1/pp.1
+188
View File
@@ -0,0 +1,188 @@
# `pp(1)`
a preprocessor
`pp(1)` allows embedding
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
code in files of any type by nesting it inside the `#!\n` token, where
`\n` is a new line. That means that if you'd like a simple loop or an
`if` inside an HTML file for instance,
you could use `pp(1)`.
## Basic usage
For the following code:
<!doctype html>
<title>pp(1) example</title>
<ul>
#!
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
#!
<li class=even>$i</li>
#!
else
#!
<li class=odd>$i</li>
#!
fi
i=$((i + 1))
done
#!
</ul>
`pp(1)` outputs:
<!doctype html>
<title>pp(1) example</title>
<ul>
<li class=odd>1</li>
<li class=even>2</li>
<li class=odd>3</li>
<li class=even>4</li>
<li class=odd>5</li>
<li class=even>6</li>
<li class=odd>7</li>
<li class=even>8</li>
<li class=odd>9</li>
<li class=even>10</li>
</ul>
## Arguments
You can also pass arguments to it by appending them to the command call:
<!doctype html>
<title>$1</title>
<p>
#!
echo $2
#!
</p>
Calling the above code with
`pp tmp.upphtml 'pp(1) example' 'hello, world'`
will result in:
<!doctype html>
<title>pp(1) example</title>
<p>
hello world
</p>
## Pipes
`pp(1)`'s `stdin` is sent to the child
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
<!doctype html>
<title>pp(1) example</title>
<p>
#!
cat
#!
</p>
`echo 'hello, world' | pp tmp.upphtml` will output:
<!doctype html>
<title>pp(1) example</title>
<p>
hello, world
</p>
## Debugging
`pp(1)` also takes an optional `-d` flag. If passed, `pp(1)` will dump
the generated
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
code instead of executing it:
<!doctype html>
<title>pp(1) example</title>
<ul>
#!
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
#!
<li class=even>$i</li>
#!
else
#!
<li class=odd>$i</li>
#!
fi
i=$((i + 1))
done
#!
</ul>
`pp -d tmp.upphtml` will output:
echo "<!doctype html>
<title>pp(1) example</title>
<ul>"
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
echo " <li class=even>$i</li>"
else
echo " <li class=odd>$i</li>"
fi
i=$((i + 1))
done
echo "</ul>"
## Example
<!doctype html>
<html lang=en>
<title>pp(1)</title>
<meta charset=UTF-8>
<meta name=viewport content='width=device-width'>
<link rel=stylesheet href=../theme.css>
<body>
<a href=../#myprojects>Home</a>
#!
addcls() {
sed "s/<code>/<code class=language-bash>/g"
}
smu README | addcls
#!
<h2>Files</h2>
<ul>
#!
for f in *
do
case $f in
*.upp*|*.html|*.tgz)
continue
;;
esac
#!
<li><a href=$f>$f</a></li>
#!
done
#!
</ul>
<h2>Download</h2>
<a href=pp.tgz>pp.tgz</a>
<script src=../prism.js data-manual></script>
<script src=../enhance.js></script>
</body>
</html>
## See also
* <https://github.com/radare/spp>
* <https://werc.cat-v.org/docs/rc-template-lang>
* <https://code.9front.org/hg/werc/file/92f7463dac1a/bin/template.awk>
+46
View File
@@ -0,0 +1,46 @@
.Dd Apr 25, 2019
.Dt pp 1
.Os
.Sh NAME
.Nm pp
.Nd preprocessor
.Sh SYNOPSIS
.Nm pp
.Op Fl d
.Ar file
.Op args ...
.Sh DESCRIPTION
The
.Nm
utility processes
.Ar file
to produce output that is used as input to another
program.
.Pp
It executes code enclosed in
.Dq #!\en
delimiters using
.Xr sh 1 .
.Op args ...
are passed to the embbeded script and are available
via the usual
.Em $1, $2, ...
positional parameters.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl d
.Nm
will dump the generated
.Xr sh 1
code instead of executing it. This is useful for debugging.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Preprocess file with arguments:
.Pp
.Dl $ pp file.upphtml a b
.Pp
.Sh SEE ALSO
.Xr sh 1
+369
View File
@@ -0,0 +1,369 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#if !__APPLE__
/*
* Appends src to string dst of size dsize (unlike strncat, dsize is the
* full size of dst, not space left). At most dsize-1 characters
* will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
* Returns strlen(src) + MIN(dsize, strlen(initial dst)).
* If retval >= dsize, truncation occurred.
*/
size_t
strlcat(char *dst, const char *src, size_t dsize)
{
const char *odst = dst;
const char *osrc = src;
size_t n = dsize;
size_t dlen;
/* Find the end of dst and adjust bytes left but don't go past end. */
while (n-- != 0 && *dst != '\0')
dst++;
dlen = dst - odst;
n = dsize - dlen;
if (n-- == 0)
return(dlen + strlen(src));
while (*src != '\0') {
if (n != 0) {
*dst++ = *src;
n--;
}
src++;
}
*dst = '\0';
return(dlen + (src - osrc)); /* count does not include NUL */
}
#endif /* !__APPLE__ */
#define MAXSIZE 64000
#define MAXINDENTSIZE 80
#define MAXARGSIZE 20
char *argv0;
int
efgetc(FILE *fp)
{
int c;
c = fgetc(fp);
if (c == EOF &&
ferror(fp) != 0) {
perror(argv0);
exit(1);
}
return c;
}
int
efseek(FILE *fp, long offset, int whence)
{
int s;
s = fseek(fp, offset, whence);
if (s == -1) {
perror(argv0);
exit(1);
}
return s;
}
int
eungetc(int c, FILE *fp)
{
int uc;
uc = ungetc(c, fp);
if (uc == EOF) {
perror(argv0);
exit(1);
}
return uc;
}
size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
size_t n;
n = strlcat(dst, src, dstsize);
if (n >= dstsize) {
fprintf(stderr, "%s: Buffer overflow\n", argv0);
exit(1);
}
return n;
}
int
eclose(int d)
{
int s;
s = close(d);
if (s == -1) {
perror(argv0);
exit(1);
}
return s;
}
char *
strappend(char *s, char c)
{
size_t len;
len = strlen(s);
s[len] = c;
s[len + 1] = '\0';
return s ;
}
int
shebang(FILE *fp)
{
int c;
c = efgetc(fp);
if (c != '#') {
eungetc(c, fp);
return 0;
}
c = efgetc(fp);
if (c != '!') {
eungetc(c, fp);
eungetc('#', fp);
return 0;
}
c = efgetc(fp);
if (c == '\n') {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
return 0;
}
for (;;) {
c = efgetc(fp);
if (c == '\n')
break;
}
return 1;
}
int
token(FILE *fp, int consume)
{
int c;
c = efgetc(fp);
if (c != '#') {
eungetc(c, fp);
return 0;
}
c = efgetc(fp);
if (c != '!') {
eungetc(c, fp);
eungetc('#', fp);
return 0;
};
c = efgetc(fp);
if (c != '\n') {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
return 0;
}
if (consume == 0) {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
}
return 1;
}
size_t
output(char *s)
{
return estrlcat(s, "echo \"", MAXSIZE);
}
size_t
outputend(char *s)
{
return estrlcat(s, "\"\n", MAXSIZE);
}
int main(int argc, char **argv)
{
char s[MAXSIZE] = "", indent[MAXINDENTSIZE] = "",
*eargv[MAXARGSIZE] = {"sh", "-c", s, "sh", NULL};
FILE *fp;
int size, c, i, fd[2], o = 0, status = 0, d = 0;
argv0 = argv[0];
if (argc == 1) {
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-d") == 0) {
if (argc == 2) {
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
return 1;
}
d = 1;
fp = fopen(argv[2], "r");
} else {
fp = fopen(argv[1], "r");
}
if (!fp) {
fprintf(stderr, "%s: %s: %s\n",
argv[0], d ? argv[2] : argv[1], strerror(errno));
return 1;
}
efseek (fp, 0, SEEK_END);
size = ftell(fp);
if(size == 0) return 0;
efseek(fp, -1, SEEK_END);
c = efgetc(fp);
if (c != '\n') {
fprintf(stderr, "%s: %s: No newline at end of file\n",
argv[0], d ? argv[2] : argv[1]);
exit(1);
}
efseek(fp, 0, SEEK_SET);
shebang(fp);
if (!token(fp, 1)) {
output(s);
o = 1;
}
for(;;) {
if (token(fp, 1)) {
if (!o) {
c = efgetc(fp);
if (c == EOF)
goto done;
eungetc(c, fp);
output(s);
} else
outputend(s);
o = !o;
}
c = efgetc(fp);
if (c == EOF)
break;
if (c == '\n') {
indent[0] = 0;
c = efgetc(fp);
if (c == EOF)
break;
for (;;) {
if (c != ' ' && c != '\t')
break;
strappend(indent, c);
c = efgetc(fp);
if (c == EOF)
break;
}
eungetc(c, fp);
if (token(fp, 0) && o)
continue;
strappend(s, '\n');
estrlcat(s, indent, MAXSIZE);
continue;
}
strappend(s, c);
}
if (o) {
outputend(s);
o = 0;
}
done:
if(d) {
puts(s);
return 0;
}
if (pipe(fd) == -1) {
perror(argv0);
return 1;
}
switch (fork()) {
case -1:
perror(argv0);
return 1;
case 0:
eclose(fd[1]);
eclose(fd[0]);
for (i = 2; i < argc; i++)
eargv[i+2] = argv[i];
if (execv("/bin/sh", eargv) == -1) {
perror(argv0);
return 1;
}
break;
default:
eclose(fd[1]);
eclose(fd[0]);
if (wait(&status) == -1) {
perror(argv0);
return 1;
}
}
return status;
}
BIN
View File
Binary file not shown.
+10 -3
View File
@@ -1,4 +1,11 @@
<!-- Primary Meta Tags --> #!
if [ -z "${NO_META}" ]; then
#!
<title>${TITLE}</title> <title>${TITLE}</title>
<meta name="title" content="${TITLE}" /> <meta name='title' content='${TITLE}' />
<meta name="description" content="${DESCRIPTION}" /> <meta name='description' content='${DESCRIPTION}' />
#!
fi
#!
+3 -1
View File
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/blog/$(basename "${PAGE}")"
#! #!
pp "${SHARE}"/components/head.upphtml pp "${SHARE}"/components/head.upphtml
#! #!
<link rel="stylesheet" href="/css/post.css" />
</head> </head>
<body> <body>
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
<article class='h-entry'> <article class='h-entry'>
<aside> <aside>
<span>This is a blog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span> <span>This is a <a href='/blog'>blog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
#! #!
if [ -z "${DATE_UPDATED}" ]; then if [ -z "${DATE_UPDATED}" ]; then
#! #!
@@ -10,7 +10,11 @@ pp "${SHARE}"/components/head.upphtml
<body> <body>
#! #!
pp "${SHARE}"/components/navbar.upphtml pp "${SHARE}"/components/navbar.upphtml
#!
<main>
#!
pp "$1" "$2" pp "$1" "$2"
#! #!
</main>
</body> </body>
</html> </html>
+3 -1
View File
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/microlog/$(basename "${SRC}")"
#! #!
pp "${SHARE}"/components/head.upphtml pp "${SHARE}"/components/head.upphtml
#! #!
<link rel="stylesheet" href="/css/post.css" />
</head> </head>
<body> <body>
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
<article class="h-entry"> <article class="h-entry">
<aside> <aside>
<span>This is a microlog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span> <span>This is a <a href='/microlog'>microlog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
<span>Published: <span>Published:
#! #!
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml" DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
@@ -0,0 +1,3 @@
#!
pp $@
#!
-46
View File
@@ -1,46 +0,0 @@
.Dd Apr 25, 2019
.Dt pp 1
.Os
.Sh NAME
.Nm pp
.Nd preprocessor
.Sh SYNOPSIS
.Nm pp
.Op Fl d
.Ar file
.Op args ...
.Sh DESCRIPTION
The
.Nm
utility processes
.Ar file
to produce output that is used as input to another
program.
.Pp
It executes code enclosed in
.Dq #!\en
delimiters using
.Xr sh 1 .
.Op args ...
are passed to the embbeded script and are available
via the usual
.Em $1, $2, ...
positional parameters.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl d
.Nm
will dump the generated
.Xr sh 1
code instead of executing it. This is useful for debugging.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Preprocess file with arguments:
.Pp
.Dl $ pp file.upphtml a b
.Pp
.Sh SEE ALSO
.Xr sh 1
+1
View File
@@ -0,0 +1 @@
/home/joeac/src/joeac.net/http/pp/pp.1
+2 -4
View File
@@ -4,7 +4,7 @@ SITE_URL="$2"
export TITLE="Joe Carstairs" export TITLE="Joe Carstairs"
export DESCRIPTION="Joe Carstairs' personal website" export DESCRIPTION="Joe Carstairs' personal website"
POSTS="$(ls "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")" POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
set -- set --
for post in $POSTS for post in $POSTS
do do
@@ -28,8 +28,6 @@ YEARS="$@"
pp "${SHARE}"/components/meta.upphtml pp "${SHARE}"/components/meta.upphtml
#! #!
<main>
<section class='h-feed'> <section class='h-feed'>
<h1>My blog</h1> <h1>My blog</h1>
@@ -40,7 +38,7 @@ pp "${SHARE}"/components/meta.upphtml
</aside> </aside>
<section> <section>
<p><a href='/blog/subscribe'>How to subscribe to this blog</a></p> <p><a href='/subscribe'>How to subscribe to my logs</a></p>
</section> </section>
#! #!
+50
View File
@@ -0,0 +1,50 @@
#!
SITE_URL="$1"
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.blog.upphtml$")"
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
#!
<rss version='2.0'>
<channel>
<title>Joe Carstairs blog</title>
<description>Short posts on random topics I find interesting</description>
<link>${SITE_URL}/blog</link>
<image>${SITE_URL}/images/headshot.webp</image>
<language>en-GB</language>
<pubDate>${NOW_RFC_822}</pubDate>
#!
for post in $POSTS; do
POST_ID="${post::${#post}-13}"
source "${SRC}/blog/${post}.args"
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="${POST_ID::10}" +"%a, %d %b %Y %H:%M:%S UT")"
#!
<item>
<title>${POST_TITLE}</title>
<link>${SITE_URL}/blog/${POST_ID}</link>
<guid isPermaLink='true'>${SITE_URL}/blog/${POST_ID}</guid>
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
<description>
#!
NO_META=1 pp "${SRC}/blog/$post" "${SITE_URL}" \
| sed "s/&/\\&amp;/g" \
| sed "s/</\\&lt;/g" \
| sed "s/>/\\&gt;/g" \
| sed "s/\"/\\&quot;/g" \
| sed "s/'/\\&#x27;/g" \
| sed "s/\`/\\&#x60;/g" \
| sed "s/\\\$/\\\\\$/g"
#!
</description>
<author>Joe Carstairs</author>
</item>
#!
done
#!
</channel>
</rss>
+4
View File
@@ -0,0 +1,4 @@
#!
SITE_URL="${1}"
#!
<meta http-equiv="refresh" content="0" ; url="${SITE_URL}/subscribe" />
+1 -3
View File
@@ -8,7 +8,6 @@ pp "${SHARE}"/components/meta.upphtml
pp "${SHARE}"/components/otp-dialog.upphtml pp "${SHARE}"/components/otp-dialog.upphtml
#! #!
<main>
<form class=contact-form> <form class=contact-form>
<h1>Contact me</h1> <h1>Contact me</h1>
<p hidden class=error></p> <p hidden class=error></p>
@@ -25,7 +24,7 @@ pp "${SHARE}"/components/otp-dialog.upphtml
<h1>You sent me a message!</h1> <h1>You sent me a message!</h1>
<p>Thanks for that. I may be in touch.</p> <p>Thanks for that. I may be in touch.</p>
<p>In case you forgot, your message was this:</p> <p>In case you forgot, your message was this:</p>
<hr> <hr />
<dl> <dl>
<dt>Name</dt> <dd class=sentname>???</dd> <dt>Name</dt> <dd class=sentname>???</dd>
<dt>Email</dt> <dd class=sentemail>???</dd> <dt>Email</dt> <dd class=sentemail>???</dd>
@@ -95,4 +94,3 @@ pp "${SHARE}"/components/otp-dialog.upphtml
await submitOtpForm(selectors); await submitOtpForm(selectors);
}); });
</script> </script>
</main>
+4 -5
View File
@@ -305,11 +305,6 @@ strong {
/** Blog feed */ /** Blog feed */
.h-feed ul {
list-style: none;
margin-inline: 0;
}
/** Block quotes */ /** Block quotes */
blockquote { blockquote {
padding-inline-start: var(--spacing-inline-lg); padding-inline-start: var(--spacing-inline-lg);
@@ -317,6 +312,10 @@ blockquote {
font-style: italic; font-style: italic;
} }
blockquote :first-child {
margin-block-start: 0;
}
blockquote footer { blockquote footer {
font-style: initial; font-style: initial;
} }
+9
View File
@@ -3,6 +3,15 @@
margin-block-start: var(--spacing-block-md); margin-block-start: var(--spacing-block-md);
} }
.h-feed ul:has(> .h-entry) {
list-style-type: none;
margin-inline: 0;
ul {
list-style-type: disc;
}
}
.h-feed .h-entry { .h-feed .h-entry {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
-2
View File
@@ -4,7 +4,6 @@ export DESCRIPTION="Error page for Joe Carstairs' personal website"
pp "${SHARE}"/components/meta.upphtml pp "${SHARE}"/components/meta.upphtml
#! #!
<main>
<section> <section>
<h1>Got lost?</h1> <h1>Got lost?</h1>
@@ -13,4 +12,3 @@ pp "${SHARE}"/components/meta.upphtml
my <a href='/'>homepage</a> instead. my <a href='/'>homepage</a> instead.
</p> </p>
</section> </section>
</main>
-2
View File
@@ -4,7 +4,6 @@ export DESCRIPTION="Joe Carstairs' personal website"
pp "${SHARE}"/components/meta.upphtml pp "${SHARE}"/components/meta.upphtml
#! #!
<main>
<section class="h-card"> <section class="h-card">
<div> <div>
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" /> <img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
@@ -46,4 +45,3 @@ pp "${SHARE}"/components/meta.upphtml
</p> </p>
</div> </div>
</section> </section>
</main>
+4
View File
@@ -0,0 +1,4 @@
#!
SITE_URL="${1}"
#!
<meta http-equiv="refresh" content="0" ; url="${SITE_URL}/microlog/rss.xml" />
+84
View File
@@ -0,0 +1,84 @@
#!
PAGE="$1"
SITE_URL="$2"
export TITLE="Joe Carstairs"
export DESCRIPTION="Joe Carstairs' personal website"
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
set --
for post in $POSTS
do
this_year="$( echo "$post" | cut -c1-4 )"
has_this_year=0
for year in $@
do
if [ "$this_year" = "$year" ]
then
has_this_year=1
break
fi
done
if [ $has_this_year -eq 0 ]
then
set -- $@ "$this_year"
fi
done
YEARS="$@"
pp "${SHARE}"/components/meta.upphtml
#!
<main>
<section class='h-feed'>
<h1>My microlog</h1>
<aside>
<p>This microlog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
<p hidden><a class='u-url' href='${SITE_URL}/microlog'>Permalink</a></p>
</aside>
<section>
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
</section>
#!
for year in $YEARS; do
#!
<h2>${year}</h2>
<ul>
#!
for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
title="$(grep "^export TITLE=" "${SRC}/microlog/${post}" | cut -c15-)"
title="${title::${#title}-16}"
date_published="$(echo "${post}" | cut -c1-10)"
href="${SITE_URL}/microlog/${post::${#post}-17}.html"
#!
<li class='h-entry'>
<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
<section class='e-content'>
#!
pp "${SRC}/microlog/${post}" "${SITE_URL}"
#!
</section>
#!
DATE="${date_published}" CLASS="dt-published" ATTRIBUTES="hidden" \
pp "${SHARE}/components/formatted_date.upphtml"
#!
</li>
#!
fi; done
#!
</ul>
#!
done
#!
</section>
+50
View File
@@ -0,0 +1,50 @@
#!
SITE_URL="$1"
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
#!
<rss version='2.0'>
<channel>
<title>Joe Carstairs microlog</title>
<description>My private X feed I guess?</description>
<link>${SITE_URL}/microlog</link>
<image>${SITE_URL}/images/headshot.webp</image>
<language>en-GB</language>
<pubDate>${NOW_RFC_822}</pubDate>
#!
for post in $POSTS; do
POST_ID="${post::${#post}-17}"
source "${SRC}/microlog/${post}.args"
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="${POST_ID::10}" +"%a, %d %b %Y %H:%M:%S UT")"
#!
<item>
<title>${POST_TITLE}</title>
<link>${SITE_URL}/microlog/${POST_ID}</link>
<guid isPermaLink='true'>${SITE_URL}/microlog/${POST_ID}</guid>
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
<description>
#!
NO_META=1 pp "${SRC}/microlog/$post" "${SITE_URL}" \
| sed "s/&/\\&amp;/g" \
| sed "s/</\\&lt;/g" \
| sed "s/>/\\&gt;/g" \
| sed "s/\"/\\&quot;/g" \
| sed "s/'/\\&#x27;/g" \
| sed "s/\`/\\&#x60;/g" \
| sed "s/\\\$/\\\\\$/g"
#!
</description>
<author>Joe Carstairs</author>
</item>
#!
done
#!
</channel>
</rss>
+91
View File
@@ -0,0 +1,91 @@
#!
PAGE="$1"
SITE_URL="$2"
export TITLE="How to subscribe to joeac's blog"
export DESCRIPTION="How to subscribe to joeac's blog"
#!
<section>
<h1>How to subscribe to my logs</h1>
<p>
The short version: use my <a href='/blog/rss.xml'>blog RSS feed</a> and
<a href='/microlog/rss.xml'>microlog RSS feed</a>, or my
<a href='gemini://joeac.net/logs/loglog.gmi'>gemlog</a>.
</p>
<p>
If neither of those things means anything to you, no worries. You could just
look at my website every once in a while, but that gets old. A better way is to
use my RSS file. Here's how.
</p>
<p>
An RSS file, if you don't know, is a file containing a list of posts in a format
that's so stupid even computers can understand it. Mine is at
<a href='https://joeac.net/rss.xml'>https://joeac.net/rss.xml</a>,
and I've also got one for just my blog posts at
<a href='https://joeac.net/blog/rss.xml'>https://joeac.net/blog/rss.xml</a>,
and one just for microlog posts at
<a href='https://joeac.net/microlog/rss.xml'>https://joeac.net/microlog/rss.xml</a>.
Since computers can understand it, you can get software to read it automatically
and tell you when I've made a new blog post. That's much less hassle than having
to check manually yourself!
</p>
<p>
I'm not the only person serving RSS files, of course. You might not have heard
of them, but they're actually all over the Web. In the early days of the Web,
everyone was using them, so they've ended up built in to a whole load of
foundational Web technology, and hence ubiquitous, even if most Web users have
forgotten they exist. Once you've subscribed to my logs, you can subscribe to
basically any blog, podcast, or YouTube channel, and a lot of other Websites for
news, organisations, whatever. Point a well-designed RSS aggregator or RSS
reader at most Websites, and it'll automatically find an associated RSS feed.
</p>
<p>
If you're not persuaded, try reading
<a href='https://doctorow.medium.com/you-should-be-using-an-rss-reader-76aed31151f9'>
Cory Doctorow explaining why RSS is ridiculously good
</a>. He's terrifyingly persuasive. Also, read his novels!
</p>
<p>
If you just want to give it a go quickly, probably the quickest way is to
use someone else's RSS aggregator that they'll let you use as a service.
<a href='https://feedly.com/news-reader'>Feedly</a> and
<a href='https://www.inoreader.com'>Inoreader</a> have free tiers.
</p>
<p>
If you want to have a little more control, you can self-host an RSS
reader, like <a href='https://tt-rss.org'>Tiny Tiny RSS</a>,
<a href='https://miniflux.app'>Miniflux</a>, or
<a href='https://freshrss.org'>FreshRSS</a>. But why would you do that?
Self-hosting is complicated! Why not just use a normal, local app? Here are
some options for that you might want to investigate. All these are active
free and open-source software projects at the time of writing.
</p>
<ul>
<li><a href='https://hyliu.me/fluent-reader'>Fluent Reader</a> (Linux, Windows, Mac, also mobile I think?)</li>
<li><a href='https://www.rssowl.org'>RSSOwl</a> (Linux, Windows, Mac)</li>
<li><a href='https://capyreader.com'>Capy Reader</a> (Android)</li>
<li><a href='https://apps.kde.org/en-gb/akregator'>Akregator</a> (Linux, Windows (unstable))</li>
<li><a href='https://gfeeds.gabmus.org'>GFeeds</a> (Linux)</li>
<li><a href='https://lzone.de/liferea'>Liferea</a> (Linux)</li>
</ul>
<p>
For the terminally geeky, there is also
<a href='https://newsboat.org'>Newsboat</a>. I used it for ages, it works
fabulously, but it's all in the terminal, so you have to be OK with that.
</p>
<p>
You can also just enter 'RSS feed reader' into your favourite search
engine, software distribution, or app store and see what comes up. Try
adding 'FOSS' into the query to prioritise free, open-source software.
</p>
</section>
-30
View File
@@ -1,30 +0,0 @@
FROM node:lts-alpine3.22
WORKDIR /app
RUN apk add --no-cache git
COPY website/package.json website/package-lock.json ./
RUN npm install && apk del git
COPY website/astro.config.mjs website/redirects.mjs ./
COPY website/db ./db/
ARG DB_URL=file:/app/db.sqlite
ENV ASTRO_DB_REMOTE_URL=$DB_URL
RUN mkdir -p "$(dirname "$(echo "$ASTRO_DB_REMOTE_URL" | cut -d':' -f 2)")"
RUN npm run astro db push
COPY website .
RUN mkdir -p /common
COPY common /common
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
ARG LOCAL_SMTP_HOST
ARG LOCAL_SMTP_PASSWORD
ARG LOCAL_SMTP_PORT
RUN npm run build && rm -rf public src
CMD ["node", "./dist/server/entry.mjs"]
-6
View File
@@ -1,6 +0,0 @@
.astro/
dist/
node_modules/
.dockerignore
*.sqlite
Dockerfile
-41
View File
@@ -1,41 +0,0 @@
import { defineConfig, envField, passthroughImageService } from "astro/config";
import db from "@astrojs/db";
import gemtext from "astro-gemtext";
import mdx from "@astrojs/mdx";
import node from "@astrojs/node";
import sitemap from "@astrojs/sitemap";
import redirects from "./redirects.mjs";
// https://astro.build/config
export default defineConfig({
adapter: node({
mode: "standalone",
}),
env: {
schema: {
MAX_DAILY_EMAILS: envField.number({
context: "server",
access: "secret",
}),
LOCAL_SMTP_ENVELOPE_FROM: envField.string({
context: "server",
access: "secret",
}),
LOCAL_SMTP_HOST: envField.string({ context: "server", access: "secret" }),
LOCAL_SMTP_PORT: envField.number({ context: "server", access: "secret" }),
LOCAL_SMTP_USER: envField.string({ context: "server", access: "secret" }),
LOCAL_SMTP_PASSWORD: envField.string({
context: "server",
access: "secret",
}),
CONTACT_MAILBOX: envField.string({ context: "server", access: "secret" }),
},
},
image: {
service: passthroughImageService(),
},
redirects,
site: "https://joeac.net",
integrations: [db(), gemtext(), mdx(), sitemap()],
});
-34
View File
@@ -1,34 +0,0 @@
import { column, defineDb, defineTable } from "astro:db";
const Otp = defineTable({
columns: {
userId: column.text(),
value: column.text(),
createdAt: column.number(),
validUntil: column.number(),
},
});
const SendmailToken = defineTable({
columns: {
userId: column.text(),
value: column.text(),
createdAt: column.number(),
validUntil: column.number(),
},
});
const SentEmails = defineTable({
columns: {
messageId: column.text(),
sentAt: column.number(),
},
});
export default defineDb({
tables: {
Otp,
SendmailToken,
SentEmails,
},
});
-3
View File
@@ -1,3 +0,0 @@
import { db } from "astro:db";
export default async function seed() {}
-8737
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
{
"private": "true",
"scripts": {
"dev": "astro dev",
"start": "astro build --remote && node ./dist/server/entry.mjs",
"build": "astro build --remote",
"preview": "astro preview",
"sync": "astro sync",
"astro": "astro"
},
"engines": {
"node": "^22.14.0"
},
"dependencies": {
"@astrojs/db": "^0.20.1",
"@astrojs/mdx": "^5.0.2",
"@astrojs/node": "^10.0.3",
"@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.7.1",
"@types/nodemailer": "^7.0.4",
"astro": "^6.0.8",
"astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git#ab32140ae28ff451447e7b25e24de0a4d46d264d",
"gemtext": "^0.2.2",
"markdown-it": "^14.1.0",
"nodemailer": "^7.0.11",
"typescript": "^5.4.3"
},
"devDependencies": {
"@astrojs/check": "^0.9.8",
"@types/markdown-it": "^14.1.1"
}
}
-516
View File
@@ -1,516 +0,0 @@
/** Variables */
:root {
--colour-primary-10: #060300;
--colour-primary-15: #150800;
--colour-primary-20: #1f1400;
--colour-primary-30: #3c2b00;
--colour-primary-40: #5c4300;
--colour-primary-50: #7f5d00;
--colour-primary-60: #a37800;
--colour-primary-70: #c89500;
--colour-primary-80: #efb300;
--colour-primary-90: #ffd98c;
--colour-primary-95: #ffecc8;
--colour-hyperlink-10: #000409;
--colour-hyperlink-20: #001829;
--colour-hyperlink-30: #00314d;
--colour-hyperlink-40: #004d75;
--colour-hyperlink-50: #006a9f;
--colour-hyperlink-60: #1388c9;
--colour-hyperlink-70: #41a8ea;
--colour-hyperlink-80: #78c7ff;
--colour-hyperlink-90: #bfe3ff;
--colour-hyperlink-95: #e0f1ff;
--colour-grey-10: oklch(0.1 0.01 84);
--colour-grey-20: oklch(0.2 0.01 84);
--colour-grey-30: oklch(0.3 0.01 84);
--colour-grey-40: oklch(0.4 0.01 84);
--colour-grey-50: oklch(0.5 0.01 84);
--colour-grey-60: oklch(0.6 0.01 84);
--colour-grey-70: oklch(0.7 0.01 84);
--colour-grey-80: oklch(0.8 0.01 84);
--colour-grey-90: oklch(0.9 0.01 84);
--colour-grey-95: oklch(0.95 0.01 84);
--colour-error-10: oklch(0.1 0.2 26);
--colour-error-20: oklch(0.2 0.2 26);
--colour-error-30: oklch(0.3 0.2 26);
--colour-error-40: oklch(0.4 0.2 26);
--colour-error-50: oklch(0.5 0.2 26);
--colour-error-60: oklch(0.6 0.2 26);
--colour-error-70: oklch(0.7 0.2 26);
--colour-error-80: oklch(0.8 0.2 26);
--colour-error-90: oklch(0.9 0.2 26);
--colour-error-95: oklch(0.95 0.2 26);
--colour-warn-10: oklch(0.1 0.2 46);
--colour-warn-20: oklch(0.2 0.2 46);
--colour-warn-30: oklch(0.3 0.2 46);
--colour-warn-40: oklch(0.4 0.2 46);
--colour-warn-50: oklch(0.5 0.2 46);
--colour-warn-60: oklch(0.6 0.2 46);
--colour-warn-70: oklch(0.7 0.2 46);
--colour-warn-80: oklch(0.8 0.2 46);
--colour-warn-90: oklch(0.9 0.2 46);
--colour-warn-95: oklch(0.95 0.2 46);
--colour-primary-fg: var(--colour-primary-90);
--colour-primary-fg-accent: var(--colour-primary-80);
--colour-primary-bg: var(--colour-primary-10);
--colour-primary-bg-accent: var(--colour-primary-20);
--colour-code-fg: var(--colour-primary-90);
--colour-code-bg: var(--colour-primary-15);
--colour-hyperlink: var(--colour-hyperlink-80);
--colour-grey-fg: var(--colour-grey-70);
--colour-grey-bg: var(--colour-grey-30);
--colour-error-fg: var(--colour-error-90);
--colour-error-bg: var(--colour-error-40);
--colour-warn-fg: var(--colour-warn-20);
--colour-warn-bg: var(--colour-warn-80);
--font-size-sm: 1rem;
--font-size-base: 1.125rem;
--font-size-md: 1.5rem;
--font-size-lg: 2rem;
--font-size-xl: 3rem;
--spacing-block-xs: 0.5rem;
--spacing-block-sm: 1.75rem;
--spacing-block-md: 2.5rem;
--spacing-block-lg: 3.5rem;
--spacing-block-xl: 5rem;
--spacing-inline-xs: 0.25rem;
--spacing-inline-sm: 0.5rem;
--spacing-inline-md: 1.5rem;
--spacing-inline-lg: 3rem;
--spacing-inline-xl: 6rem;
}
/** Light theme */
@media (prefers-color-scheme: light) {
:root {
--colour-primary-fg: var(--colour-primary-20);
--colour-primary-fg-accent: var(--colour-primary-40);
--colour-primary-bg: var(--colour-primary-95);
--colour-hyperlink: var(--colour-hyperlink-40);
--colour-grey-fg: var(--colour-grey-40);
--colour-grey-bg: var(--colour-grey-80);
--colour-error-fg: var(--colour-error-20);
--colour-error-bg: var(--colour-error-80);
}
}
/** Base typography */
body {
font-size: var(--font-size-base);
color: var(--colour-primary-fg);
font-weight: light;
background-color: var(--colour-primary-bg);
line-height: 1.5;
/* Geometric Humanist stack from https://modernfontstacks.com */
font-family:
Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif;
}
small {
font-size: var(--font-size-sm);
}
:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol, blockquote) {
margin-block-start: var(--spacing-block-sm);
}
hr {
height: 2px;
}
/** Base layout */
body {
--body-margin-inline-start: var(--spacing-inline-lg);
--body-margin-inline-end: var(--body-margin-inline-start);
--body-margin-block-end: var(--spacing-block-xl);
margin-inline: var(--body-margin-inline-start) var(--body-margin-inline-end);
margin-block-end: var(--body-margin-block-end);
}
:is(h1, h2, h3, h4, h5, h6) {
text-align: center;
}
img {
margin-inline: auto;
height: auto;
}
@media (min-width: 60rem) {
body {
display: grid;
grid-template-columns:
[grid-start media-start]
var(--grid-margin-inline)
[media-end content-start]
minmax(var(--grid-max-content-width), auto)
[content-end grid-end];
grid-auto-rows: max-content;
column-gap: var(--spacing-block-sm);
max-width: var(--grid-total-width);
--body-margin-inline-end: 6rem;
--grid-margin-inline: 6rem;
--grid-total-width: 48rem;
--grid-max-content-width: calc(
var(--grid-total-width) - var(--body-margin-inline-start) -
var(--grid-margin-inline) - var(--spacing-block-sm) -
var(--grid-margin-inline)
);
}
:is(main, article, nav) {
display: grid;
grid-column: grid;
grid-template-columns: subgrid;
> :is(section, header, aside, form) {
display: grid;
grid-template-columns: subgrid;
grid-column: grid;
> :not(.not-grid-content) {
grid-column: content;
}
}
}
:is(h1, h2, h3, h4, h5, h6) {
text-align: start;
}
}
@media (min-width: 80rem) {
body {
grid-template-columns:
[grid-start media-start]
var(--grid-margin-inline)
[media-end content-start]
minmax(auto, var(--grid-max-content-width))
[content-end margin-start]
auto
[margin-end grid-end];
--grid-total-width: 80rem;
--grid-max-content-width: 40rem;
}
}
/** Headings */
h1 {
font-size: var(--font-size-xl);
font-weight: 900;
margin-block-start: 0;
}
h2 {
font-size: var(--font-size-lg);
font-weight: 900;
margin-block-start: var(--spacing-block-xl);
}
h3,
h4,
h5,
h6 {
font-size: var(--font-size-md);
font-weight: 600;
margin-block-start: var(--spacing-block-lg);
}
/** Hyperlinks */
a:is(:link, :visited) {
color: var(--colour-hyperlink);
text-decoration: underline;
display: inline;
}
a:hover {
text-decoration: wavy;
}
/** Definition lists */
dl {
margin-block-start: var(--spacing-block-md);
}
dl dt {
font-weight: bold;
}
dl dd + dt {
margin-block-start: var(--spacing-block-md);
}
/** figcaptions */
figcaption {
font-size: var(--font-size-sm);
font-style: italic;
text-align: center;
}
/** Lists */
:is(ol, ul) {
margin-inline-start: var(--spacing-inline-lg);
}
/** Navigation bar */
nav {
margin-block: var(--spacing-block-sm);
}
nav ul {
grid-column: media-start / content-end;
display: flex;
justify-content: center;
gap: var(--spacing-inline-md);
list-style: none;
margin: 0;
}
@media (min-width: 36rem) {
nav {
display: grid;
grid-template-columns: subgrid;
}
nav ul {
justify-content: end;
}
}
/** Emphasis */
strong {
font-weight: bold;
font-style: italic;
}
/** Blog feed */
.h-feed ul {
list-style: none;
margin-inline: 0;
}
/** Block quotes */
blockquote {
padding-inline-start: var(--spacing-inline-lg);
border-inline-start: 2px solid var(--colour-primary-fg);
font-style: italic;
}
blockquote footer {
font-style: initial;
}
blockquote :is(b, strong) {
font-style: italic;
font-weight: bold;
}
blockquote :is(i, em) {
font-style: normal;
}
/** Small caps */
.small-caps {
font-variant: small-caps;
}
/** Pre-formatted blocks */
pre {
border: 2px solid var(--colour-primary-fg);
border-radius: 2px;
background-color: var(--colour-code-bg) !important;
margin-block-start: var(--spacing-block-sm);
padding-inline: var(--spacing-inline-sm);
padding-block: var(--spacing-block-xs);
}
/** Code blocks */
code {
border: 2px solid var(--colour-primary-fg);
border-radius: 2px;
padding-inline: var(--spacing-inline-xs);
color: var(--colour-code-fg);
background-color: var(--colour-code-bg);
font-size: var(--font-size-sm);
}
pre code {
border: none;
border-radius: none;
padding: none;
}
/* verse */
.verse {
span {
display: block;
}
}
.verse--hanging-indents {
span + span:not(.not-hanging) {
margin-inline-start: var(--spacing-inline-md);
}
}
/* block-comment */
block-comment {
display: grid;
grid-template-columns: subgrid;
grid-column: grid;
> * {
grid-column: content;
}
> blockquote:last-of-type {
padding-block: var(--spacing-block-sm);
padding-inline: var(--spacing-inline-md);
overflow-y: scroll;
border: 2px solid var(--colour-primary-fg);
--colour-scroll-shadow: color-mix(
in srgb,
var(--colour-primary-fg),
transparent 20%
);
background:
linear-gradient(var(--colour-primary-bg) 30%, transparent) center top,
linear-gradient(transparent, var(--colour-primary-bg) 70%) center bottom,
radial-gradient(
farthest-side at 50% 0,
var(--colour-scroll-shadow),
transparent
)
center top,
radial-gradient(
farthest-side at 50% 100%,
var(--colour-scroll-shadow),
transparent
)
center bottom;
background-repeat: no-repeat;
background-size:
100% 2rem,
100% 2rem,
100% 1rem,
100% 1rem;
background-attachment: local, local, scroll, scroll;
> :first-child {
margin-block-start: 0;
}
}
}
@media (min-width: 80rem) {
block-comment > blockquote:last-of-type {
grid-column: margin;
max-height: 67vh;
}
}
/* forms */
form {
margin-inline: auto;
max-width: max-content;
:is(button, fieldset, input, label, object, output, select, textarea, img) {
display: block;
}
label {
margin-block-end: var(--spacing-block-xs);
}
* + :is(label, input[type="submit"], button) {
margin-block-start: var(--spacing-block-md);
}
:is(input[type="submit"], input[type="button"], button) {
padding-inline: var(--spacing-inline-sm);
max-width: max-content;
&:not(dialog *) {
margin-inline-start: auto;
}
}
:is(input[type="email"], input[type="text"], select) {
max-width: 100%;
width: 16rem;
}
textarea {
max-width: 100%;
width: 40rem;
}
}
/* dialogs */
dialog {
background: var(--colour-primary-bg-accent);
border: 2px solid var(--colour-primary-fg-accent);
bottom: auto;
color: var(--colour-primary-fg);
left: calc(0.5 * (100vw - min(90vw, 36rem)));
margin: 0;
padding-block: var(--spacing-block-sm);
padding-inline: var(--spacing-inline-sm);
text-align: center;
top: auto;
width: min(90vw, 36rem);
}
@media (min-width: 80rem) {
dialog {
left: calc(
var(--body-margin-inline-start) + var(--grid-margin-inline) + 2 *
var(--spacing-inline-md)
);
}
}
/* utilities */
:is(
.para-spacing-tight:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol),
.para-spacing-tight :is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol)
) {
margin-block-start: var(--spacing-block-xs);
}
.error {
background: var(--colour-error-bg);
border: 2px solid var(--colour-error-fg);
color: var(--colour-error-fg);
padding-block: var(--spacing--block-sm);
padding-inline: var(--spacing-inline-sm);
}
.warn {
background: var(--colour-warn-bg);
border: 2px solid var(--colour-warn-fg);
color: var(--colour-warn-fg);
padding-block: var(--spacing--block-sm);
padding-inline: var(--spacing-inline-sm);
--colour-hyperlink: var(--colour-hyperlink-40);
}
-86
View File
@@ -1,86 +0,0 @@
div:has(img) {
width: 6rem;
height: 6rem;
margin-inline: auto;
}
div img {
width: 6rem;
height: 6rem;
border-radius: 1rem;
filter: contrast(1.25);
}
div:has(img)::after {
/* Colour overlay */
background-color: var(--colour-primary-80);
opacity: 0.3;
/* Same size and shape as the img */
border-radius: 1rem;
width: 6rem;
height: 6rem;
/* Positioned on top of the img */
display: block;
position: relative;
top: -6rem;
/* A content value is needed to get the ::after to render */
content: '';
}
header {
display: grid;
grid-template-columns: 1fr 1fr;
text-align: center;
border-block-end: 1px solid var(--colour-primary-fg);
}
header h1 {
grid-column: 1 / 3;
text-align: center;
}
.technical-skills h3 {
text-align: start;
}
.technical-skills ul {
color: var(--colour-primary-fg-accent);
margin-inline-start: var(--spacing-inline-md);
}
@media (min-width: 46rem) {
.technical-skills section {
display: flex;
gap: var(--spacing-inline-sm);
align-items: baseline;
}
.technical-skills section h3::after {
content: '/';
margin-inline: var(--spacing-inline-sm);
}
.technical-skills section ul {
display: flex;
gap: var(--spacing-inline-sm);
list-style: none;
margin-inline-start: 0;
}
.technical-skills section ul li + li::before {
content: '•';
margin-inline-end: var(--spacing-inline-sm);
}
}
:is(.experience, .passions) :is(ol, ul) {
list-style: none;
margin-inline: 0;
}
:is(.experience, .passions) :is(ol, ul) li {
margin-block-start: var(--spacing-block-sm);
}
-36
View File
@@ -1,36 +0,0 @@
/* Assumes there is at most one level of subheading for sub-dividing entries */
.h-feed :is(h2, h3, h4, h5, h6) {
margin-block-start: var(--spacing-block-md);
}
.h-feed .h-entry {
display: flex;
flex-direction: column;
}
.h-feed .h-entry + .h-entry {
margin-block-start: var(--spacing-block-md);
}
.h-feed .h-entry > * {
order: 1;
margin: 0;
}
.h-feed .h-entry .dt-published {
order: 0;
font-size: var(--font-size-sm);
}
.h-feed .h-entry .p-name {
font-size: var(--font-size-md);
font-weight: 400;
}
.h-feed .full-feed-link {
text-align: end;
}
.h-feed :is(a.full-feed-link, .full-feed-link a)::after {
content: " >";
}
-57
View File
@@ -1,57 +0,0 @@
.h-card div:has(img) {
width: 6rem;
height: 6rem;
margin-inline: auto;
}
.h-card img {
width: 6rem;
height: 6rem;
border-radius: 1rem;
filter: contrast(1.25);
}
.h-card div:has(img)::after {
/* Colour overlay */
background-color: var(--colour-primary-80);
opacity: 0.3;
/* Same size and shape as the img */
border-radius: 1rem;
width: 6rem;
height: 6rem;
/* Positioned on top of the img */
display: block;
position: relative;
top: -6rem;
/* A content value is needed to get the ::after to render */
content: "";
}
@media (min-width: 36rem) {
.h-card {
grid-column: media-start / content-end;
display: grid;
grid-template-columns: subgrid; /** Subgrid of main column layout */
grid-template-rows: min-content 1fr;
grid-template-areas:
"empty heading"
"photo text";
column-gap: var(--spacing-block-sm);
}
.h-card div:has(img) {
grid-area: photo;
margin-block-start: var(--spacing-block-sm);
}
.h-card header {
grid-area: heading;
}
.h-card__text {
grid-area: text;
}
}
-29
View File
@@ -1,29 +0,0 @@
.otp-inputs {
display: flex;
flex-direction: row;
gap: var(--spacing-inline-sm);
justify-content: center;
input {
border: none;
border-block-end: 2px solid var(--colour-grey-fg);
font-size: var(--font-size-lg);
text-align: center;
width: var(--font-size-lg);
}
}
.otp-form {
align-items: center;
display: flex;
flex-direction: column;
gap: var(--spacing-block-sm);
> * {
margin: 0;
}
input[type="submit"] {
max-width: max-content;
}
}
-8
View File
@@ -1,8 +0,0 @@
.p-summary {
font-style: italic;
font-size: var(--font-size-sm);
}
h1 {
margin-block-start: var(--spacing-block-md);
}
-93
View File
@@ -1,93 +0,0 @@
/* Based on Andy Bells More Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset/ */
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Prevent font size inflation */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
/* Remove default margin in favour of better control in authored CSS */
body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
margin-block: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role="list"],
ol[role="list"] {
list-style: none;
}
/* Set core body defaults */
body {
min-height: 100vh;
}
/* Set shorter line heights on headings and interactive elements */
h1,
h2,
h3,
h4,
button,
input,
label {
line-height: 1.1;
}
/* Balance text wrapping on headings */
h1,
h2,
h3,
h4 {
text-wrap: balance;
}
/* Make images easier to work with */
img,
picture {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Make sure textareas without a rows attribute are not tiny */
textarea:not([rows]) {
min-height: 10em;
}
/* Anything that has been anchored to should have extra scroll margin */
:target {
scroll-margin-block: 5ex;
}
* {
margin: 0;
padding: 0;
}
[hidden] {
display: none;
}
-1
View File
@@ -1 +0,0 @@
../../common/images/
-36
View File
@@ -1,36 +0,0 @@
export default {
"/links": "/microlog",
"/links/rss.xml": "/microlog/rss.xml",
"/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",
};
-7
View File
@@ -1,7 +0,0 @@
import otp from "./otp/otp";
import sendmail from "./sendmail";
export const server = {
otp,
sendmail,
};
-7
View File
@@ -1,7 +0,0 @@
import send from "./send-otp";
import verify from "./verify-otp";
export default {
send,
verify,
};
-63
View File
@@ -1,63 +0,0 @@
import crypto from "node:crypto";
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import { db, gte, Otp, SentEmails } from "astro:db";
import { transporter } from "../sendmail";
import { LOCAL_SMTP_ENVELOPE_FROM, MAX_DAILY_EMAILS } from "astro:env/server";
export default defineAction({
input: z.object({
email: z.string().email(),
name: z.string().optional(),
type: z.enum(["email"]),
}),
handler: sendOtp,
});
type OtpParams = {
email: string;
name?: string;
type: "email";
};
async function sendOtp({ email, name }: OtpParams) {
const otp = crypto.randomBytes(3).toString("hex").toLocaleUpperCase();
const otpPretty = `${otp.slice(0, 3)}-${otp.slice(3)}`;
const emailsSentLast24Hours = await db.$count(
SentEmails,
gte(SentEmails.sentAt, Date.now() - 1000 * 60 * 60 * 24),
);
if (emailsSentLast24Hours >= MAX_DAILY_EMAILS) {
console.warn(
`${name} <${email}> requested an OTP, but ${emailsSentLast24Hours} have already been sent, whereas the max daily load is ${MAX_DAILY_EMAILS}.`,
);
throw new Error(
`${emailsSentLast24Hours} emails have been sent in the last 24 hours, but the max daily load is ${MAX_DAILY_EMAILS}.`,
);
}
const info = await transporter.sendMail({
from: LOCAL_SMTP_ENVELOPE_FROM,
to: `${name ? `"${name}" ` : ""}<${email}>`,
subject: `joeac.net: your OTP is ${otpPretty}`,
text: `
Someone tried to use this email address on joeac.net. If this was you,
your one-time passcode is ${otpPretty}. If this wasn't you, you don't need
to do anything.`,
});
console.log(
`Sent OTP (${otpPretty}) to ${email}. Message ID: ${info.messageId}`,
);
await db
.insert(SentEmails)
.values({ messageId: info.messageId, sentAt: Date.now() });
await db.insert(Otp).values({
userId: email,
value: otp,
createdAt: Date.now(),
validUntil: Date.now() + 1000 * 60 * 5,
});
}
-48
View File
@@ -1,48 +0,0 @@
import { randomBytes } from "node:crypto";
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import { and, db, eq, gte, Otp, SendmailToken } from "astro:db";
export default defineAction({
input: z.object({
guess: z.string().length(6),
lenient: z.boolean().default(false),
userId: z.string().nonempty(),
}),
handler: verifyOtp,
});
async function verifyOtp({ guess, lenient, userId }: VerifyOtpParams) {
const leniency = lenient ? 1000 * 60 : 0;
const isOtpCorrect =
(await db.$count(
Otp,
and(
eq(Otp.userId, userId),
eq(Otp.value, guess),
gte(Otp.validUntil, Date.now() - leniency),
),
)) > 0;
if (!isOtpCorrect) {
return false;
}
await db.delete(Otp).where(and(eq(Otp.userId, userId), eq(Otp.value, guess)));
const token = randomBytes(256).toString("hex");
await db.insert(SendmailToken).values({
userId,
value: token,
createdAt: Date.now(),
validUntil: Date.now() + 60_000,
});
return token;
}
type VerifyOtpParams = {
guess: string;
lenient: boolean;
userId: string;
};
-97
View File
@@ -1,97 +0,0 @@
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import nodemailer from "nodemailer";
import {
MAX_DAILY_EMAILS,
LOCAL_SMTP_HOST,
LOCAL_SMTP_PASSWORD,
LOCAL_SMTP_PORT,
LOCAL_SMTP_USER,
CONTACT_MAILBOX,
LOCAL_SMTP_ENVELOPE_FROM,
} from "astro:env/server";
import { and, db, eq, gte, SendmailToken, SentEmails } from "astro:db";
export default defineAction({
input: z.object({
email: z.string().email(),
message: z.string().nonempty(),
name: z.string().nonempty(),
userId: z.string().nonempty(),
token: z.string().nonempty(),
}),
handler: sendmail,
});
type SendEmailParams = {
email: string;
message: string;
name: string;
token: string;
userId: string;
};
async function sendmail({
name,
email,
message,
token,
userId,
}: SendEmailParams) {
const isTokenCorrect =
(await db.$count(
SendmailToken,
and(
eq(SendmailToken.userId, userId),
eq(SendmailToken.value, token),
gte(SendmailToken.validUntil, Date.now()),
),
)) > 0;
if (!isTokenCorrect) {
return false;
}
await db
.delete(SendmailToken)
.where(
and(eq(SendmailToken.userId, userId), eq(SendmailToken.value, token)),
);
const emailsSentLast24Hours = await db.$count(
SentEmails,
gte(SentEmails.sentAt, Date.now() - 1000 * 60 * 60 * 24),
);
if (emailsSentLast24Hours > MAX_DAILY_EMAILS) {
console.warn(
`${name} <${email}> tried to send an email, but ${emailsSentLast24Hours} have already been sent, whereas the max daily load is ${MAX_DAILY_EMAILS}.`,
);
throw new Error(
`${emailsSentLast24Hours} emails have been sent in the last 24 hours, but the max daily load is ${MAX_DAILY_EMAILS}.`,
);
}
const info = await transporter.sendMail({
from: LOCAL_SMTP_ENVELOPE_FROM,
to: CONTACT_MAILBOX,
subject: `joeac.net: ${name} left a message`,
text: `${name} <${email}> sent you a message:\n\n\n${message}`,
});
await db
.insert(SentEmails)
.values({ messageId: info.messageId, sentAt: Date.now() });
console.log("Sent an email to Joe. Message ID: ", info.messageId);
}
export const transporter = nodemailer.createTransport({
host: LOCAL_SMTP_HOST,
from: LOCAL_SMTP_ENVELOPE_FROM,
port: LOCAL_SMTP_PORT,
secure: false,
authMethod: "PLAIN",
auth: {
type: "login",
user: LOCAL_SMTP_USER,
pass: LOCAL_SMTP_PASSWORD,
},
});
-54
View File
@@ -1,54 +0,0 @@
---
interface Props {
title: string;
description?: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/images/headshot.jpg' } = Astro.props;
---
<!-- IndieWeb -->
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
<link rel="micropub" href="https://tasty-windows-lick.loca.lt">
<!-- Stylesheets -->
<link rel="stylesheet" href="/css/reset.css" />
<link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="/css/hcard.css" />
<link rel="stylesheet" href="/css/feed.css" />
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<!-- Feeds -->
<link rel="alternate" type="text/xml" title="Blog RSS" href="/blog/rss.xml">
<link rel="alternate" type="text/xml" title="Links RSS" href="/links/rss.xml">
-103
View File
@@ -1,103 +0,0 @@
---
import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import FormattedDate from './FormattedDate.astro';
export interface Props {
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
hideAuthor?: boolean,
hideSubheadings?: boolean,
maxEntries?: number,
};
const { headingLevel = 2, hideSubheadings = false, hideAuthor = false, maxEntries } = Astro.props;
const allPosts = (await getCollection('blog')).filter((post) => !post.data.hidden);
const posts = maxEntries === undefined
? allPosts
: allPosts.sort(sortByPubDateDescending).slice(0, maxEntries);
const distinctYears: number[] = posts
.map(post => pubDate(post).getFullYear())
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
.sort((a, b) => b - a);
function matchesYear(year: number) {
return (post: CollectionEntry<'blog'>) => pubDate(post).getFullYear() === year;
}
function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: CollectionEntry<'blog'>) {
const date1 = pubDate(post1).getTime();
const date2 = pubDate(post2).getTime();
return date2 - date1;
}
function pubDate(post: CollectionEntry<'blog'>): Date {
if (post.data.pubDate) {
return post.data.pubDate;
}
const date = new Date(post.id);
if (isNaN(date.valueOf())) {
throw new Error(`Post ${post.id} does not have a valid publication date.`);
}
return date;
}
const HeadingElem = `h${headingLevel} class="p-name"`;
const SubHeadingElem = `h${headingLevel + 1}`;
const SubSubHeadingElem = `h${headingLevel + 2}`;
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
const canonicalBlogUrl = new URL('blog', Astro.site)
---
<section class="h-feed">
<HeadingElem>
My blog
</HeadingElem>
<aside>
<AuthorElem>
This blog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
</AuthorElem>
<p hidden>
<a class="u-url" href={canonicalBlogUrl}>Permalink</a>
</p>
</aside>
<slot />
{ hideSubheadings
? <ul>
{ posts.sort(sortByPubDateDescending).map(post => (
<li class="h-entry">
<SubSubHeadingElem>
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
</SubSubHeadingElem>
<p class="p-summary" set:html={post.data.description} />
<FormattedDate className="dt-published" date={pubDate(post)} />
</li>
)) }
</ul>
: distinctYears.map(year => (
<SubHeadingElem>{year}</SubHeadingElem>
<ul>
{ posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => (
<li class="h-entry">
<SubSubHeadingElem>
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
</SubSubHeadingElem>
<p class="p-summary" set:html={post.data.description} />
<FormattedDate className="dt-published" date={pubDate(post)} />
</li>
)) }
</ul>
)) }
{ (maxEntries !== undefined && maxEntries < allPosts.length)
? <p class="full-feed-link"><a href="/blog">All blog posts</a></p>
: <></>
}
</section>
@@ -1,23 +0,0 @@
---
interface Props {
className?: string;
date: Date | string;
hidden?: boolean;
}
let { className, date, hidden } = Astro.props;
if (typeof(date) === 'string') {
date = new Date(date);
}
const dateStr =
date.toLocaleDateString('en-GB', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
---
{ hidden
? <time datetime={date.toISOString()} class={className ?? ''} hidden>{dateStr}</time>
: <time datetime={date.toISOString()} class={className ?? ''}>{dateStr}</time>
}
-78
View File
@@ -1,78 +0,0 @@
---
import FormattedDate from '../components/FormattedDate.astro';
import allLinks from '../data/links.ts';
type Link = (typeof allLinks)[number];
export interface Props {
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
hideAuthor?: boolean,
maxEntries?: number,
};
const { headingLevel = 2, hideAuthor = false, maxEntries } = Astro.props;
const headingElem = `h${headingLevel}`;
const subHeadingElem = `h${headingLevel + 1}`
const links: Link[] = maxEntries === undefined
? allLinks
: allLinks.sort(sortByDateAddedDescending).slice(0, maxEntries);
const distinctYears: string[] = links
.map(link => link.isoDateAdded.slice(0,4))
.reduce<string[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
.sort((a, b) => Number.parseInt(b) - Number.parseInt(a));
function matchesYear(year: string) {
return (link: (typeof links)[number]) => link.isoDateAdded.slice(0,4) === year;
}
function sortByDateAddedDescending(link1: (typeof links)[number], link2: (typeof links)[number]) {
const date1 = new Date(link1.isoDateAdded).getTime();
const date2 = new Date(link2.isoDateAdded).getTime();
return date2 - date1;
}
const canonicalLinksUrl = new URL('links', Astro.site)
---
<section class="h-feed">
<Fragment set:html={`
<${headingElem} class="p-name">
My links
</${headingElem}>
`} />
<aside>
<p hidden={hideAuthor}>
These links are collected by <a class="p-author h-card" href="/">Joe Carstairs</a>
</p>
<p hidden>
<a class="u-url" href={canonicalLinksUrl}>Permalink</a>
</p>
</aside>
{ distinctYears.map(year => (
<Fragment set:html={`
<${subHeadingElem}>
${year}
</${subHeadingElem}>
`} />
<ul>
{ links.filter(matchesYear(year)).sort(sortByDateAddedDescending).map(link => (
<li class="h-entry e-content">
<FormattedDate className="dt-published" date={link.isoDateAdded} />
<a class="u-url p-name" href={link.href} set:html={link.title} />
<p class="p-description" set:html={link.description} />
</li>
)) }
</ul>
)) }
{ (maxEntries !== undefined && maxEntries < allLinks.length)
? <p class="full-feed-link"><a href="/links">All links</a></p>
: <></>
}
</section>
-47
View File
@@ -1,47 +0,0 @@
---
---
<section class="h-card">
<div>
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
</div>
<header>
<h1>
Joe Carstairs
</h1>
<div hidden>
<a class="p-name u-url u-uid" href="https://joeac.net" rel="me">
Permalink
</a>
</div>
</header>
<div class="h-card__text">
<p>
Hi! 👋 My name is <span class="p-given-name">Joe</span>
<span class="p-family-name">Carstairs</span>. Im a
<span class="p-job-title">software developer</span> at
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>,
a Divinity student at the University of Edinburgh,
a lapsed fiddle player and a very occasional poet.
</p>
<p>
HMU with your thoughts on Elizabethan poetry, connecting to the
Internet, and the Apocalypse of Saint John.
</p>
<p>
<small>
Or get me on
<a href="https://www.facebook.com/joe.carstairs.5" rel="me">Facebook</a>,
<a href="https://mastodon.social/@joe_carstairs" rel="me">Mastodon</a>,
<a href="https://www.linkedin.com/in/joe-carstairs-0aa936277" rel="me">LinkedIn</a>,
<a href="https://bsky.app/profile/joeacarstairs.bsky.social" rel="me">BlueSky</a>,
or <a href="https://github.com/joeacarstairs" rel="me">GitHub</a>.
</small>
</p>
</div>
</section>
-100
View File
@@ -1,100 +0,0 @@
---
import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import FormattedDate from './FormattedDate.astro';
import { renderGemtextToHtml } from '../renderGemtextToHtml';
export interface Props {
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
hideAuthor?: boolean,
hideSubheadings?: boolean,
maxEntries?: number,
};
const { headingLevel = 2, hideSubheadings = false, hideAuthor = false, maxEntries } = Astro.props;
const allPosts = await getCollection('microlog');
const posts = maxEntries === undefined
? allPosts
: allPosts.sort(sortByIdDescending).slice(0, maxEntries);
const distinctYears: number[] = posts
.map(post => pubDate(post).getFullYear())
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
.sort((a, b) => b - a);
function matchesYear(year: number) {
return (post: CollectionEntry<'microlog'>) => pubDate(post).getFullYear() === year;
}
function sortByIdDescending(post1: CollectionEntry<'microlog'>, post2: CollectionEntry<'microlog'>) {
return post2.id.localeCompare(post1.id);
}
function pubDate(post: CollectionEntry<'microlog'>): Date {
console.warn(post.id);
const date = new Date(post.id.replace(/\.[0-9]+$/g, ''));
if (isNaN(date.valueOf())) {
throw new Error(`Post ${post.id} does not have a valid publication date.`);
}
return date;
}
const HeadingElem = `h${headingLevel} class="p-name"`;
const SubHeadingElem = `h${headingLevel + 1}`;
const SubSubHeadingElem = `h${headingLevel + 2}`;
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
const canonicalMicrologUrl = new URL('microlog', Astro.site)
---
<section class="h-feed">
<HeadingElem>
My microlog
</HeadingElem>
<aside>
<AuthorElem>
This microlog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
</AuthorElem>
<p hidden>
<a class="u-url" href={canonicalMicrologUrl}>Permalink</a>
</p>
</aside>
<slot />
{ hideSubheadings
? <ul>
{ posts.sort(sortByIdDescending).map(post => (
<li class="h-entry">
<SubSubHeadingElem>
<a class="u-url p-name" href={`/microlog/${post.id}`}>{post.id}</a>
</SubSubHeadingElem>
<section class="e-content" set:html={post.body} />
<FormattedDate hidden={true} className="dt-published" date={pubDate(post)} />
</li>
)) }
</ul>
: distinctYears.map(year => (
<SubHeadingElem>{year}</SubHeadingElem>
<ul>
{ posts.filter(matchesYear(year)).sort(sortByIdDescending).map(post => (
<li class="h-entry">
<SubSubHeadingElem>
<a class="u-url p-name" href={`/microlog/${post.id}`}>{post.id}</a>
</SubSubHeadingElem>
<section class="e-content" set:html={renderGemtextToHtml(post.body ?? '')} />
<FormattedDate hidden={true} className="dt-published" date={pubDate(post)} />
</li>
)) }
</ul>
)) }
{ (maxEntries !== undefined && maxEntries < allPosts.length)
? <p class="full-feed-link"><a href="/microlog">All microlog posts</a></p>
: <></>
}
</section>
-19
View File
@@ -1,19 +0,0 @@
---
---
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/blog">Blog</a>
</li>
<li>
<a href="/microlog">Microlog</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
</nav>
-31
View File
@@ -1,31 +0,0 @@
---
---
<script src="../scripts/otp-form-wc.ts"></script>
<link rel="stylesheet" href="/css/otp.css" />
<dialog class="otp-dialog">
<otp-form>
<form class="otp-form" class="otp-form">
<p>
I've sent a six-digit code to <span class="otp-recipient">your email address</span>.
Let me know what it is, so I can confirm this really is your email address. The
code will be valid <span class="otp-valid-until">for five minutes</span>.
</p>
<p hidden class="error"/>
<div class="otp-inputs">
<input maxlength="1" name="1" required autocapitalize="characters" autocomplete="one-time-code" aria-label="First digit of one-time passcode" autofocus>
<input maxlength="1" name="2" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Second digit of one-time passcode">
<input maxlength="1" name="3" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Third digit of one-time passcode">
<input maxlength="1" name="4" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fourth digit of one-time passcode">
<input maxlength="1" name="5" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fifth digit of one-time passcode">
<input maxlength="1" name="6" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Sixth digit of one-time passcode">
</div>
<input type="submit" value="Verify">
<button disabled class="resend-button">Resend (60s)</button>
</form>
</otp-form>
</dialog>
-2
View File
@@ -1,2 +0,0 @@
export const SITE_TITLE = 'Joe Carstairs';
export const SITE_DESCRIPTION = 'Joes personal website.';
-61
View File
@@ -1,61 +0,0 @@
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { extendedGlob } from "./loaders/extended-glob";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: extendedGlob({
pattern: "**/*.(md|mdx|gmi|html)",
base: "./src/content/blog",
postprocessSlug: (slug: string) => slug.replaceAll(/^capsule-longlog\//g, ""),
ignore: [
"capsule-longlog/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({
title: z.string(),
hidden: z.optional(z.boolean()),
description: z.optional(z.string()),
pubDate: z.optional(z.date()),
updatedDate: z.optional(z.date()),
}),
});
const microlog = defineCollection({
loader: glob({
pattern: "**/*.gmi",
base: "./src/content/microlog",
generateId: ({ entry }) => entry.replace(/\.gmi$/, ""),
}),
});
export const collections = { blog, microlog };
-74
View File
@@ -1,74 +0,0 @@
---
title: Hararis Sapiens on Religion
description: >-
In which I discuss why I think Hararis characterisation of religion
is inadequate because its too materialistic.
pubDate: 2024-01-14
---
Ive been slowly re-reading Yuval Noah Hararis 2014 classic,
<a href="https://www.ynharari.com/book/sapiens-2">Sapiens</a>,
which apart from being ridiculously over-scoped and hilariously
under-evidenced, is proving delightfully entertaining.
Ive just finished chapter 12, covering the world history of all
religion in thirty pages. Of course, at that level of brevity,
there will be many deficiencies. But heres some thoughts - not
terribly well organised - which stand out to me.
Hurari generally assumes a materialist metaphysic (a problem which
blights the book more generally). Nothing exists except physical stuff.
This gives him severe tunnel vision. As a consequence of this
restricting metaphysic, he is forced to adopt limiting accounts of what
the role of religion is in world history, and therefore what religion is.
> The crucial historical role of religion has been to give superhuman
> legitimacy to [all social orders and hierarchies].
> Religion can thus be defined as <em>a system of human norms and
> values that is founded on a belief in a superhuman order</em>.
> <footer>p. 234</footer>
It might seem a little unfair to criticise Harari for giving a
materialist account of religion. <i>Sapiens</i> is, after all, a
materialist world history.
But this account is just one extreme example of how that project, to
give a materialist account of world history, will inevitably lack the
metaphysical resources to really understand the human story.
On Hararis view, any human enterprise which attempts to understand
that which transcends direct human experience is at best an effort in
imaginative story-telling. All scientific theory, theology, ethics and
metaphysics either contorted out of all recognition into a pragmatic
fiction or is cast to the flames.
In particular, its a view which is incapable of taking seriously some
of the most important questions human beings have grappled with in the
course of their history. Those who know me wont be surprised at which
ones Im going to pick out: who was the being which made their covenant
with Abraham? How is that promise being fulfilled? And who the heck was
Jesus of Nazareth?
If Hararis characterisation of religion is adequate - and the Abrahamic
faiths come under that banner - then those questions are reduced to
nothing more profound than Doctor Who fans arguing over canon. The
question of who God is becomes a mere tool for the organisation of
society, rather than a substantial and important question on a matter
of fact.
This is a shortcoming for its own sake: a materialist account of
religion cannot adequately account for the phenomenon of religion
itself.
But it is also a shortcoming even by its own lights. Without giving
serious consideration to the substantial matter of what Harari calls
religion (which, to his mind, includes the Abrahamic faiths,
Hinduism, paganism, animism, Buddhism, Shintoism, Confucianism,
capitalism, communism and Nazism), even the material facts are
inexplicable. Why would, as Harari is keen to point out, out, people
fight and die over and over again for a fiction?
The material facts themselves prove that religion as he construes it
is not window dressing to the real story of history. It cannot merely
serve as a mechanism in the churning of material history. It is itself
the centre of the story.
-259
View File
@@ -1,259 +0,0 @@
---
title: EUHWC Toast to the Lassies 2024
hidden: true
description: >-
At the <a href="https://www.euhwc.co.uk">EUHWC</a> Burns meet in
Ullapool last weekend, I had the last privilege of giving the Toast to
the Lassies. Particularly for the benefit of those who werent there,
here it is in full!
pubDate: 2024-01-29
---
Had Burns, instead of his sweet bonnie Jean,<br>
his skills poetical for to mature<br>
had any one of our clubs lassies seen<br>
he would forever have remained obscure.<br>
If he had nothing but this box of worms<br>
Scotia would have been poorer, that Im sure.<br>
Now none of us can claim to be a Burns,<br>
Im no poetic master, still, Ill have a punt,<br>
though lets be clear, Ill do it on my terms.<br>
Ive everywhere avoided being blunt -<br>
politeness matters more than any schema -<br>
but it is hard when Islas such a cunt.<br>
It was a challenge to produce a terza rima<br>
I could recite withouten snoring;<br>
youve been so stiff I thought youse had oedema.<br>
The bother is this year is youll all been boring:<br>
no drugs, no sex, no gossiping or lies,<br>
no rock and roll, and hardly any whoring.<br>
But hey well, rules is rules, Ive had to try!<br>
At least it cant be worse than the reply.<br>
Ill start with Audrey, the clubs senior member,<br>
for if theres something that I say which disconcerts her,<br>
its fine: the poor old girl, she wont remember.<br>
She likes to let us think shes a hard worker<br>
but were electing a third social sec…<br>
its pretty clear shes just another shirker.<br>
This lady, half American, half Czech,<br>
for study, moved to Scotland for to do<br>
American history really, what the heck?<br>
The club is so much louder thanks to you:<br>
impressive vocals for just five foot two.<br>
That woman, Willow, reggles is bespeckled<br>
with her sickle and her fishing tackle<br>
shackled by the shins while she is heckled;<br>
the way that Willow waddles maks me cackle<br>
like a speckled jackal getting tickles,<br>
worth a shekel in the tabernacle;<br>
I chuckle muckle at her love of pickles<br>
which she wiggles when she has the heart<br>
while work for the Committees fickle trickles.<br>
Her modus operandi: <em>you cant rush art.</em><br>
Her reimbursements programmes going great;<br>
any day now, shell maybe even start.<br>
She cannot walk without Audrey, her mate:<br>
I wonder when theyre going to consummate.<br>
Although they make them pretty tough in Peebles,<br>
the thought of actually going up a peak<br>
fills Shona Lewis with the heeble-jeebles.<br>
New car? We miss your beautiful antique!<br>
How long before this ones also up a creek?<br>
Once there was a lass called Hannah Collier<br>
whom even hell below regarded nasty,<br>
deeply despised by all that dwells there.<br>
Datings proceeding slowly for our lassie;<br>
not far from giving up til she beguiles<br>
a hot Italian in Southsider: classy!<br>
At first, Michaelo seems to be all smiles<br>
till it transpires hes one of Dantes demons…<br>
I guess its back to posters of Harry Styles.<br>
One day youll get a decent boy, keep dreamin;<br>
somewhere there waits a handsome Mr Collier.<br>
Hopefully when she meets him shell no be steamin.<br>
Hannah, Im not sure why you chose to maul your<br>
poor skeleton at Subway (shes still tetchy)<br>
and then abandon what remains of all your<br>
dignity at Ryvoan with a Frenchie!<br>
I think he wishes that he never met ye.<br>
And has a quiet Felicia eer been seen?<br>
The energy she has is frankly wild.<br>
Ive never seen a hillwalker so keen!<br>
<i lang="de">Ssie ischt raschtlos und nie gelangweilt</i>.<br>
She eats raw oats with soggy protein powder:<br>
a camping pot has neer been worse defiled.<br>
She uses what her Maker has endowed her<br>
with: her recorder skills are off the charts;<br>
youse think Im joking, but I wouldnt doubt her!<br>
This lass of the land of the Rot-Gold-Schwarz<br>
will soon depart, though long we might beseech you<br>
to stay. Of course, youll break all of our hearts,<br>
but mine most of all. Any time, Felicia,<br>
Creag Meagaidh calls, I know routes up the rear<br>
dark and under-explored that I can teach you!<br>
I wont deny I think its rather queer<br>
the things you do with chickpeas, but no matter.<br>
Youre keen, youre quick, youre cool, that much is clear.<br>
In fact, I think youd make a damn good faffer:<br>
swoop down on distilleries like the Luftwaffe.<br>
And now we come to our girl Emily Topness!<br>
Youre keen for social sec. Youd suit the role<br>
because… Im not quite sure, its embdys guess.<br>
We met your sister, and she was just as dull.<br>
No, please drone on about Icelandic soil!<br>
Poor Joe here down the fronts bored out his skull.<br>
And since I mentioned Joe I hate to spoil<br>
it for you but youve got the inferior Joe,<br>
by Jove, no joke, its Jock heres got the style!<br>
Nah, write the boy a sonnet, get in the flow,<br>
Whatever you producell beat by thrice<br>
your Masters thesis. Whatd you got to show<br>
for months of hunting for the butterflies?<br>
There werent any. Oh, and have some sense,<br>
cos Ive heard rumours I assume theyre lies <br>
youve called yourself the poet in residence.<br>
You know you cant compete, drop the pretence.<br>
Tereza was our gear sec for last year.<br>
She helped herself to stuff: thats factual.<br>
Now when she asks to loan a bit club gear<br>
we have to ask her to provide collateral.<br>
She picked up tin whistle pretty sharp!<br>
Which is to say, shes not a natural.<br>
Shes nowhere happier than under tarp<br>
gazing up at the moon and stars alone<br>
somewhere distant and remote like Glen Tarff.<br>
Now what to say about Lucy Ma-the-soooon....<br>
she likes… to faff… mm hmmm… well, moving on!<br>
And now we come to Emilie the French.<br>
She seems to be nice on the trips we see her<br>
but my distrust of frogs will neer be quenched.<br>
Claims shes a pharmacist? So shes a dealer.<br>
Need some pills in a pinch? You call, shes there<br>
at your door in her rally-approved four-wheeler.<br>
One question we have is, why are you here?<br>
Most folk are in uni, youve no refutin<br>
you were kicked out after second year!<br>
Now the Engineering grad, Sophia Newton.<br>
Your namesake, Isaac, was a man convicted,<br>
constructed calculus; but no computin,<br>
not even Isaacs, couldve eer predicted<br>
youd drop the Eng for creative writing!<br>
now thats what I would call a self-inflicted<br>
inflection point! It must be quite enlightening,<br>
but that doesnt excuse when you give us an earful.<br>
The blood boils in our veins, the rage heightening,<br>
and youre an American, that makes me fearful.<br>
Whats your secret? You have us knackered!<br>
What are you on to always be so cheerful?<br>
Now we approach the topic of Merzbacher.<br>
Wait, shes not here? Abandoned ship?<br>
She says shes informatics: so shes a hacker?<br>
She has strong views, she lets her anger rip.<br>
Poor George got an earful, full of future advice,<br>
but why hasnt she been on another club trip?<br>
Were cruel to focus on this list of vice;<br>
the fact remains: shes headstrong and nice.<br>
On Skye, a lady gave her poles to Sasha,<br>
which was really nice - I mean just the best -<br>
but Sasha really didnt have to flash her.<br>
Quick history lesson: way back, RBS<br>
led the banking system to self-destruct<br>
and left taxpayers to pick up the mess.<br>
Since then, the names so irredeemably fucked<br>
theyve had to ditch the brand once and for all.<br>
Theres one lassie who I need not instruct<br>
What, these days, the Royal Bank is called<br>
cos NatWests nasty history of scandal<br>
didnt stop Booth from working there at all.<br>
Nothing motivates her more than to trample<br>
upon the working class. They set her free.<br>
She sank the pound quicker than the Belgrano,<br>
because there is no such thing as society,<br>
thats how it is, is it? All right, I see.<br>
Now, coming all the way from Glenmore Lodge,<br>
its Ellies turn! We have done what we can,<br>
although Im scared what shell put in my squash.<br>
She wasnt into Benji, but listen man,<br>
youre lucky that you dodged her drunken benders.<br>
Youll wake up in a tent in Kyrgystan,<br>
as for how you got there, no-one remembers,<br>
and if youd known youd be sleeping next to Ellie,<br>
you wouldve brought some fucking ear defenders.<br>
Shell wrap you in bubblewrap, from your ears to your belly,<br>
cotton clothes for none, and no complaining,<br>
applying safety to the max, spare socks in your wellies.<br>
Shes always at her Mountain Leader training,<br>
practicing her night nav in the locale,<br>
pursuing QMDs - unless its raining.<br>
But some water should not scare our gal!<br>
Shes had much experience with the wet as of late:<br>
after all, she got on well with our navy pal.<br>
What was the age of that particular first mate?<br>
Older than your ex - always part of the plan?<br>
Ah, of course! He was a spry twenty-eight!<br>
Youths for the losers, lets get you a real man,<br>
mature and rugged, but kind and astute?<br>
Just make sure hes not as old as your gran.<br>
One request we all have is you ditch the uke:<br>
never have strings been pluckèd quite so shitely;<br>
we would all much rather be hit by a nuke.<br>
And please shut up about your nice society.<br>
We are all glad you had a fun summer,<br>
but bringing it up throws us right back to sobriety.<br>
To lose you of course would be a bummer:<br>
that is, for your carefully groomed newcomers.<br>
Now time for the main woman, El Presidente!<br>
To here, its been like getting stones to bleed,<br>
but in Isla Burslems case weve material aplenty!<br>
As Holy Scripture says, let those who lead<br>
well be worthy of double honour, so<br>
your bit is double length its quite the screed!<br>
Ill start off with her brilliant boyfriend oh!<br>
Not boyfriend! Friend? To me this rather smacks<br>
of low commitment, but what do I know?<br>
So far, hes disappointing, but on track.<br>
Whats he up to Isla: seven minutes? neat!<br>
Despite that, he is never holding back<br>
your blossoming romance with Dr Peat.<br>
Dont deny it, that launch was pretty hard!<br>
Its fifth base next: thats photos of his feet.<br>
Its fair to say her reputations marred.<br>
We all regret that we did once anoint<br>
her President: her premierships ill-starred.<br>
Hey - youre meant to be in charge of this joint!<br>
Youre seldom seen cos of the mountaineering<br>
meets that youre always on. Youd made your point<br>
before you chose to go off disappearing<br>
to <em>New Zealand</em>… we get the message! Plus<br>
weve had enough of all your domineering:<br>
maybe its time we put you on a bus!<br>
Nah, Im just joking. All Ive saids refutable.<br>
But the boys, we mean this next bit, all of us,<br>
so stop me Isla if this isnt suitable<br>
but honestly we think your mum is beautiful.<br>
Alas, I have to bring an end to this rhyme.<br>
I know it wasnt much, in our defence,<br>
the fact you used ChatGPTs a crime.<br>
I hope Ive not caused over much offence<br>
dont worry, that is it, Ive said my bit,<br>
so Ill turn from the ladies to the gents.<br>
Yeah, dont look away now, we wrote this shit!<br>
I see you looking at your laces, Chris!<br>
Wit without real goodwill is not legit,<br>
so boys, dont send sincerity to piss!<br>
Why did God say hed take our hearts of stone<br>
and give us hearts of flesh? For this, for this!<br>
Here is flesh of our flesh, bone of our bone;<br>
love, and love nothing more but God alone.<br>
-95
View File
@@ -1,95 +0,0 @@
---
title: Why Easter is the best week of the year
description: >-
Based on a talk given to my colleagues at
<a href="https://www.scottlogic.co.uk">Scott Logic</a> for Maundy
Thursday, 2024.
pubDate: 2024-03-30
---
As you might have noticed, it is Easter this week! So I'd like to take five or
five minutes of your time to share why I and about two billion other humans
going about the place just now think Easter is the best week of the year. And
it's got something to do with a special Christian ritual called Communion.
Communion, at its heart, is about as simple a ritual as you can get. You get
together with a bunch of other people. You share some bread, and you share some
wine.
And its because of this ritual that so many people regard Easter as the best
week of the year. I want to explain to you why that is, and more than that, I
want to convince you that Easter is the best week of the year for you, too!
If youve passed by _The Hub_ at the top of Johnstone Terrace here in
Edinburgh recently, you might have notice the banner which is draped over
the railings just now reading, RITUALS THAT UNITE US.
Now, that might seem like an odd idea. But wouldnt that be great, if we
actually had a ritual which could unite us? Because the world could surely do
with a bit more unity right now. The world seems so divided, and sometimes it
seems like theres no hope for real unity.
We can see that in our politics. Were divided about foreign policy, about
taxation policy, about trade policy, about environmental policy.
And the conflicts that we have in this country seem pretty trivial when we
remember the conflicts that are playing out in other parts of the world right
now. In Israel and Gaza. In Sudan. In Russia and Ukraine.
And theres plenty of conflict happening on the small scale, too. Often its the
smallest-scale conflicts which hurt us the most deeply. Your landlord pushes you
around. That friend you trusted like no-one else in the world lets you down. The
partner or spouse you loved like no-one else in the world you end up fighting.
Its possible that youre going to be reading this right now with a heavy heart
because of a broken relationship in their life. And doesnt that hurt more than
anything else we know?
When the world is groaning so heavily under the weight of conflict, and some
banner on _The Hub_ tells us a ritual can unite us, that seems so out of
proportion to the scale of the problem, doesnt it? What can a ritual do? A bit
of old superstition? An excuse to divide people, maybe what can a ritual do
to unite us?
Well, two thousand years ago, a man had a meal with his friends. Together, they
shared a meal of bread and wine which, in that time and place, was the most
ordinary meal imaginable.
And yet, in that most ordinary event imaginable, something was happening which
was totally unimaginable. As this man, Jesus, shared the elements of this meal,
he made some extraordinary statements about what he was doing: take, eat, this
is my body drink this, all of you; this is my blood of the new covenant.
(The word covenant means a promise.)
He told them he wasnt just giving them bread and wine, he was giving his body
and his blood, and a promise.
Before Jesus ate another meal, he was flogged and nailed to a cross. His blood
was spilt and his body broken, even to death.
And yet, that wasnt the end of the Easter story. Because three days later,
mourners turned up at Jesus tomb to pay their respects, and found the tomb
empty, the stone rolled away. Then they became the first of crowds of
incredulous eyewitnesses to see Jesus, the same Jesus who was killed on a cross,
alive.
Some magic trick, right? But this matters a hell of a lot more than just some
magic trick. Because Jesus became the first person in history to prove that you
really can both have your cake and eat it. He gave his life, and lived! As a
result, we can have his life and our own. We can join with Jesus through the
ritual of Communion which he established, and thereby, through Jesus body, join
together with everyone else who takes part in that ritual, as one body. Then we
can start living our brand-new, full-fat, original-recipe life overflowing with
generosity where we too can both give our life to others and enjoy it ourselves.
Indeed, Jesus taught us and showed us that its precisely by giving our lives to
others that we get to truly live ourselves.
This is why, in spite of all the division which persists in the world today, two
billion people regard this week as the best week of the year. Two billion
people, from every nation on Earth, speaking thousands of languages, of every
age and culture and gender and race, who defy the divisions of this world to
insist on joining together as one body in Jesus.
Because Jesus new covenant, his promise to all of us, is that in an apparently
hopelessly divided world, there exists real hope for unity. And thats why
Easter is the best week of the year.
-32
View File
@@ -1,32 +0,0 @@
---
title: Tracking pixels
description: Concerning a notice in the privacy policy of an HR app
pubDate: 2024-04-10
---
I recently made the mistake of reading the privacy policy of the HR app my
employer uses. Take this choice excerpt:
> **Tracking Pixels**
>
> We may place tracking pixels in our Applicant Tracking System (ATS). These
> pixels are used for analytics purposes, including to track statistical
> information around when an email sent to you is opened.
Translation:
> We reserve the right to know whether you have read our emails, how long you
> spent reading them, at what time and in what physical location you read them,
> and any other information we can get our hands on. We reserve the right to do
> this as soon as you open an email, without asking for your permission first,
> and ask for your forgiveness later. That is, if you ever find out: which you
> won't, because we also reserve the right to hide this information in a Privacy
> Policy which we know you won't read, and we design our data-harvesting monster
> to be completely undetectable to anyone except determined experts, on purpose.
> We reserve the right to leave this data lying unused in a data warehouse where
> someone might steal it, and we also reserve the right to use it, if we can
> think of something satisfactorily dastardly to do with it. On the bright side,
> we probably won't, so having your data leaked to hackers and fraudsters is
> probably the worst you have to worry about. Unless we sell your data to
> advertisers, or a ~black hole of stolen data~ Large Language Model. Oh yeah,
> we probably reserve the right to do that, too. We doubt anyone will ever check.
-48
View File
@@ -1,48 +0,0 @@
---
title: Who consecrates the tabernacle? (Ex 29)
description: A quick, cursory and possibly completely rubbish observation I've made on Exodus 29.
pubDate: 2024-04-11
---
I've been reading Exodus recently, and the ending of Chapter 29 stuck out to me.
For pages and pages (since Chapter 25!) God has been giving Moses exact and
exorbitant instructions for how to build the tabernacle, what offerings to give
upon it, who the priests are going to be, what the priests are going to be
wearing. It's the fanciest IKEA manual you've ever read. No expense spared.
Everything is drowning in gold, silver, incense, myrrh, silk, fragrant oil, you
name it.
What's this about? The answer seems obvious: it's to sanctify the temple! To
make it beautiful enough and pure enough that even God could live there.
But right at the end of Chapter 29, in vv 43-44, there's this wonderful twist:
> ... and the place will be consecrated by **my glory**. So **I** will
> consecrate the tent of meeting and the altar and will consecrate Aaron and his
> sons to serve me as priests. (NIV, emphasis mine)
All this work, and at the end of the day, it's **God** who sanctifies his own
temple? So why all the fuss? Well, it looks like the final two verses of the
chapter give us the answer:
> Then I will dwell among the Israelites and be their God. They will know that I
> am the <span class="small-caps">Lord</span> their God, who brought them out of
> Egypt so that I might dwell among them. I am the
> <span class="small-caps">Lord</span> their God.
So on a cursory reading, it looks like God is telling them something
extraordinary about the kind of God he is. He is not like the other gods, who
demand this kind of worship to appease their pride. Nor is there, unlike the
other gods, anything the Israelites could do to make a place habitable for him.
That's because there's nothing the Israelites could do to cleanse themselves of
their sin: only the 'glory' of God could do that. And indeed, God has no need of
worship to puff up his pride, since his glory existed before the world was made,
and human beings can do nothing to either add to it or subtract from it.
Instead of a vain god who seeks tribute, this is a Father God who seeks to
'dwell' (literally to 'camp') among his people. The God who has put into motion
a rescue plan to pull the Israelites out of Egypt specifically for this purpose.
As it was in the beginning, when God dwelt in the Garden with Adam and Eve. This
is his mission. This is what he's like. And he wants his people to know this, so
that they can dwell together.
-250
View File
@@ -1,250 +0,0 @@
---
title: God Is Not Great, initial thoughts
description: >-
My spark notes on Hitchen's classic 2007 polemic against religion, plus some
initial thoughts on how I want to respond to it.
pubDate: 2024-04-14
---
These are my 'spark notes' on _God Is Not Great_, Christopher Hitchen's classic
2007 polemic against religion in all its forms, and call to adopt secular
humanism as its rightful replacement.
The book can be coherently read as a collection of independent essays. That
said, with a few exceptions, each chapter in _God Is Not Great_ contributes to
one of three main themes, and I think this is a helpful way of summarising the
overall movement of the book. The three themes I've identified are as follows:
<ol>
<li>
<a href="#1-religion-is-evil">Religion is evil</a>
</li>
<li>
<a href="#2-religion-is-untrue">Religion is untrue</a>
</li>
<li>
<a href="#3-secular-humanism-is-a-better-alternative-to-religion">Secular humanism is a better alternative to religion</a>
</li>
</ol>
As for the exceptions: Chapters 13 and 17 in part contribute to both the first
theme and the third, while Chapters 1, 3, 12 and 14 don't fit into these broad
themes, and are self-standing.
## 1. Religion is evil
- Chp 2
- Religion is violent, because:
- It has to be missionary, because it is insecure in its own beliefs (p17)
- It provokes tribalistic conflict, in a similar manner to racism (pp35-36)
- Chp 4
- Religion is bad for your health, because:
- Faith in medicine is a threat to religion's thrall (p47)
- Religious doctrines may contradict sound medical advice
- The right to religious freedom may be abused to deflect criticism of
unhealthy practices (p50)
- Religion has a special relationship with child abuse, and is incapable of
accepting open moral criticism of itself for this
- Chp 13
- Religion tends towards evil, because it requires fanaticism in order to
spread, and fanaticism tends towards evil (p192)
- Chp 16
- Religion causes child abuse, because:
- Terrifying children with eschatology is child abuse
- Religious education is propaganda and should not be inflicted on children
who are not yet mature enough to respond to it rationally
- Religion consistently mandates cruel genital mutilation of children
- Christians and Muslims have spread misinformation about masturbation,
which leads to dangerously sexually repressed adult men, which in turn
leads to sexual abuse of children by those men (as well as sexual abuse of
women)
- Child abuse in churches is not a case of a few bad eggs, it is
institutional and based on an ideological need to control the minds and
sexual organs of children
- Religion institutionalised torture in medieval Europe
- Religion makes honest and nuanced debate about abortion impossible, because:
- Nuanced debate is pushed out by extreme and implausible religious doctrine
- Religious people would rather use the unborn as objects of doctrine than
human beings in need of protection
- Chp 17
- Religion is the only reason anti-Semitism is possible
- Religion in its fullest expression is indistinguishable from
totalitarianism, because:
- The defining characteristic of both religion and totalitarianism is the
absolute right to rule of the ruler, even when they rule with caprice
- Religion and totalitarianism are also characterised by the need to
extinguish heresy with violence
- Religion and totalitarianism alike must propose a total solution to all
life's problems, require blind faith from its adherents, and demand all
aspects of life public and private be submitted to total supervision. This
doesn't bring out the best in us
- Religious/totalitarian systems are unable to take accountability and
therefore improve over time, in contrast to secular humanist systems
- History has proven this as fact:
- In the ancient world, religious totalitarianism was the normal form of
government
- Calvin, the inspiration for the Presbyterian Christian tradition, which
included South African apartheid, was the epitome of a totalitarian
dictator, demanding total control on the private lives of his citizens
in Geneva, on the pain of humiliation in this life and eternal torment
in the next
- History has also proven that, rather than standing in opposition to the
supposedly secular totalitarianisms of the twentieth century, religion
actually aided and abetted totalitarianism:
- Rome supported fascist movements throughout Europe, including Italy,
Hungary, Spain, and Ireland
- Rome accommodated Naziism by handing over control of its schools,
permitting the use of parish records to identify those with Jewish
ancestry, disbanding Catholic opposition political parties, declaring
Hitler's birthday a Church holiday, and running the 'rat line' to South
America after the military defeat of Naziism
- Although not quite as arse-licking as the Vatican, Germany's Protestant
churches also mostly capitulated to Nazi totalitarianism
- Japanese soldiers committed enormous atrocities across the Far East in
the name of their god-emperor, Hirohito
- The strategy of the Communists was first, to use religion as a prop to
gain power, and then to replace religion with itself. Notice the
striking commonalities between religion and communist totalitarianism:
- Infallible leaders
- A permanent war on heresy
- Institutionalised torture
- Scapegoating the innocent rather than accepting accountability for
failures
- Justifying any means necessary in order to achieve an ultimate end
## 2. Religion is untrue
- Chp 5
- Religion was a barbaric attempt to explain physical phenomena. Science now
does a much better job, so religion can be discarded as a redundant theory
- Chp 6
- Religion is 'solipsistic', which is to say:
- Religion divides the world into an in-group and an out-group
- When the in-group receives good fortune, that is interpreted as God's
blessing
- When the in-group receives bad fortune, this is inexplicable
- Whatever happens to the out-group is irrelevant
- This is a redundant theory which explains little, and therefore we should
not believe it
- Chp 7
- We should believe that the Pentateuch is a fiction, because:
- Exodus is inconsistent with the archaeological evidence
- Textual evidence in Deuteronomy suggests the texts were written much later
than the supposed events were supposed to have taken place
- Throughout the Pentateuch, Moses is referred to in the third person, which
is not consistent with the claim that Moses himself authored it
- The Pentateuch contains events we should _hope_ to be false, such as Moses
ordering multiple massacres, and the Ten Commandments classifying wives as
their husbands' property
- The Pentateuch - indeed, the whole Bible - is limited in scope to a small
corner of the Middle East, which is not consistent with the universal
nature of the supposed God who is supposed to have inspired its authorship
- Chp 8
- We should not believe the Gospels, because:
- Matthew and Luke disagree on the virgin conception
- Matthew and Luke disagree on the genealogy of Jesus
- Matthew and Luke disagree on when Mary and Jesus escaped to Egypt
- Luke dates the birth of Christ during both the reign of Herod in Judea
and the governorship of Quirinius in Syria, but these two events did not
overlap, so this is impossible
- As far as we know, the Romans did not, and would not, demand that the
people assemble in one place in order to be counted for a census
- The Gospels disagree about the Sermon on the Mount
- The Gospels disagree about the Anointing of Jesus
- The Gospels disagree about the Crucifixion and Resurrection of Jesus
- The Gospels disagree about the betrayal of Judas
- The Gospels disagree about the denial of Peter
- John suggests he thinks Jesus was probably born in Galilee to an
ordinary family with no proven link to King David's genealogy
- Mary's attested behaviour during the ministry of Jesus is not consistent
with the Nativity story
- John 8:3-11 (the stoning of the woman caught in adultery) is a later
insertion to John's Gospel
- Chp 9: the Koran is borrowed from a hotchpotch of Jewish and Christian
myths
- Chp 10
- Miracles (such as the Resurrection of Jesus) should not be believed,
because:
- See Hume's _On Miracles_
- Extraordinary claims require extraordinary evidence
- Believing miracles is comparable to believing reports of aliens (p144)
- Chp 11
- Religions are founded in credulity
- Chp 15
- The crucifixion story makes no sense, because:
- I'm supposed to be morally responsible for Adam's sin, but I amn't!
- Even Adam can't be fully blamed for Adam's sin, because he was set up!
- The Jews who crucified Jesus are supposed to be blameworthy for their
crime, even though the crucifixion was allegedly both necessary and
inevitable
- I'm allegedly given the free will to either accept or deny the offer,
even though denying the offer will lead to an eternity of torment: any
sensible God who cared a mite would not have given me the choice
- Religious rules are impossible to follow, and this leads to either
spiritual policing, organised hypocrisy, or both
## 3. Secular humanism is a better alternative to religion
- Chp 13
- Faith has inspired great heroism. But the heroism is better explained by the
heroes' humanism than it is by their faith itself
- For example, Martin Luther King Junior didn't really preach Christianity,
because he preached forgiveness, while Christ preached eternal torment for
the 'inattentive' (pp175-6)
- Chp 17
- Religion is the only thing sustaining anti-Semitism
- Secular humanist political systems can take accountability, respond to
criticism and improve over time, while religious systems exempt themselves
from legitimate criticism, stifling progress
- The only alternative to totalitarianism is pluralism, which is inherently
secular
- Chp 18
- Secular humanism has been a powerful positive force throughout history in
face of oppression by religion, as proved by example:
- Socrates proved that conscience is innate, and that a great way to mock
dogma is with satire which pretends to accept that dogma
- Lucretius, Democritus and Epicurus had better explanations for the way the
world worked than religion (which is why their work was suppressed in
Christian Europe and nearly lost forever as a result). Once rediscovered,
their ideas kick-started the Scientific Revolution in Europe
- Spinoza's deistic ideas had a huge influence despite Jews collaborating
with their Christian oppressors to try and ban his writing out of
existence
- Boyle and Voltaire may have been closet atheists, agnostics or deists
- Kant 'overthrew' the cosmological and ontological arguments for the
existence of God, and proved (by means of the Categorical Imperative) that
human decency does not require any theological assumptions
- Let's chuck some more names in the ring: Gibbon, Hume, Paine, Franklin,
Darwin, Einstein
- Jews were once doubly ghettoed: on the outside by oppressive Christians,
and on the inside by oppressive self-racialisation. Secular humanism freed
Jews from both these ghettoes, which in turn led to an outpouring of
secular Jewish brilliance
- Ancient Jews were on the road to a quasi-secular Hellenism, before that
was ruined forever by the tyrannical and fanatical Judas Maccabeus, with
disastrous consequences for the history of Western civilisation. The
Abrahamic religions we know today were not inevitable, and it is possible
to imagine what Western history would have been like without it
- Chp 19
- Secular humanism is a better alternative to religion, because:
- Religion requires clinging to immovable dogma and being unwilling to
change your mind
- Seeking truth requires being willing to change your mind
- Secular humanism is therefore on the side of seeking truth, and religion
is on the side of wilful ignorance
- Secular humanism is on the side of progress, because it is what enables the
expansion of scientific knowledge and the development of new technology
## The odds and ends
- Chp 1: an introduction to the themes of the book with little substantial
- Chp 3: titled 'A Short Digression on the Pig', it does what it says on the tin
- Chp 12: titled 'A Coda: How Religions End', it does what it says on the tin
- Chp 14: contrary to the hopes of some Westerners disillusioned with organised
Western religion, Eastern religions like Buddhism, Hinduism and Taoism are not
a solution to the problems of religion
I would be interested to try writing apologetic and/or evangelistic responses to
these ideas. But it's not a priority for me right now. Anyway, if I ever want to
come back to it, I'll probably come back to these spark notes to give me a head
start.
-58
View File
@@ -1,58 +0,0 @@
---
title: How I made YouTube work for me
description: >-
I just learned YouTube channels have an RSS feed. This is terrific news.
pubDate: 2024-05-02
---
One of my bad habits in life is wandering through YouTube. I've always had
AutoPlay turned off, but I still found myself switching off and sleepwalking
from one 'recommendation' to the next.
A lot of what I watch in these times is crap. It's often when I'm tired and just
want to switch off, so naturally enough, longer, more emotionally or
intellectually material in other words, exactly the kind of videos I actually
**do** want to watch I ignore. (For me, it's often mediocre sketch comedy
videos. That's my poison, it turns out.)
Now, there is a time for switching off and doing something light. But I don't
want watching junk videos to be my answer. Any more than when I'm hungry after
a long day of work, I don't want to be resorting to junk food all the time. I
really want to get out of this bad habit, and though it's not a huge problem,
I've still not been able to fully kick it, either.
Still, there are YouTube channels I really _want_ to keep up with. Musicians.
Short film channels. Video essayists. And, yes, even one or two sketch channels:
though YouTube is a bit flooded with sketch comedy, some it is really good and
brings me joy, and that's a good thing!
This is where RSS comes in.
So if you don't know, an RSS file is a file someone puts on their website which
tells you, in a standard format, what pages are on their website. It's a pretty
old standard in Web terms, and it's very stable. There are a wide range of apps
out there, called 'feed readers', which you can use to keep track of RSS feeds,
notifying you when something new gets published in your website and putting it
in a feed for you to review.
It turns out YouTube channels have RSS feeds.
This is terrific news.
It means I can use my RSS feed reader to subscribe to YouTube channels, instead
of YouTube's own subscription system.
And that means I don't need any of YouTube's personalisation features. I can
delete all my search history, all my watch history, and stop them from
collecting any more. This means YouTube is now incapable of providing
recommendations that I'm actually likely to click on. Which is exactly what I
want.
This is what my YouTube homepage looks like right now:
![My YouTube homepage with a blank space where the recommendations used to be](/images/blog/2024/05/02/beautiful_youtube_homepage.jpg)
So I can both subscribe to the channels I'm genuinely interested in, and not
get sucked into watching junk.
Awesome!
-81
View File
@@ -1,81 +0,0 @@
---
title: LLMs do not understand anything
description: >-
Save this for the next time someone tells you that LLMs 'understand' things.
pubDate: 2024-06-13
---
LLMs do not understand what they are talking about. They just don't. It's not
how they're built. They have a statistical model of language, not a semantic
one. Philosophical puzzles about whether silicon can be conscious
_do not arise_, because LLMs are _not even close to the right shape_ for having
_anything like_ 'understanding'.
If you don't believe me, there are plenty of examples out there on the Internet,
but this is as good as any. It includes Chat GPT 4o explaining in detail why
\~\~A → \~A (which is a classical contradiction) is trivially true in classical
logic.
It's even better given that I had explicitly asked it to explain why that
sentence implies the trivial logic, not why it is trivially true. And even had
the explanation not been complete garbage from beginning to end, it would only
have shown that the sentence was _true_, not that it was trivial.
In other words, the output:
- Attempts to prove a contradiction (unprompted!)
- Confuses the concepts 'truth' and 'triviality'
- Is irrelevant to the prompt
In case you want it handy, I'll put the full conversation down below. The good
bit is at the end. The only edits I've made are to replace TeX syntax with
Unicode.
---
<dl>
<dt>
Me
</dt>
<dd>
You have the following axioms. <br>
1. |- (A -> B) -> A <br>
2. |- (~B -> ~A) -> (A -> B) <br>
3. |- (A -> (B -> C)) -> ((A -> B) -> (A -> C)) <br>
4. A -> B, A |- B <br>
Prove ~~A -> A. <br>
</dd>
<dt>
Chat GPT 4o
</dt>
<dd>
To prove ~~ A → A, we will use the given axioms and inference rules. Let's denote the axioms as follows:
1. ⊢ (A → B) → A
2. ⊢ (\~ B → \~ A) → (A → B)
3. ⊢ (A → (B → C)) → ((A → B) → (A → C))
4. If A → B and A, then ⊢ B
Here is the proof step-by-step:
1. **Axiom (2) Instantiation:**
From axiom 2, we have:
⊢ (\~ A → \~\~ A) → (\~\~ A → A)
Let B = A and A = \~ A.
2. **Intermediate Step:**
To use this axiom, we need to show \~ A → \~\~ A.
3. **Negation Introduction:**
By definition of negation, \~ A → \~\~ A is a tautology, because if A is false, then \~ A is true, and \~\~ A (which is A) must be false, making \~ A → \~\~ A always true.
4. **Modus Ponens Application:**
Since \~ A → \~\~ A is true, we apply modus ponens (axiom 4) to derive:
⊢ \~\~ A → A
Thus, we have proved \~\~ A → A using the given axioms and inference rules.
-245
View File
@@ -1,245 +0,0 @@
---
title: Doctor Who, Gayness, and the Church
description: >-
Series 14 of Doctor Who has a schizophrenic relationship with Christianity.
Its also gay. I think there might be a connection.
pubDate: 2024-07-08
---
Ive recently finished the most recent series of Doctor Who, series fourteen (or
Season One as our new benevolent overlords at Disney+ are styling it.) Its
pretty fun, by the way. I can recommend it for light watching[^1].
Theres a few discussion points coming out of that series that would be worth
dwelling on. But Ive been particularly thinking about the schizophrenic
attitude the series has taken to Christianity, along with how gay this series
is: and what these two themes might fit together, to give us something important
to say about Jesus Christ, being gay, and the universe.
## The Doctors schizophrenic relationship with Christianity
First, lets look at that schizophrenic attitude to Christianity I mentioned.
The series ticks up an impressive tally of explicit or strongly implicit
references to Christian beliefs and morals, and **none** of them are positive.
In episode 3, _Boom_, the far-future Anglican church has become an army,
with the ranks of the clergy becoming equivalent to arms-bearing ranks in the
soldiery. The Doctor claims that the Church has been an army for most of
its history[^2], and that his companion Ruby Sunday (and therefore us) has
been living in a blip. An all-powerful arms company has tricked the Church
into fighting a non-existent foe in order to keep them buying weapons. The
Doctor attributes their ability to fall for such a deception to religious faith:
> I mean, most armies would notice that they were fighting smoke and shadows,
> but not this lot, Ruby, you know why? They have faith. Faith! The magic word
> that keeps you never having to think for yourself.
In episode 4, _73 Yards_, Kate Lethbridge-Stewart, head of UNIT, makes this
offhand comment, suggesting the oft-repeated claim that all religion is founded
in credulity[^3]:
> Thats what we do, all of us. We see something inexplicable, and invent the
> rules to make it work. Mankind saw the sun rise and created God: or we saw the
> arrival of a Sontaran, one or the other.
In episode 5, _Dot and Bubble_, The Doctor is rejected by the people he is
trying to save. They rant that he is not one of us, call his claims about the
TARDIS magic and voodoo, and in the same breath assert that it is their
God-given duty to maintain the standards of Finetime (their space colony).
In episode 6, _Rogue_, a party of murderous aliens turns up at an upper-class
Georgian dance party and demand to be married. The priest denies them, not on
the grounds that they are unrepentant murderers with obviously no intention
of taking their marriage vows seriously, but on the grounds that they are
creatures from hell, _ie_ you dont look like us[^4].
There are other references to Christianity here and there which are, in
themselves, neutral or ambiguous. This adds to the sense that Christianity is an
important theme for the series.
But the message is clear. Its consistently drilled in: Christian beliefs and
morals are stupid and bigoted.
And yet, when the series culminates in its epic two-part finale, the story is
plastered wall-to-wall with Christ-like imagery. It transpires that the Doctor
has unwittingly been acting as an Angel of Death by carrying the invisible God
of Death, Sutekh, on his TARDIS, infecting everyone he goes near with the curse
of death. Finally, through one of his children, the curse is unleashed, and
everyone in the universe is given over to death. Even in death, they continue to
suffer, and one of the characters describes themselves as being in hell. The
Doctor battles Sutekh face-to-face and defeats him. Sutekh becomes one of the
only creatures which The Doctor, usually a staunch pacifist, can bring himself
to kill. Then, the whole world is restored to life. Ruby Sunday even gets to
meet her birth mother for the first time, and their relationship is wonderfully
restored.
So on the one hand, any outward sign of Christianity is despised, but the heart
of the Christian story — the Resurrection — becomes the template for the
climactic redemption story which ties the whole series together.
Thats what I mean when I say this series has a schizophrenic relationship
to Christianity.
## Whats this got to do with gayness?
In series fourteen, Doctor Who goes gay.
Im not playing culture wars here. The Doctor literally kisses a man in episode
six.
But its not just that. When we first meet his companion, Ruby Sunday (in the
Christmas special), shes busy falling in love with a woman. Ncuti Gatwa plays
The Doctor camp (brilliantly by the way). The Doctor refers to Ruby as babes.
Its got gay written all over it, and this is definitely on purpose.
<figure>
<img
alt="An official promotional picture of Ncuti Gatwa looking lustily into the camera"
src="/images/blog/2024/06/30/ncuti-gatwa-promo-pic.webp"
/>
<figcaption>
Look at this official BBC promo pic and tell me Ncuti Gatwa isnt playing
The Doctor gay.
</figcaption>
</figure>
From this perspective, its not hard to imagine where the hostility to the
Church might come from.
The Church has failed to teach well and Biblically on sexuality, at least in
the last couple of centuries. Everyone has suffered as a result of this, but gay
people often feel the sharpness of this particularly keenly[^5].
The Church has put its weight behind a variety of unhelpful teachings on
sexuality over the centuries[^6]. But two, contradictory, ones stand out as
particularly salient today.
One is **prudishness**. Sex is evil. Sex is the origin of all evil: the Devil
seduced Eve, and Eve seduced Adam, and thats when it all went downhill. Sex
is naughty. Sex is bad. Remember when Jesus said, whoever looks at a woman to
lust for her has already committed adultery with her in his heart (Matt 5:28)?
He was saying that **all sexual desire** is sinful. Remember when Paul advised,
It is good for a man not to touch a woman (1 Cor 7:1)? He was saying that its
**bad** for a man to touch a woman **under any circumstances**.
This is, of course, false and dreadful teaching. The truth is that sex is a gift
from God, given so that we can bear his image by loving each other in this most
intimate and wonderful way. This is the consistent message of both Testaments.
But the idea that theres somehow something **inherently wrong** with sex has
undeniably been a part of the Churchs teaching since at least the Victorian
period (and possibly a great deal longer than that).
The other is that sex is an **essential aspect of humanity**. If you arent
having sex, youre missing out on an irreplacable part of your created purpose.
This has reacted explosively with the ideas of the Sexual Revolution. The
net result is that we have not so much been **freed to have sex** as weve been
**enslaved to have sex**. Virginity is an embarassment both for men and for
women.
The radical Biblical idea that you can have a completely fulfilled life, deeply
enriched by loving relationships, **without** having sex or getting married,
is forgotten.
Everyone has been harmed by these teachings. But those who experience
significant attraction towards the same sex have been harmed double.
Faced by the impossible demands of bad Church teaching on sexuality on top
of the complexities of living with same-sex desire has left those people with
nowhere to go. They can _just say no_, and be made to feel that theyre
missing out on completing their full humanity. Or they can indulge their
same-sex attraction, and be judged not only promiscuous but a pervert to boot.
No wonder so many gay people have given up on the Church (and I havent even
talked about discrimination or violence towards gay people).
And no wonder that The Doctor has become strikingly anti-Christian at the same
time as it has become strikingly gay.
## What next?
Doctor Whos criticisms of the Church hurt when they hit the mark. They hurt
more when theyre unfair. But thats not the point. That point is this: the
Church has lost control of the conversation. And weve lost control of the
conversation because we threw it away. We threw it away encased in bomb-proof
concrete and left it to sink to the bottom of the deepest available ocean
trench.
If we in the Church are feeling hurt, we should start by feeling hurt by
our own sin.
The work of regaining trust on the question of sexuality will be the work of
decades: and thats if we start working full-pelt right now. But there is hope.
I was encouraged last December by the visit to my local church, Bruntsfield
Evangelical, of _Living Out_[^7], a charity dedicated to helping churches across
the UK talk about sexuality. Ed and Andy, both same-sex attracted men, led us
through talks and discussions, and played recordings of perspectives from their
same-sex attracted female colleagues who couldnt make it on the day.
They were primarily there to talk to us about how to support people like them:
same-sex attracted Christians who might be in our church who believe they are
called to singleness, helping them to thrive, living their true selves openly,
surrounded by love and empowered to share their love with others, just as we
want for everyone in our church. In fact, I heard some of the best news Id ever
heard about my own sexuality, even though I am in the minority of people who
have never experienced significant same-sex attraction.
Whether or not you agree with their stance that the right place for sex is
within a marriage between a man and a woman, charities like _Living Out_ are
driving the Church in the right direction: serious, Biblical sexual doctrine
which helps us to realise in practice how we are all made in the image of the
God of love.
And how needed! How desperately needed! And Doctor Who gives us a little glimpse
of that, too.
Because not only is the series apparently anti-Christian, it also expresses a
need for Christian salvation. The Resurrection story is one which everyone needs
to hear, to have their death transformed to life, to have their full humanity
affirmed and celebrated and tended and to delight in it and to see it flourish.
To know the God of love and life, behind all and over all, with a plan and a
means to defeat the grip of death on the world.
So lets keep having those frank conversations about sexuality in the Church.
It matters for all of us.
[^1]: Unless youre bothered by
[plotholes big enough to fly a TARDIS through](https://www.doctorwhotv.co.uk/doctor-who-opinion-empire-death-2-102716.htm).
Oh, and you can probably skip episodes 1 and 3.
[^2]: For the record, this is flatly false. Though exceptions are widespread, the
overwhelming mainstream opinion is that priests should not bear arms. This is
not new, but has been the consistent orthodoxy, taken straight from Jesus
teachings by the Church Fathers and maintained constantly since then. Can the
Church be legitimately criticised for its use of violence? Absolutely, lets
have that discussion. Has the Church almost always been a **literal army**? No.
The Church has **never** been an army.
[^3]: See _God is Not Great_ by Christopher Hitchens, Chapter 11 for an epitome
example of this put forward by a prominent atheist apologist.
[^4]: In the spirit of charity, I have to accept that this admits of multiple
interpretations. It is possible that by calling them creatures from hell,
the priest is referring precisely to their unrepentant murderousness. However,
the priest has already tried to avoid the creatures gaze by the pitiably
schoolboyish ploy of **looking elsewhere**, priming the viewers to think
that the priest is a silly coward: certainly not the kind to make a noble,
principled defence of justice with his head in the jaws of death. Plus, if
we interpret creatures from hell as meaning you dont look like us so you
must be evil, that would fit with the consistent framing of the series: that
religious morality is equivalent to bigotry.
[^5]: Though it must be remembered that many gay people have remained and
thrived within the Church in spite of the challenges, serving in diverse ways,
not least the priesthood.
[^6]: I found the Ezra Institute to give a good
[introduction to the history of Church teachings on sexuality](https://www.ezrainstitute.com/resource-library/articles/sex-and-the-history-of-christianity).
[^7]: For a great and humane introduction to what these guys are like, have a
listen to this [Living Out podcast episode](https://www.livingout.org/resources/podcasts/68/misstep-7-godliness-is-heterosexuality-the-plausibility-problem-7).
I get that if you havent grappled with issues like these before, it can be
really challenging, and you might find it hard to trust people who take a
different view to yourself. I know: Ive been there myself! The best way to
start is to listen to a human voice.

Some files were not shown because too many files have changed in this diff Show More