Port to TypeScript/Elysia

This commit is contained in:
sepia 2025-07-28 13:39:01 -05:00
parent 29739d243f
commit 513335c485
17 changed files with 413 additions and 2603 deletions

53
src/index.tsx Normal file
View file

@ -0,0 +1,53 @@
import { Elysia } from "elysia";
import { html } from "@elysiajs/html";
import { staticPlugin } from "@elysiajs/static";
import path from "path";
import fs from "fs/promises";
import { renderTemplate } from "./util";
import { recentPosts } from "./recent-posts";
const app = new Elysia()
.use(
staticPlugin({
assets: "./data/style",
prefix: "/style",
}),
)
.use(html())
.get("/", async ({ set }) => {
try {
const content = await fs.readFile(
path.join(process.cwd(), "data/pages/about.html"),
"utf8",
);
return renderTemplate("index.html", { content });
} catch (error) {
console.error("Error serving main page: ", error);
set.status = 500;
set.headers["Content-Type"] = "text/plain";
return "Internal server error";
}
})
.get("/posts/:post_name", async ({ params, set }) => {
try {
const filePath = path.join(
process.cwd(),
"data/posts",
`${params.post_name}.html`,
);
const fileContent = await fs.readFile(filePath, "utf8");
return renderTemplate("index.html", { content: fileContent });
} catch (error) {
console.error(`Error serving post ${params.post_name}: `, error);
set.status = 404;
set.headers["Content-Type"] = "text/plain";
return "Post not found.";
}
})
.use(recentPosts);
const port = Number(process.env.PORT || 3000);
app.listen(port, () => {
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${port}`);
});

View file

@ -1,102 +0,0 @@
#[macro_use]
extern crate rocket;
use chrono::{DateTime, Utc};
use rocket::fs::NamedFile;
use rocket_dyn_templates::{context, Template};
use scraper::{Html, Selector};
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
#[get("/")]
fn serve_main() -> Template {
Template::render(
"index",
context! {
content: std::fs::read_to_string("data/pages/about.html").unwrap_or_else(|_| "Error loading about page.".to_string())
},
)
}
#[get("/posts/<post_name>")]
async fn serve_post(post_name: &str) -> Template {
let file_path = Path::new("data/posts").join(format!("{}.html", post_name));
let file_maybe = std::fs::read_to_string(file_path);
let file = match file_maybe.ok() {
Some(s) => s,
None => "404: File not found.".to_string(),
};
Template::render(
"index",
context! {
content: file
},
)
}
#[get("/style/<file>")]
async fn serve_css(file: &str) -> Option<NamedFile> {
let file_path = Path::new("data/style").join(format!("{}", file));
NamedFile::open(file_path).await.ok()
}
#[get("/recent_posts")]
fn list_recent_posts() -> Template {
#[derive(Serialize)]
struct Post {
title: String,
path: String,
date: String,
date_utc: DateTime<Utc>,
}
let posts_dir = Path::new("data/posts");
let mut posts = Vec::new();
if let Ok(entries) = std::fs::read_dir(posts_dir) {
for entry in entries {
if let Ok(entry) = entry {
if let Some(file_name) = entry.file_name().to_str() {
if file_name.ends_with(".html") {
let file = match std::fs::read_to_string(posts_dir.join(file_name)) {
Ok(s) => s,
Err(_) => continue,
};
let document = Html::parse_document(file.as_str());
let mut tags: HashMap<&str, &str> = HashMap::new();
for meta_tag in document.select(&Selector::parse("meta").unwrap()) {
let name = meta_tag.value().attr("name").unwrap_or("");
let content = meta_tag.value().attr("content").unwrap_or("");
tags.insert(name, content);
}
let title = tags.get("title").unwrap_or(&"Untitled Post").to_string();
let date_utc = tags
.get("date")
.and_then(|s_date| DateTime::parse_from_rfc3339(s_date).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap());
let date = date_utc.format("%d-%m-%Y").to_string();
posts.push(Post {
title,
date_utc,
date,
path: format!("/posts/{}", file_name.trim_end_matches(".html")),
});
}
}
}
}
}
posts.sort_by_key(|post| std::cmp::Reverse(post.date_utc));
Template::render("posts_list", context! { posts: posts })
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![serve_post, serve_main, serve_css, list_recent_posts])
.attach(Template::fairing())
}

92
src/recent-posts.tsx Normal file
View file

@ -0,0 +1,92 @@
import { Elysia } from "elysia";
import * as cheerio from "cheerio";
import path from "path";
import fs from "fs/promises";
import { renderTemplate } from "./util";
interface Post {
title: string;
path: string;
date: string;
date_utc: Date;
}
export const recentPosts = (app: Elysia) =>
app.get("/recent_posts", async ({ set }) => {
const postsDir = path.join(process.cwd(), "data/posts");
const posts: Post[] = [];
try {
const entries = await fs.readdir(postsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith(".html")) {
const filePath = path.join(postsDir, entry.name);
try {
const fileContent = await fs.readFile(filePath, "utf8");
const $ = cheerio.load(fileContent);
// Get a map of the post's metadata tags
const tags: { [key: string]: string } = {};
$("meta").each((_i, el) => {
const name = $(el).attr("name");
const content = $(el).attr("content");
if (name && content) {
tags[name] = content;
}
});
const title = tags["title"] || "Untitled Post";
const dateStr = tags["date"];
let date_utc: Date;
if (dateStr) {
try {
date_utc = new Date(dateStr);
if (isNaN(date_utc.getTime())) {
// Check for invalid date
date_utc = new Date(0); // Epoch start if invalid
}
} catch (e) {
console.warn(
`Could not parse date ${dateStr} for ${entry.name}, using epoch.`,
e,
);
date_utc = new Date(0); // Epoch start if parsing fails
}
} else {
date_utc = new Date(0); // Epoch start if no date provided
}
// dd-mm-yyyy
const date = date_utc.toLocaleDateString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
posts.push({
title,
date_utc,
date,
path: `/posts/${entry.name.replace(".html", "")}`,
});
} catch (readError) {
console.error(
`Error reading or processing file ${entry.name}:`,
readError,
);
continue;
}
}
}
posts.sort((a, b) => b.date_utc.getTime() - a.date_utc.getTime()); // Sort by date_utc descending
return renderTemplate("posts_list.html", { posts });
} catch (error) {
console.error("Error listing recent posts: ", error);
set.status = 500;
set.headers["Content-Type"] = "text/plain";
return "Internal Server Error";
}
});

22
src/util.tsx Normal file
View file

@ -0,0 +1,22 @@
import Handlebars from "handlebars";
import path from "path";
import fs from "fs/promises";
export async function renderTemplate(
templateName: string,
context: Record<string, any>,
) {
try {
const templatePath = path.join(
process.cwd(),
"templates",
`${templateName}.hbs`,
);
const templateContent = await fs.readFile(templatePath, "utf8");
const template = Handlebars.compile(templateContent);
return template(context);
} catch (error) {
console.error(`Error rendering template ${templateName}:`, error);
return `Error rendering template ${templateName}`;
}
}