sourceapprouteshomepage.js

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>&amp;</i> assorted aphorisms"
	},
	{
		slug: "ratings",
		title: "How i rate films*",
		description: "*Plus music, games, books, <i>&amp;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();
	}

	// Now getting the stats for all the pages on the site. Aaaalll of them.
	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;
		}

		// Use the oldest and newest Garden posts for its values
		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}`, // Short for “qualify”
		compileDebug: true
	};

	Object.assign(toSendOver, homePage);

	return prettyRender(
		res,
		next,
		`${homePage.translates}/${homePage.lang}`,
		toSendOver
	);
};

export default displayHomePage;