import Database from "better-sqlite3";
import express from "express";
import gazetteer from "../gazetteer.js";
import grimm from "../grimm.js";
import { prettyRender, getRelativeDate } from "../helpers.js";
const router = express.Router();
const db = new Database("./db/site.db");
db.pragma("journal_mode = DELETE");
const pageLinks = [
{
slug: "100",
title: "The internet’s 100 favourite albums"
},
{
slug: "100/films",
title: "The internet’s 100 favourite films"
},
{
slug: "about",
title: "About the author"
},
{
slug: "bandnames",
title: "Good names for a band"
},
{
slug: "belief",
title: "What i believe",
tags: ["pagan"]
},
{
slug: "bestof/2025",
title: "The <i>Satyrs’ Forest</i> Horny Awards 2025",
description: "The best things of the year that was"
},
{
slug: "binturong",
title: "Where can i go see a binturong?"
},
{
slug: "bucketlist",
title: "Things to do before i die"
},
{
slug: "calendrical",
title: "A calendrical table",
tags: ["pagan"]
},
{
slug: "christmas",
title: "Our Christmas tradition",
tags: ["favourites"]
},
{
slug: "clock",
title: "A World Clock",
tags: ["interactives"]
},
{
slug: "codex",
title: "Codex Satyrorum"
},
{
slug: "comments",
title: "About comments on this site",
description: "Also a <i>de facto</i> guestbook, really"
},
{
slug: "continents",
title: "There are two continents"
},
{
slug: "copypaste",
title: "Copypastables"
},
{
slug: "crying",
title: "Songs i’ve cried to"
},
{
slug: "discord",
title: "Discord funny moments"
},
{
slug: "dreams",
title: "Dream log"
},
{
slug: "english",
title: "A nicer ƿay of ƿriting Engliſh"
},
{
slug: "equala",
title: "EqualA",
description: "The last basemap you’ll ever need"
},
{
slug: "films",
title: "My favourite films"
},
{
slug: "furry",
title: "The Otter’s Holt",
description: "Proud furry trash",
tags: ["favourites", "creative"]
},
{
slug: "furry/ardha",
title: "The Crew of the <i>Ardhanarishvara</i>",
description: "More furry nonsense",
tags: ["favourites", "creative"]
},
{
slug: "gallery",
title: "The Hoornsche Galerij",
description: "My favourite works of art",
tags: ["favourites"]
},
{
slug: "garden",
title: "<i>The Garden</i>",
description: "An occasional blog",
tags: ["favourites", "creative"]
},
{
slug: "heraldry",
title: "My coat of arms"
},
{
slug: "heximal",
title: "Heximal"
},
{
slug: "hymns",
title: "The Orphic Hymns Online",
tags: ["pagan"]
},
{
slug: "imo",
title: "In my opinion"
},
{
slug: "latbs",
title: "<i>Looking at the Big Sky</i>",
tags: ["favourites", "creative"]
},
{
slug: "licence",
title: "Public domain declaration",
description: "No rights reserved."
},
{
slug: "linkroll",
title: "Linkroll",
description:
"Come one, come all, to Mx Tynehorne’s<br>cabinet of hypertext curiosities!",
tags: ["favourites"]
},
{
slug: "loanwords",
title: "One loan word per language"
},
{
slug: "longs",
title: "s → ſ",
tags: ["interactives"]
},
{
slug: "maps",
title: "The Cartothèque",
tags: ["creative"]
},
{
slug: "matrix",
title: "The Matrix of Power",
description: "I can’t believe it’s not 2048!",
tags: ["interactives"]
},
{
slug: "media",
title: "Stuff i’ve watched* recently",
description: "*{watched, listened to, read, played}"
},
{
slug: "misrule",
title: "Lords of Misrule",
description:
"Our annual <span class='theonym'>Saturnalia</span> tradition"
},
{
slug: "music",
title: "The Panpipes",
description: "Or, my favourite music"
},
{
slug: "new",
title: "What’s new"
},
{
slug: "phanes",
title: "The Cult of Phanes",
tags: ["favourites", "creative"]
},
{
slug: "predictions/2024",
title: "My predictions for 2024",
description: "On hiatus until 2029"
},
{
slug: "proust",
title: "Proust’s Questionnaire"
},
{
slug: "quoth",
title: "Quotes <i>&</i> assorted aphorisms"
},
{
slug: "ratings",
title: "How i rate films*",
description: "*Plus music, games, books, <i>&c.</i>"
},
{
slug: "rumpelstiltskin",
title: "Rumpelstiltskin",
description: "I can’t believe it’s not Wordle!",
tags: ["interactives"]
},
{
slug: "saqvk",
title: "Są̂qʌk",
description: "A constructed language",
tags: ["creative"]
},
{
slug: "shrines",
title: "The Grove of Shrines",
description: "Euoi!",
tags: ["favourites", "pagan"]
},
{
slug: "source",
title: "Source code"
},
{
slug: "srb2",
title: "<i>Sonic Robo Blast 2</i>: The Game: The Fan Page",
description: "Guest-written by the ghost of my ten-year-old self"
},
{
slug: "styleguide",
title: "The Hoorn Manual of Style"
},
{
slug: "uses",
title: "What i use"
},
{
slug: "videotape",
title: "This’ll be on my videotape"
},
{
slug: "vinyl",
title: "My music collection"
},
{
slug: "words",
title: "The Compendium of Curious Words",
tags: ["favourites"]
},
{
slug: "write",
title: "Dotpad 9000",
description: "Just start writing.",
tags: ["interactives"]
}
];
const isDaytime = date => {
const latitude = 54.978;
const longitude = -1.161;
const utcMinutes =
date.getUTCHours() * 60 +
date.getUTCMinutes() +
date.getUTCSeconds() / 60 +
date.getUTCMilliseconds() / 60_000;
const dayOfYear =
(date.getTime() - Date.UTC(date.getUTCFullYear(), 0, 1)) / 86_400_000;
const yearAngle = ((2 * Math.PI) / 365) * (dayOfYear - 0.5);
const equationOfTime =
229.18 *
(0.000075 +
0.001868 * Math.cos(yearAngle) -
0.032077 * Math.sin(yearAngle) -
0.014615 * Math.cos(2 * yearAngle) -
0.040849 * Math.sin(2 * yearAngle));
const decl =
0.006918 -
0.399912 * Math.cos(yearAngle) +
0.070257 * Math.sin(yearAngle) -
0.006758 * Math.cos(2 * yearAngle) +
0.000907 * Math.sin(2 * yearAngle) -
0.002697 * Math.cos(3 * yearAngle) +
0.00148 * Math.sin(3 * yearAngle);
const solarMinutes = utcMinutes + equationOfTime + 4 * longitude;
const hourAngle = ((solarMinutes / 4 - 180) * Math.PI) / 180;
const lat = (latitude * Math.PI) / 180;
const cosZenith =
Math.sin(lat) * Math.sin(decl) +
Math.cos(lat) * Math.cos(decl) * Math.cos(hourAngle);
return cosZenith > 0;
};
const displayHomePage = async (slug, res, next) => {
const homePage = db
.prepare(
`Select * From editions
Join pagesets
On editions.translates = pagesets.pagesetID
Where editions.editionID = ?`
)
.get("index");
if (homePage === undefined) {
return next();
}
const queryPages = `Select pagesets.pagesetID, pagesets.original, pagesets.pageCreated, editions.pageUpdated
From pagesets
Inner Join editions
On editions.editionID = pagesets.original`;
const pages = db.prepare(queryPages).all();
for (const plink of pageLinks) {
const page = pages.find(el => plink.slug == el.pagesetID);
if (page === undefined) {
plink.url = plink.slug;
} else {
plink.url = page.original;
plink.created = page.pageCreated;
plink.updated = page.pageUpdated || page.pageCreated;
}
if (plink.slug == "garden") {
plink.updated = db
.prepare(`Select Max(pageCreated) From garden Where public = 1`)
.get()["Max(pageCreated)"];
plink.created = db
.prepare(`Select Min(pageCreated) From garden Where public = 1`)
.get()["Min(pageCreated)"];
}
if (plink.slug == "source") {
plink.created = "2026-03-20";
plink.updated = "2026-03-20";
}
if (plink.updated) {
plink.updatedRelative = getRelativeDate(
new Date(),
new Date(plink.updated)
);
}
if (!("tags" in plink)) {
plink.tags = [];
}
}
pageLinks.sort((a, b) =>
a.updated < b.updated ? 1 : a.updated > b.updated ? -1 : 0
);
const toSendOver = {
slug: "index",
translations: [],
gazetteer: gazetteer,
grimm: grimm,
tr: grimm.translator(homePage.lang),
baseDomain: "satyrs.eu",
pageLinks: pageLinks,
isDaytime: isDaytime,
qua: text => `/${homePage.translates}/${text}`,
compileDebug: true
};
Object.assign(toSendOver, homePage);
return prettyRender(
res,
next,
`${homePage.translates}/${homePage.lang}`,
toSendOver
);
};
export default displayHomePage;