Compare commits
14 Commits
8744ba58c9
...
b590a2ffef
| Author | SHA1 | Date | |
|---|---|---|---|
| b590a2ffef | |||
| 52b254ff59 | |||
| f78387413a | |||
| f17061b4cb | |||
| 00effae167 | |||
| e50b62afa2 | |||
| 6470d7f8bd | |||
| 5a56c572af | |||
| ac8011ddb1 | |||
| d98908ad5c | |||
| 6d58eee37f | |||
| fab6515531 | |||
| 60f6f4cd80 | |||
| 7792e96334 |
@@ -5,6 +5,7 @@
|
||||
"start": "astro build --remote && node ./dist/server/entry.mjs",
|
||||
"build": "astro build --remote",
|
||||
"preview": "astro preview",
|
||||
"sync": "astro sync",
|
||||
"astro": "astro"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
.h-feed .h-entry .p-name {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.h-feed .full-feed-link {
|
||||
|
||||
@@ -46,6 +46,7 @@ function pubDate(post: CollectionEntry<'blog'>): 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)
|
||||
@@ -72,7 +73,9 @@ const canonicalBlogUrl = new URL('blog', Astro.site)
|
||||
? <ul>
|
||||
{ posts.sort(sortByPubDateDescending).map(post => (
|
||||
<li class="h-entry">
|
||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||
<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>
|
||||
@@ -83,7 +86,9 @@ const canonicalBlogUrl = new URL('blog', Astro.site)
|
||||
<ul>
|
||||
{ posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => (
|
||||
<li class="h-entry">
|
||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||
<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>
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
interface Props {
|
||||
className?: string;
|
||||
date: Date | string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
let { className, date } = Astro.props;
|
||||
let { className, date, hidden } = Astro.props;
|
||||
if (typeof(date) === 'string') {
|
||||
date = new Date(date);
|
||||
}
|
||||
---
|
||||
|
||||
<time datetime={date.toISOString()} class={className ?? ''}>
|
||||
{
|
||||
date.toLocaleDateString('en-GB', {
|
||||
const dateStr =
|
||||
date.toLocaleDateString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</time>
|
||||
});
|
||||
---
|
||||
|
||||
{ hidden
|
||||
? <time datetime={date.toISOString()} class={className ?? ''} hidden>{dateStr}</time>
|
||||
: <time datetime={date.toISOString()} class={className ?? ''}>{dateStr}</time>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
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>
|
||||
@@ -9,6 +9,9 @@
|
||||
<li>
|
||||
<a href="/blog">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/microlog">Microlog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/links">Links</a>
|
||||
</li>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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({
|
||||
@@ -49,4 +50,12 @@ const blog = defineCollection({
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
const microlog = defineCollection({
|
||||
loader: glob({
|
||||
pattern: "**/*.gmi",
|
||||
base: "./src/content/microlog",
|
||||
generateId: ({ entry }) => entry.replace(/\.gmi$/, ""),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, microlog };
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../common/microlog/
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
import Navbar from '../components/Navbar.astro';
|
||||
@@ -19,7 +18,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={`${title} | joeac’s blog`} description={description} />
|
||||
<link rel="stylesheet" href="/css/blog.css">
|
||||
<link rel="stylesheet" href="/css/post.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
import Navbar from '../components/Navbar.astro';
|
||||
|
||||
type Props = {
|
||||
pubDate: Date;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { pubDate, title } = Astro.props;
|
||||
|
||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={`${title} | joeac’s microlog`} description={`${title} | joeac's microlog`} />
|
||||
<link rel="stylesheet" href="/css/post.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Navbar />
|
||||
<article class="h-entry">
|
||||
<aside>
|
||||
<span>
|
||||
This is a <a href="/microlog">microlog</a> post by
|
||||
<a class="p-author h-card" href="/">Joe Carstairs</a>.
|
||||
</span>
|
||||
<span>Published: <FormattedDate date={pubDate} className="dt-published"/>.</span>
|
||||
<span hidden><a class="u-url uid" href={canonicalUrl}>Permalink</a></span>
|
||||
</aside>
|
||||
|
||||
<header>
|
||||
<h1 class="h-name">{title}</h1>
|
||||
</header>
|
||||
|
||||
<section class="e-content">
|
||||
<slot />
|
||||
</section>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection, render } from 'astro:content';
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
import * as Gemtext from 'gemtext';
|
||||
import { renderGemtextToHtml } from '../../renderGemtextToHtml';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
@@ -12,57 +12,9 @@ export async function getStaticPaths() {
|
||||
}
|
||||
type Props = CollectionEntry<'blog'>;
|
||||
|
||||
function htmlEscape(str: string) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
const GemtextHTMLRenderer: Gemtext.Renderer<string> = {
|
||||
preamble: function (): string {
|
||||
return '';
|
||||
},
|
||||
postamble: function (): string {
|
||||
return '';
|
||||
},
|
||||
text: function (content: string): string {
|
||||
content = content.trim();
|
||||
if (content === '') {
|
||||
return '';
|
||||
}
|
||||
return `<p>${htmlEscape(content)}</p>`
|
||||
},
|
||||
link: function (url: string, alt: string): string {
|
||||
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>`;
|
||||
},
|
||||
preformatted: function (content: string[], alt: string): string {
|
||||
return `<pre alt="${alt}">${htmlEscape(content.join('\n'))}</pre>`
|
||||
},
|
||||
heading: function (level: number, text: string): string {
|
||||
return `<h${level}>${htmlEscape(text)}</h${level}>`;
|
||||
},
|
||||
unorderedList: function (content: string[]): string {
|
||||
return `<ul>${content.map(li=>`<li>${htmlEscape(li)}</li>`).join('')}</ul>`;
|
||||
},
|
||||
quote: function (content: string): string {
|
||||
return `<blockquote>${htmlEscape(content)}</blockquote>`;
|
||||
}
|
||||
};
|
||||
|
||||
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 Content = post.filePath?.endsWith('.gmi')
|
||||
? postProcessGemtextToHtml(Gemtext.parse((post.body ?? '').replace(`# ${post.data.title}`, '')).generate(GemtextHTMLRenderer))
|
||||
? renderGemtextToHtml(post.body ?? '', post.data.title)
|
||||
: (await render(post)).Content;
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection, render } from 'astro:content';
|
||||
import MicrologPost from '../../layouts/MicrologPost.astro';
|
||||
import { renderGemtextToHtml } from '../../renderGemtextToHtml';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('microlog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'microlog'>;
|
||||
|
||||
const { id: slug, body } = Astro.props;
|
||||
const pubDateStr = slug.replaceAll(/\.[0-9]+/g, '');
|
||||
const pubDate = new Date(pubDateStr);
|
||||
if (isNaN(pubDate.valueOf())) {
|
||||
throw new Error(`Could not construct a valid publication date for microlog post: ${slug}`);
|
||||
}
|
||||
const Content = renderGemtextToHtml(body ?? '', pubDateStr);
|
||||
---
|
||||
|
||||
<MicrologPost pubDate={pubDate} title={pubDateStr}>
|
||||
<Fragment set:html={Content}></Fragment>
|
||||
</MicrologPost>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
|
||||
import MicrologFeed from '../../components/MicrologFeed.astro';
|
||||
import Page from '../../layouts/Page.astro';
|
||||
---
|
||||
|
||||
<Page title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<MicrologFeed headingLevel={1}>
|
||||
<!-- <section>
|
||||
<p><a href="/blog/subscribe">How to subscribe to this blog</a></p>
|
||||
</section> -->
|
||||
</MicrologFeed>
|
||||
</Page>
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as Gemtext from "gemtext";
|
||||
|
||||
export function renderGemtextToHtml(gemtext: string, title?: string): string {
|
||||
gemtext = title ? gemtext.replace(`# ${title}`, "") : gemtext;
|
||||
return postProcessGemtextToHtml(
|
||||
Gemtext.parse(gemtext).generate(GemtextHTMLRenderer),
|
||||
);
|
||||
}
|
||||
|
||||
const GemtextHTMLRenderer: Gemtext.Renderer<string> = {
|
||||
preamble: function (): string {
|
||||
return "";
|
||||
},
|
||||
|
||||
postamble: function (): string {
|
||||
return "";
|
||||
},
|
||||
|
||||
text: function (content: string): string {
|
||||
content = content.trim();
|
||||
if (content === "") {
|
||||
return "";
|
||||
}
|
||||
return `<p>${htmlEscape(content)}</p>`;
|
||||
},
|
||||
|
||||
link: function (url: string, alt: string): string {
|
||||
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>`;
|
||||
},
|
||||
|
||||
preformatted: function (content: string[], alt: string): string {
|
||||
return `<pre alt="${alt}">${htmlEscape(content.join("\n"))}</pre>`;
|
||||
},
|
||||
|
||||
heading: function (level: number, text: string): string {
|
||||
return `<h${level}>${htmlEscape(text)}</h${level}>`;
|
||||
},
|
||||
|
||||
unorderedList: function (content: string[]): string {
|
||||
return `<ul>${content.map((li) => `<li>${htmlEscape(li)}</li>`).join("")}</ul>`;
|
||||
},
|
||||
|
||||
quote: function (content: string): string {
|
||||
return `<blockquote>${htmlEscape(content)}</blockquote>`;
|
||||
},
|
||||
};
|
||||
|
||||
function htmlEscape(str: string) {
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function postProcessGemtextToHtml(html: string): string {
|
||||
html = mergeAdjacentBlockquotes(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
function mergeAdjacentBlockquotes(html: string): string {
|
||||
return html.replaceAll(/\<\/blockquote\>\<blockquote\>/gm, "<br>");
|
||||
}
|
||||
Reference in New Issue
Block a user