Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e193ce86f | |||
| 4198ceb4b3 | |||
| ef451a063b | |||
| 1a64bce2fb | |||
| cf6dbc2aa1 |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 914 KiB After Width: | Height: | Size: 914 KiB |
@@ -27,13 +27,13 @@ It also hasn't been any good for distinguishing between stuff I don't want to re
|
|||||||
Every morning, I open my terminal and run newsboat.
|
Every morning, I open my terminal and run newsboat.
|
||||||
|
|
||||||
=> https://newsboat.org
|
=> https://newsboat.org
|
||||||
=> ./2025-01-19_newsboat.webp newsboat showing how many unread posts I have at a glance in the opening view
|
=> /images/longlog/2025-01-19_newsboat.webp newsboat showing how many unread posts I have at a glance in the opening view
|
||||||
|
|
||||||
I know the unread count is pretty fresh, because I've set up a systemd service to run newsboat at startup to fetch the feeds.
|
I know the unread count is pretty fresh, because I've set up a systemd service to run newsboat at startup to fetch the feeds.
|
||||||
|
|
||||||
I press `l` twice to open a post. Then I press `n` to navigate to the next unread post until I run out of unread posts.
|
I press `l` twice to open a post. Then I press `n` to navigate to the next unread post until I run out of unread posts.
|
||||||
|
|
||||||
=> ./2025-01-19_newsboat-post.webp newsboat displaying a post
|
=> /images/longlog/2025-01-19_newsboat-post.webp newsboat displaying a post
|
||||||
|
|
||||||
If I encounter something I want to read later, but don't have time right now, I press `b`, which runs a home-made bookmarking script. Here it is:
|
If I encounter something I want to read later, but don't have time right now, I press `b`, which runs a home-made bookmarking script. Here it is:
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ I can also run this script manually and pass it a URL of my choice at any time,
|
|||||||
When I want to read from my reading list, I run `readnow.sh`, which simply opens my reading list folder, `~/readlist/unread`, in my terminal file browser of choice: namely, ranger.
|
When I want to read from my reading list, I run `readnow.sh`, which simply opens my reading list folder, `~/readlist/unread`, in my terminal file browser of choice: namely, ranger.
|
||||||
|
|
||||||
=> https://ranger.github.io
|
=> https://ranger.github.io
|
||||||
=> ./2025-01-19_ranger.webp ranger showing the contents of my reading list with a preview
|
=> /images/longlog/2025-01-19_ranger.webp ranger showing the contents of my reading list with a preview
|
||||||
|
|
||||||
Although ranger has a preview, I'll typically open the file up in my terminal web browser of choice, which is w3m (plus a couple of custom key-bindings).
|
Although ranger has a preview, I'll typically open the file up in my terminal web browser of choice, which is w3m (plus a couple of custom key-bindings).
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ I've configured this to be my preferred web browser in ranger by shifting it to
|
|||||||
|
|
||||||
Having configured my default web browser in my ranger config, all I need to do is press `l`.
|
Having configured my default web browser in my ranger config, all I need to do is press `l`.
|
||||||
|
|
||||||
=> ./2025-01-19_w3m.webp A post displaying in w3m
|
=> /images/longlog/2025-01-19_w3m.webp A post displaying in w3m
|
||||||
|
|
||||||
No ads, no cookie popups, no giant banner images taking 2 seconds to load and shifting the content all over the place: just the text I want to read. Isn't it beautiful?
|
No ads, no cookie popups, no giant banner images taking 2 seconds to load and shifting the content all over the place: just the text I want to read. Isn't it beautiful?
|
||||||
|
|
||||||
@@ -134,4 +134,3 @@ I still need to know:
|
|||||||
* What's the best way of sending and receiving comments/replies/reactions?
|
* What's the best way of sending and receiving comments/replies/reactions?
|
||||||
|
|
||||||
TBC. Answers on a postcard please.
|
TBC. Answers on a postcard please.
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Where green apocalypse succeeds, it succeeds by delving into the riches of myth-
|
|||||||
|
|
||||||
In ‘The Lost Words,’ apparently ordinary characters from the British environment – as ordinary as dandelions, acorns, and ferns – are depicted in beautiful illustrated portraits on gold leaf, in the manner of a religious icon. In the pages in-between, illustrations of fields, thickets, and moors are scattered with a jumble of golden letters, waiting to be assembled. Each icon is accompanied by an acrostic poem. Consider ‘Bramble’:
|
In ‘The Lost Words,’ apparently ordinary characters from the British environment – as ordinary as dandelions, acorns, and ferns – are depicted in beautiful illustrated portraits on gold leaf, in the manner of a religious icon. In the pages in-between, illustrations of fields, thickets, and moors are scattered with a jumble of golden letters, waiting to be assembled. Each icon is accompanied by an acrostic poem. Consider ‘Bramble’:
|
||||||
|
|
||||||
=> /logs/longlong/2026-04-12_bramble.webp ‘Bramble’ icon
|
=> /images/longlog/2026-04-12_bramble.webp ‘Bramble’ icon
|
||||||
|
|
||||||
> Bramble is on the march again,
|
> Bramble is on the march again,
|
||||||
> Rolling and arching along the hedges,
|
> Rolling and arching along the hedges,
|
||||||
@@ -79,7 +79,7 @@ In ‘The Lost Words,’ apparently ordinary characters from the British environ
|
|||||||
|
|
||||||
Picture it! The human world tied up, people pushed out. Bramble barging through, conquering cities, streets, houses. Bramble is on the march again. It is decisively not fact-speak. It is imaginative – myth-speak, perhaps?
|
Picture it! The human world tied up, people pushed out. Bramble barging through, conquering cities, streets, houses. Bramble is on the march again. It is decisively not fact-speak. It is imaginative – myth-speak, perhaps?
|
||||||
|
|
||||||
You could also study ‘Weasel,’ who ‘acts on land like spark on tinder – / Scorches grass, turns fields to pyre, sand to glass, tree to cinder.’ Or there is willow, the wise one, who will never, can never, share willow-wisdom with us: ‘ou will never know a word of willow – for we are willow and you are not.’
|
You could also study ‘Weasel,’ who ‘acts on land like spark on tinder – / Scorches grass, turns fields to pyre, sand to glass, tree to cinder.’ Or there is willow, the wise one, who will never, can never, share willow-wisdom with us: ‘you will never know a word of willow – for we are willow and you are not.’
|
||||||
|
|
||||||
My thoughts are not quite there yet, but I sense there’s something here. We’re not just being given a scientific account of bramble, weasel, willow. We’re being given more than that – a mythic account, peeling back ordinary reality to find something more precious behind. Can this refocus our eyes on what really matters? Can this transcend eco-anxiety? Can this ground hope instead of despair?
|
My thoughts are not quite there yet, but I sense there’s something here. We’re not just being given a scientific account of bramble, weasel, willow. We’re being given more than that – a mythic account, peeling back ordinary reality to find something more precious behind. Can this refocus our eyes on what really matters? Can this transcend eco-anxiety? Can this ground hope instead of despair?
|
||||||
|
|
||||||
@@ -94,4 +94,3 @@ Since I have fixed the concept ‘apocalypse’ into the scope of my project, th
|
|||||||
Therefore, my project amounts to an attempt to explain (at least) one way in which the Apocalypse might have once grounded hope instead of despair, and then to explore how green stories can do something similar today.
|
Therefore, my project amounts to an attempt to explain (at least) one way in which the Apocalypse might have once grounded hope instead of despair, and then to explore how green stories can do something similar today.
|
||||||
|
|
||||||
I’ll be handing this in on the 5th of August. Wish me luck!
|
I’ll be handing this in on the 5th of August. Wish me luck!
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ services:
|
|||||||
|
|
||||||
website:
|
website:
|
||||||
build:
|
build:
|
||||||
context: website
|
context: .
|
||||||
|
dockerfile: website.Dockerfile
|
||||||
args:
|
args:
|
||||||
LOCAL_SMTP_HOST: smtp
|
LOCAL_SMTP_HOST: smtp
|
||||||
LOCAL_SMTP_PORT: 2500
|
LOCAL_SMTP_PORT: 2500
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ FROM node:lts-alpine3.22
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
COPY package.json package-lock.json ./
|
COPY website/package.json website/package-lock.json ./
|
||||||
RUN npm install && apk del git
|
RUN npm install && apk del git
|
||||||
|
|
||||||
COPY astro.config.mjs redirects.mjs ./
|
COPY website/astro.config.mjs website/redirects.mjs ./
|
||||||
COPY db ./db/
|
COPY website/db ./db/
|
||||||
ARG DB_URL=file:/app/db.sqlite
|
ARG DB_URL=file:/app/db.sqlite
|
||||||
ENV ASTRO_DB_REMOTE_URL=$DB_URL
|
ENV ASTRO_DB_REMOTE_URL=$DB_URL
|
||||||
RUN mkdir -p "$(dirname "$(echo "$ASTRO_DB_REMOTE_URL" | cut -d':' -f 2)")"
|
RUN mkdir -p "$(dirname "$(echo "$ASTRO_DB_REMOTE_URL" | cut -d':' -f 2)")"
|
||||||
RUN npm run astro db push
|
RUN npm run astro db push
|
||||||
|
|
||||||
COPY . .
|
COPY website .
|
||||||
|
|
||||||
|
# Some capsule content is symlinked into the website, so
|
||||||
|
# we need to copy over the targets of the symlinks
|
||||||
|
RUN mkdir -p /capsule/content/logs
|
||||||
|
COPY capsule/content/logs/longlog /capsule/content/logs/longlog
|
||||||
|
COPY capsule/content/images /capsule/content/images
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=4321
|
ENV PORT=4321
|
||||||
@@ -23,8 +29,8 @@ ARG LOCAL_SMTP_PASSWORD
|
|||||||
ARG LOCAL_SMTP_PORT
|
ARG LOCAL_SMTP_PORT
|
||||||
#ENV LOCAL_SMTP_ENVELOPE_FROM=$LOCAL_SMTP_ENVELOPE_FROM \
|
#ENV LOCAL_SMTP_ENVELOPE_FROM=$LOCAL_SMTP_ENVELOPE_FROM \
|
||||||
#ENV LOCAL_SMTP_USER=$LOCAL_SMTP_USER \
|
#ENV LOCAL_SMTP_USER=$LOCAL_SMTP_USER \
|
||||||
#LOCAL_SMTP_HOST=$LOCAL_SMTP_HOST \
|
#LOCAL_SMTP_HOST=$LOCAL_SMTP_HOST \
|
||||||
#LOCAL_SMTP_PASSWORD=$LOCAL_SMTP_PASSWORD \
|
#LOCAL_SMTP_PASSWORD=$LOCAL_SMTP_PASSWORD \
|
||||||
# LOCAL_SMTP_PORT=$LOCAL_SMTP_PORT \
|
# LOCAL_SMTP_PORT=$LOCAL_SMTP_PORT \
|
||||||
# REMOTE_SMTP_HOST=$REMOTE_SMTP_HOST \
|
# REMOTE_SMTP_HOST=$REMOTE_SMTP_HOST \
|
||||||
# REMOTE_SMTP_PORT=$REMOTE_SMTP_PORT \
|
# REMOTE_SMTP_PORT=$REMOTE_SMTP_PORT \
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../capsule/content/images/
|
||||||
@@ -19,20 +19,31 @@ const posts = maxEntries === undefined
|
|||||||
: allPosts.sort(sortByPubDateDescending).slice(0, maxEntries);
|
: allPosts.sort(sortByPubDateDescending).slice(0, maxEntries);
|
||||||
|
|
||||||
const distinctYears: number[] = posts
|
const distinctYears: number[] = posts
|
||||||
.map(post => post.data.pubDate.getFullYear())
|
.map(post => pubDate(post).getFullYear())
|
||||||
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
|
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
|
||||||
.sort((a, b) => b - a);
|
.sort((a, b) => b - a);
|
||||||
|
|
||||||
function matchesYear(year: number) {
|
function matchesYear(year: number) {
|
||||||
return (post: CollectionEntry<'blog'>) => post.data.pubDate.getFullYear() === year;
|
return (post: CollectionEntry<'blog'>) => pubDate(post).getFullYear() === year;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: CollectionEntry<'blog'>) {
|
function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: CollectionEntry<'blog'>) {
|
||||||
const date1 = post1.data.pubDate.getTime();
|
const date1 = pubDate(post1).getTime();
|
||||||
const date2 = post2.data.pubDate.getTime();
|
const date2 = pubDate(post2).getTime();
|
||||||
return date2 - date1;
|
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 HeadingElem = `h${headingLevel} class="p-name"`;
|
||||||
const SubHeadingElem = `h${headingLevel + 1}`;
|
const SubHeadingElem = `h${headingLevel + 1}`;
|
||||||
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
|
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
|
||||||
@@ -61,7 +72,7 @@ const canonicalBlogUrl = new URL('blog', Astro.site)
|
|||||||
<li class="h-entry">
|
<li class="h-entry">
|
||||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||||
<p class="p-summary" set:html={post.data.description} />
|
<p class="p-summary" set:html={post.data.description} />
|
||||||
<FormattedDate className="dt-published" date={post.data.pubDate} />
|
<FormattedDate className="dt-published" date={pubDate(post)} />
|
||||||
</li>
|
</li>
|
||||||
)) }
|
)) }
|
||||||
</ul>
|
</ul>
|
||||||
@@ -72,7 +83,7 @@ const canonicalBlogUrl = new URL('blog', Astro.site)
|
|||||||
<li class="h-entry">
|
<li class="h-entry">
|
||||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||||
<p class="p-summary" set:html={post.data.description} />
|
<p class="p-summary" set:html={post.data.description} />
|
||||||
<FormattedDate className="dt-published" date={post.data.pubDate} />
|
<FormattedDate className="dt-published" date={pubDate(post)} />
|
||||||
</li>
|
</li>
|
||||||
)) }
|
)) }
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const blog = defineCollection({
|
|||||||
loader: extendedGlob({
|
loader: extendedGlob({
|
||||||
pattern: "**/*.(md|mdx|gmi|html)",
|
pattern: "**/*.(md|mdx|gmi|html)",
|
||||||
base: "./src/content/blog",
|
base: "./src/content/blog",
|
||||||
postprocessSlug: (slug: string) => slug.replaceAll(/^gemlog\//g, ""),
|
postprocessSlug: (slug: string) => slug.replaceAll(/^capsule-longlog\//g, ""),
|
||||||
ignore: [
|
ignore: [
|
||||||
"capsule-longlog/index.gmi",
|
"capsule-longlog/index.gmi",
|
||||||
/* Some blog posts were ported to gemtext. Don't port them back again:
|
/* Some blog posts were ported to gemtext. Don't port them back again:
|
||||||
@@ -44,7 +44,7 @@ const blog = defineCollection({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
hidden: z.optional(z.boolean()),
|
hidden: z.optional(z.boolean()),
|
||||||
description: z.optional(z.string()),
|
description: z.optional(z.string()),
|
||||||
pubDate: z.date(),
|
pubDate: z.optional(z.date()),
|
||||||
updatedDate: z.optional(z.date()),
|
updatedDate: z.optional(z.date()),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import BaseHead from '../components/BaseHead.astro';
|
|||||||
import FormattedDate from '../components/FormattedDate.astro';
|
import FormattedDate from '../components/FormattedDate.astro';
|
||||||
import Navbar from '../components/Navbar.astro';
|
import Navbar from '../components/Navbar.astro';
|
||||||
|
|
||||||
type Props = CollectionEntry<'blog'>['data'];
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
updatedDate?: Date;
|
||||||
|
pubDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const { title, description, pubDate, updatedDate } = Astro.props;
|
const { title, description, pubDate, updatedDate } = Astro.props;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ const GemtextHTMLRenderer: Gemtext.Renderer<string> = {
|
|||||||
return `<p>${htmlEscape(content)}</p>`
|
return `<p>${htmlEscape(content)}</p>`
|
||||||
},
|
},
|
||||||
link: function (url: string, alt: string): string {
|
link: function (url: string, alt: string): string {
|
||||||
|
url = url.replaceAll(/^\/images\//g, '/images/capsule/');
|
||||||
|
const imgExtensions: (string | undefined)[] = ['webp', 'png', 'svg', 'gif', 'jpg', 'jpeg', 'apng']
|
||||||
|
if (imgExtensions.includes(url.split('.').at(-1)?.toLowerCase())) {
|
||||||
|
return `<img src="${url}" alt="${alt}" />`;
|
||||||
|
}
|
||||||
return `<p>=> <a href="${url}">${alt}</a></p>`;
|
return `<p>=> <a href="${url}">${alt}</a></p>`;
|
||||||
},
|
},
|
||||||
preformatted: function (content: string[], alt: string): string {
|
preformatted: function (content: string[], alt: string): string {
|
||||||
@@ -47,12 +52,26 @@ const GemtextHTMLRenderer: Gemtext.Renderer<string> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function postProcessGemtextToHtml(html: string): string {
|
||||||
|
html = mergeAdjacentBlockquotes(html);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAdjacentBlockquotes(html: string): string {
|
||||||
|
return html.replaceAll(/\<\/blockquote\>\<blockquote\>/gm, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
const post = Astro.props;
|
const post = Astro.props;
|
||||||
const Content = post.filePath?.endsWith('.gmi')
|
const Content = post.filePath?.endsWith('.gmi')
|
||||||
? Gemtext.parse((post.body ?? '').replace(`# ${post.data.title}`, '')).generate(GemtextHTMLRenderer)
|
? postProcessGemtextToHtml(Gemtext.parse((post.body ?? '').replace(`# ${post.data.title}`, '')).generate(GemtextHTMLRenderer))
|
||||||
: (await render(post)).Content;
|
: (await render(post)).Content;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogPost {...post.data}>
|
<BlogPost
|
||||||
|
title={post.data.title}
|
||||||
|
description={post.data.description}
|
||||||
|
pubDate={post.data.pubDate ?? new Date(post.id)}
|
||||||
|
updatedDate={post.data.updatedDate}
|
||||||
|
>
|
||||||
{ post.filePath?.endsWith('.gmi') ? <Fragment set:html={Content}></Fragment> : <Content />}
|
{ post.filePath?.endsWith('.gmi') ? <Fragment set:html={Content}></Fragment> : <Content />}
|
||||||
</BlogPost>
|
</BlogPost>
|
||||||
|
|||||||