Port to TypeScript/Elysia
This commit is contained in:
parent
29739d243f
commit
513335c485
17 changed files with 413 additions and 2603 deletions
53
src/index.tsx
Normal file
53
src/index.tsx
Normal 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}`);
|
||||
});
|
||||
102
src/main.rs
102
src/main.rs
|
|
@ -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
92
src/recent-posts.tsx
Normal 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
22
src/util.tsx
Normal 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}`;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue