Utilize 11ty to build static pages and modify server to serve them

This commit is contained in:
Gordon Pedersen 2023-09-27 14:13:10 +10:00
parent 283220111f
commit b76eb76e1a
10 changed files with 97 additions and 34 deletions

10
.eleventy.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = function(eleventyConfig) {
eleventyConfig.setUseGitIgnore(false)
// Return your Object options:
return {
dir: {
input: "_content",
output: "_site"
}
}
};

1
.gitignore vendored
View file

@ -170,3 +170,4 @@ dist
# My custom ignores # My custom ignores
_content _content
_site

BIN
bun.lockb

Binary file not shown.

View file

@ -14,6 +14,7 @@
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@11ty/eleventy": "^2.0.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"node-forge": "^1.3.1" "node-forge": "^1.3.1"
} }

View file

@ -8,7 +8,7 @@ export default (req: Request): Response | Promise<Response> | undefined => {
const url = new URL(req.url) const url = new URL(req.url)
let match let match
if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 }) if(!reqIsActivityPub(req)) return undefined
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1]) // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1])
@ -26,7 +26,8 @@ export default (req: Request): Response | Promise<Response> | undefined => {
else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT) else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1]) else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, ACCOUNT, match[1]) else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getOutboxActivity(req, ACCOUNT, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/([^\/]+)\/?$/i))) return getOutboxActivity(req, ACCOUNT, match[1])
return undefined return undefined
} }
@ -190,13 +191,13 @@ const getPost = async (req:Request, account:string, id:string):Promise<Response>
console.log("GetPost", account, id) console.log("GetPost", account, id)
if (ACCOUNT !== account) return new Response("", { status: 404 }) if (ACCOUNT !== account) return new Response("", { status: 404 })
if(reqIsActivityPub(req)) return Response.json((await db.getActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}}) if(reqIsActivityPub(req)) return Response.json((await db.getOutboxActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}})
else return Response.json(await db.getPost(id)) else return Response.json(await db.getPost(id))
} }
const getActivity = async (req:Request, account:string, id:string):Promise<Response> => { const getOutboxActivity = async (req:Request, account:string, id:string):Promise<Response> => {
console.log("GetActivity", account, id) console.log("GetOutboxActivity", account, id)
if (ACCOUNT !== account) return new Response("", { status: 404 }) if (ACCOUNT !== account) return new Response("", { status: 404 })
return Response.json((await db.getActivity(id)), { headers: { "Content-Type": "application/activity+json"}}) return Response.json((await db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
} }

View file

@ -1,6 +1,6 @@
import { idsFromValue } from "./activitypub" import { idsFromValue } from "./activitypub"
import * as db from "./db" import * as db from "./db"
import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL } from "./env" import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL, CONTENT_PATH, STATIC_PATH } from "./env"
import outbox from "./outbox" import outbox from "./outbox"
import { activityMimeTypes, fetchObject } from "./request" import { activityMimeTypes, fetchObject } from "./request"
@ -14,6 +14,7 @@ export default (req: Request): Response | Promise<Response> | undefined => {
let match let match
if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 }) if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 })
else if(req.method == "POST" && (match = url.pathname.match(/^\/rebuild\/?$/i))) return rebuild(req)
else if(req.method == "POST" && (match = url.pathname.match(/^\/create\/?$/i))) return create(req) else if(req.method == "POST" && (match = url.pathname.match(/^\/create\/?$/i))) return create(req)
else if(req.method == "POST" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return follow(req, match[1]) else if(req.method == "POST" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return follow(req, match[1])
else if(req.method == "DELETE" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return unfollow(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return unfollow(req, match[1])
@ -41,6 +42,12 @@ const checkAuth = (headers: Headers): Boolean => {
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD return username === ADMIN_USERNAME && password === ADMIN_PASSWORD
} }
// rebuild the 11ty static pages
export const rebuild = async(req:Request):Promise<Response> => {
await db.rebuild()
return new Response("", { status: 201 })
}
// create an activity // create an activity
const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => { const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => {
const body = await req.json() const body = await req.json()

View file

@ -1,21 +1,16 @@
import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, ACTIVITY_PATH, ACTOR, BASE_URL, DATA_PATH, POSTS_PATH } from "./env"; import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, ACTOR, CONTENT_PATH, DATA_PATH, POSTS_PATH, STATIC_PATH } from "./env";
import path from "path" import path from "path"
import { readdir } from "fs/promises" import { readdir } from "fs/promises"
import { unlinkSync } from "node:fs" import { unlinkSync } from "node:fs"
import { fetchObject } from "./request"; import { fetchObject } from "./request";
import { idsFromValue } from "./activitypub"; import { idsFromValue } from "./activitypub";
const matter = require('gray-matter') const matter = require('gray-matter')
const Eleventy = require("@11ty/eleventy")
export async function doActivity(activity:any, object_id:string|null|undefined) { // rebuild the 11ty static pages
if(activity.type === "Create" && activity.object) { export async function rebuild() {
if(!object_id) object_id = new Date(activity.object.published).getTime().toString(16) console.info(`Building 11ty from ${CONTENT_PATH}, to ${STATIC_PATH}`)
const file = Bun.file(path.join(POSTS_PATH, `${object_id}.md`)) await new Eleventy(CONTENT_PATH, STATIC_PATH, { configPath: '.eleventy.js' }).write()
const { content, published, id, attributedTo } = activity.object
//TODO: add appropriate content for different types (e.g. like-of, etc)
await Bun.write(file, matter.stringify(content || "", { id, published, attributedTo }))
const activityFile = Bun.file(path.join(ACTIVITY_PATH, `${object_id}.activity.json`))
await Bun.write(activityFile, JSON.stringify(activity))
}
} }
export async function createInboxActivity(activity:any, object_id:any) { export async function createInboxActivity(activity:any, object_id:any) {
@ -82,6 +77,7 @@ export async function createPost(post_object:any, object_id:string) {
if(inReplyTo) data.inReplyTo = idsFromValue(inReplyTo).at(0) if(inReplyTo) data.inReplyTo = idsFromValue(inReplyTo).at(0)
await Bun.write(file, matter.stringify((reply_content || "") + (content || ""), data)) await Bun.write(file, matter.stringify((reply_content || "") + (content || ""), data))
} }
rebuild()
} }
export async function getPost(id:string) { export async function getPost(id:string) {
@ -95,7 +91,7 @@ export async function getPost(id:string) {
} }
export async function getPostByURL(url_id:string) { export async function getPostByURL(url_id:string) {
if(!url_id || !url_id.startsWith(ACTOR + '/post/')) return null if(!url_id || !url_id.startsWith(ACTOR + '/posts/')) return null
const match = url_id.match(/\/([0-9a-f]+)\/?$/) const match = url_id.match(/\/([0-9a-f]+)\/?$/)
const local_id = match ? match[1] : url_id const local_id = match ? match[1] : url_id
return await getPost(local_id) return await getPost(local_id)
@ -103,28 +99,26 @@ export async function getPostByURL(url_id:string) {
export async function deletePost(id:string) { export async function deletePost(id:string) {
unlinkSync(path.join(POSTS_PATH, id + '.md')) unlinkSync(path.join(POSTS_PATH, id + '.md'))
rebuild()
} }
export async function listPosts() { export async function listPosts() {
return await Promise.all((await readdir(POSTS_PATH)).filter(v => v.endsWith('.md')).map(async filename => await getPost(filename.slice(0, -3)))) return await Promise.all((await readdir(POSTS_PATH)).filter(v => v.endsWith('.md')).map(async filename => await getPost(filename.slice(0, -3))))
} }
export async function getActivity(id:string) {
const file = Bun.file(path.join(ACTIVITY_PATH, `${id}.activity.json`))
return await file.json()
}
export async function createFollowing(handle:string, id:string) { export async function createFollowing(handle:string, id:string) {
const file = Bun.file(path.join(DATA_PATH, `following.json`)) const file = Bun.file(path.join(DATA_PATH, `following.json`))
const following_list = await file.json() as Array<any> const following_list = await file.json() as Array<any>
if(!following_list.find(v => v.id === id || v.handle === handle)) following_list.push({id, handle, createdAt: new Date().toISOString()}) if(!following_list.find(v => v.id === id || v.handle === handle)) following_list.push({id, handle, createdAt: new Date().toISOString()})
await Bun.write(file, JSON.stringify(following_list)) await Bun.write(file, JSON.stringify(following_list))
rebuild()
} }
export async function deleteFollowing(handle:string) { export async function deleteFollowing(handle:string) {
const file = Bun.file(path.join(DATA_PATH, `following.json`)) const file = Bun.file(path.join(DATA_PATH, `following.json`))
const following_list = await file.json() as Array<any> const following_list = await file.json() as Array<any>
await Bun.write(file, JSON.stringify(following_list.filter(v => v.handle !== handle))) await Bun.write(file, JSON.stringify(following_list.filter(v => v.handle !== handle)))
rebuild()
} }
export async function getFollowing(handle:string) { export async function getFollowing(handle:string) {
@ -144,6 +138,7 @@ export async function acceptFollowing(handle:string) {
const following = following_list.find(v => v.handle === handle) const following = following_list.find(v => v.handle === handle)
if(following) following.accepted = new Date().toISOString() if(following) following.accepted = new Date().toISOString()
await Bun.write(file, JSON.stringify(following_list)) await Bun.write(file, JSON.stringify(following_list))
rebuild()
} }
export async function createFollower(actor:string, id:string) { export async function createFollower(actor:string, id:string) {
@ -151,12 +146,14 @@ export async function createFollower(actor:string, id:string) {
const followers_list = await file.json() as Array<any> const followers_list = await file.json() as Array<any>
if(!followers_list.find(v => v.id === id || v.actor === actor)) followers_list.push({id, actor, createdAt: new Date().toISOString()}) if(!followers_list.find(v => v.id === id || v.actor === actor)) followers_list.push({id, actor, createdAt: new Date().toISOString()})
await Bun.write(file, JSON.stringify(followers_list)) await Bun.write(file, JSON.stringify(followers_list))
rebuild()
} }
export async function deleteFollower(actor:string) { export async function deleteFollower(actor:string) {
const file = Bun.file(path.join(DATA_PATH, `followers.json`)) const file = Bun.file(path.join(DATA_PATH, `followers.json`))
const followers_list = await file.json() as Array<any> const followers_list = await file.json() as Array<any>
await Bun.write(file, JSON.stringify(followers_list.filter(v => v.actor !== actor))) await Bun.write(file, JSON.stringify(followers_list.filter(v => v.actor !== actor)))
rebuild()
} }
export async function getFollower(actor:string) { export async function getFollower(actor:string) {
@ -175,12 +172,14 @@ export async function createLiked(object_id:string, id:string) {
const liked_list = await file.json() as Array<any> const liked_list = await file.json() as Array<any>
if(!liked_list.find(v => v.object_id === object_id)) liked_list.push({id, object_id, createdAt: new Date().toISOString()}) if(!liked_list.find(v => v.object_id === object_id)) liked_list.push({id, object_id, createdAt: new Date().toISOString()})
await Bun.write(file, JSON.stringify(liked_list)) await Bun.write(file, JSON.stringify(liked_list))
rebuild()
} }
export async function deleteLiked(object_id:string) { export async function deleteLiked(object_id:string) {
const file = Bun.file(path.join(DATA_PATH, `liked.json`)) const file = Bun.file(path.join(DATA_PATH, `liked.json`))
const liked_list = await file.json() as Array<any> const liked_list = await file.json() as Array<any>
await Bun.write(file, JSON.stringify(liked_list.filter(v => v.object_id !== object_id))) await Bun.write(file, JSON.stringify(liked_list.filter(v => v.object_id !== object_id)))
rebuild()
} }
export async function listLiked() { export async function listLiked() {
@ -193,12 +192,14 @@ export async function createDisliked(object_id:string, id:string) {
const disliked_list = await file.json() as Array<any> const disliked_list = await file.json() as Array<any>
if(!disliked_list.find(v => v.object_id === object_id)) disliked_list.push({id, object_id, createdAt: new Date().toISOString()}) if(!disliked_list.find(v => v.object_id === object_id)) disliked_list.push({id, object_id, createdAt: new Date().toISOString()})
await Bun.write(file, JSON.stringify(disliked_list)) await Bun.write(file, JSON.stringify(disliked_list))
rebuild()
} }
export async function deleteDisliked(object_id:string) { export async function deleteDisliked(object_id:string) {
const file = Bun.file(path.join(DATA_PATH, `disliked.json`)) const file = Bun.file(path.join(DATA_PATH, `disliked.json`))
const disliked_list = await file.json() as Array<any> const disliked_list = await file.json() as Array<any>
await Bun.write(file, JSON.stringify(disliked_list.filter(v => v.object_id !== object_id))) await Bun.write(file, JSON.stringify(disliked_list.filter(v => v.object_id !== object_id)))
rebuild()
} }
export async function listDisliked() { export async function listDisliked() {
@ -211,12 +212,14 @@ export async function createShared(object_id:string, id:string) {
const shared_list = await file.json() as Array<any> const shared_list = await file.json() as Array<any>
if(!shared_list.find(v => v.object_id === object_id)) shared_list.push({id, object_id, createdAt: new Date().toISOString()}) if(!shared_list.find(v => v.object_id === object_id)) shared_list.push({id, object_id, createdAt: new Date().toISOString()})
await Bun.write(file, JSON.stringify(shared_list)) await Bun.write(file, JSON.stringify(shared_list))
rebuild()
} }
export async function deleteShared(object_id:string) { export async function deleteShared(object_id:string) {
const file = Bun.file(path.join(DATA_PATH, `shared.json`)) const file = Bun.file(path.join(DATA_PATH, `shared.json`))
const shared_list = await file.json() as Array<any> const shared_list = await file.json() as Array<any>
await Bun.write(file, JSON.stringify(shared_list.filter(v => v.object_id !== object_id))) await Bun.write(file, JSON.stringify(shared_list.filter(v => v.object_id !== object_id)))
rebuild()
} }
export async function listShared() { export async function listShared() {

View file

@ -2,16 +2,16 @@ import forge from "node-forge" // import crypto from "node:crypto"
import path from "path" import path from "path"
// change "activitypub" to whatever you want your account name to be // change "activitypub" to whatever you want your account name to be
export const ACCOUNT = Bun.env.ACCOUNT || "activitypub" export const ACCOUNT = process.env.ACCOUNT || "activitypub"
// set up username and password for admin actions // set up username and password for admin actions
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || ""; export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "";
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ""; export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "";
// get the hostname (`PROJECT_DOMAIN` is set via glitch, but since we're using Bun now, this won't matter) // get the hostname (`PROJECT_DOMAIN` is set via glitch, but since we're using Bun now, this won't matter)
export const HOSTNAME = /*(Bun.env.PROJECT_DOMAIN && `${Bun.env.PROJECT_DOMAIN}.glitch.me`) ||*/ Bun.env.HOSTNAME || "localhost" export const HOSTNAME = /*(process.env.PROJECT_DOMAIN && `${process.env.PROJECT_DOMAIN}.glitch.me`) ||*/ process.env.HOSTNAME || "localhost"
export const NODE_ENV = Bun.env.NODE_ENV || "development" export const NODE_ENV = process.env.NODE_ENV || "development"
export const PORT = Bun.env.PORT || "3000" export const PORT = process.env.PORT || "3000"
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME
@ -32,9 +32,20 @@ export const PRIVATE_KEY =
(keypair && forge.pki.privateKeyToPem(keypair.privateKey)) || //keypair?.privateKey.export({ type: "pkcs8", format: "pem" }) || (keypair && forge.pki.privateKeyToPem(keypair.privateKey)) || //keypair?.privateKey.export({ type: "pkcs8", format: "pem" }) ||
"" ""
export const STATIC_PATH = path.join('.', '_site')
export const CONTENT_PATH = path.join('.', '_content') export const CONTENT_PATH = path.join('.', '_content')
export const POSTS_PATH = path.join(CONTENT_PATH, "posts") export const POSTS_PATH = path.join(CONTENT_PATH, "posts")
export const ACTIVITY_PATH = path.join(CONTENT_PATH, "posts")
export const DATA_PATH = path.join(CONTENT_PATH, "_data") export const DATA_PATH = path.join(CONTENT_PATH, "_data")
export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox") export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox")
export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox") export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox")
export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [
'index.html',
'index.shtml',
'index.htm',
'Index.html',
'Index.shtml',
'Index.htm',
'default.html',
'default.htm'
]

View file

@ -1,7 +1,10 @@
import { ACCOUNT, ACTOR, HOSTNAME, PORT } from "./env"; import { ACCOUNT, ACTOR, DEFAULT_DOCUMENTS, HOSTNAME, PORT, STATIC_PATH } from "./env";
import admin from './admin' import admin from './admin'
import activitypub from "./activitypub"; import activitypub from "./activitypub";
import { fetchObject } from "./request"; import { fetchObject } from "./request";
import path from "path"
import { BunFile } from "bun";
const { stat } = require("fs").promises
const server = Bun.serve({ const server = Bun.serve({
port: 3000, port: 3000,
@ -20,7 +23,7 @@ const server = Bun.serve({
return fetchObject(ACTOR, object_url) return fetchObject(ACTOR, object_url)
} }
return admin(req) || activitypub(req) || new Response("How did we get here?", { status: 404 }) return admin(req) || activitypub(req) || staticFile(req)
}, },
}); });
@ -46,4 +49,30 @@ const webfinger = async (req: Request, resource: string | null) => {
}, { headers: { "content-type": "application/activity+json" }}) }, { headers: { "content-type": "application/activity+json" }})
} }
const getDefaultDocument = async(base_path: string) => {
for(const d of DEFAULT_DOCUMENTS){
const filePath = path.join(base_path, d)
const file = Bun.file(filePath)
if(await file.exists()) return file
}
}
const staticFile = async (req:Request): Promise<Response> => {
try{
const url = new URL(req.url)
const filePath = path.join(STATIC_PATH, url.pathname)
let file:BunFile|undefined = Bun.file(filePath)
// if the file doesn't exist, attempt to get the default document for the path
if(!(await file.exists())) file = await getDefaultDocument(filePath)
if(file && await file.exists()) return new Response(file)
// if the file still doesn't exist, just return a 404
else return new Response("", { status: 404 })
}
catch(err) {
console.error(err)
return new Response("", { status: 404 })
}
}
console.log(`Listening on http://localhost:${server.port} ...`); console.log(`Listening on http://localhost:${server.port} ...`);

View file

@ -68,7 +68,7 @@ export default async function outbox(activity:any):Promise<Response> {
} }
async function create(activity:any, id:string) { async function create(activity:any, id:string) {
activity.object.id = activity.object.url = `${ACTOR}/post/${id}` activity.object.id = activity.object.url = `${ACTOR}/posts/${id}`
await db.createPost(activity.object, id) await db.createPost(activity.object, id)
return true return true
} }