Files
nodejs-website/main.js
T
2026-11-02 08:52:04 -07:00

370 lines
11 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
/*
2022-09-14 Hyperling
Coding my own website rather than using WordPress or anything bloaty.
*/
//// Libraries ////
let express = require('express');
let app = express();
const execSync = require('child_process').execSync;
const fs = require('fs');
const { minify } = require('html-minifier-terser');
//// Global Variables ////
const DEBUG = false;
const app_name = "hyperling.com";
let pages = [];
const pages_dir = "./pages/";
const file_types = ["php", "sh"];
let assets = [];
const asset_dir = "./assets/";
const asset_types = ['png', 'svg', 'ico', 'webmanifest', 'jpg'];
let ports = [];
// Check parameters for numeric port numbers.
process.argv.forEach(function (val, index, array) {
console.log(now() + ": Parameter", index + ':', val, !isNaN(val));
if (!isNaN(val)) {
console.log(now() + ": Adding Port", val)
ports.push(val);
}
});
// Default port if none were passed.
if (ports.length === 0) {
ports.push(8080);
}
const stringsToRemove = [
RegExp("#!/usr/bin/php\n", "g")
]
//// Functions ////
// Compact HTML and remove unnecessary content like comments and whitespace.
// May help with performance for slower connections. Also more professional.
async function mini(html) {
//console.log(`html=${html}`);
const minified = await minify(html, {
collapseWhitespace: true,
removeComments: true,
minifyJS: true,
minifyCSS: true
});
//console.log(`minified=${minified}`);
return minified.toString();
}
// Check extension and guess a MIME type.
function getMIME(extension) {
let mime;
switch (extension) {
case "apk":
mime = "application/vnd.android.package-archive";
break;
case "jpg":
case "jpeg":
mime = "image/jpeg";
break;
case "png":
mime = "image/png";
break;
case "html":
mime = "text/html";
break;
case "zip":
mime = "application/zip";
break;
case "md":
case "txt":
case "csv":
mime = "text/*";
break;
default:
mime = "application/" + extension;
break;
}
return mime;
}
// Print datetime for logging.
function now() {
return new Date().toISOString();
}
/* Code exists inside a main function so that we may use async/await.
*/
async function main() {
console.log(now() + ": ...Starting Main...");
// Getting dates in Node.js is awful, just use Linux.
const start_datetime = "" + await execSync('date "+%Y-%m-%dT%H:%M:%S%:z"');
const start_datetime_trimmed = start_datetime.trim();
console.log(now() + ": Set app to return HTML documents.");
app.use(function (req, res, next) {
res.contentType('text/html');
next();
});
/* Loop through all files in the pages subdirectory and add the allowed
// file types into the pages array for automatic router creation.
*/
console.log(now() + ": ...Starting Pages...");
let ls = await fs.promises.readdir(pages_dir);
if (DEBUG) console.log("DEBUG: Results of ls, ", ls);
for (let file of ls) {
let file_test = file.split(".");
let file_name = file_test[0];
let file_type = file_test[file_test.length - 1];
if (file_types.includes(file_type)) {
if (DEBUG) console.log("DEBUG: Hooray!", file, "is being added.");
pages[file_name] = pages_dir + file;
console.log(now() + ": * Added page", file);
} else {
if (DEBUG) console.log("DEBUG: ", file, "is not an approved type, skipping.");
}
}
console.log(now() + ": * Pages loaded: ", pages);
//return; // Stop execution FORTESTING
/* Loop through the assets folder and grab favicons, manifests, etc. and
// allow them to be accessed via the root of the website, such as a favicon
// at 'https://hyperling.com/favicon.ico'. May help with SEO!
*/
console.log(now() + ": ...Starting Assets...");
let ls_assets = await fs.promises.readdir(asset_dir);
if (DEBUG) console.log("DEBUG: Results of ls_assets, ", ls_assets);
for (let file of ls_assets) {
let file_test = file.split(".");
//let file_name = file_test[0]; // not needed (yet)
let file_type = file_test[file_test.length - 1];
if (asset_types.includes(file_type)) {
if (DEBUG) console.log("DEBUG: Hooray!", file, "is being added.");
assets[file] = asset_dir + file;
console.log(now() + ": * Added asset", file);
} else {
if (DEBUG) console.log("DEBUG: ", file, "is not an approved type, skipping.");
}
}
console.log(now() + ": * Assets loaded: ", assets);
/* Create both an XML and HTML sitemap based on these entries. XML is used for
// bots like SEO scrapers. HTML will be for human users looking for a list of
// all pages. Some are not in the menu. Generated an example XML sitemap at
// www.xml-sitemaps.com then stripped it apart and made it dynamic.
*/
let sitemap_xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
>
<url>
<loc>https://hyperling.com/</loc>
<lastmod>`+start_datetime_trimmed+`</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://hyperling.com/sitemap/</loc>
<lastmod>`+start_datetime_trimmed+`</lastmod>
<priority>0.80</priority>
</url>
`;
let sitemap_html = `
<!DOCTYPE html>
<html lang="en">
<head>
<title>
Sitemap for Hyperling
</title>
</head>
<body>
<strong>Special Pages</strong>
<ul>
<li>
<b>Main Site</b>
(<a href="/">Local</a>)
(<a href="https://`+app_name+`/">Hardlink</a>)
</li>
<li>
<b>XML Site Map</b>
(<a href="/sitemap.xml">Local</a>)
(<a href="https://`+app_name+`/sitemap.xml">Hardlink</a>)
</li>
<li>
<b>HTML Site Map</b>
(<a href="/sitemap/">Local</a>)
(<a href="https://`+app_name+`/sitemap/">Hardlink</a>)
<i>[You are here!]</i>
</li>
</ul>
<strong>Web Pages (Alphabetical)</strong>
<ul>
`;
let robots_txt = `
User-agent: *
Allow: /
Sitemap: https://hyperling.com/sitemap.xml
`;
console.log(now() + ": ...Adding Routes...");
let router = express.Router();
/* AUTOMATIC METHOD BASED ON OBJECT/ARRAY OF WEB SCRIPTS
// Creates routes with the URL of the key and location of the value.
*/
// TBD/TODO would be great to log this as "DATE: ADDRESS hit ROUTER requesting RESOURCE"
for (let key in pages) {
console.log(now() + ": * Creating router for", key);
router.get("/" + key, async function (req,res) {
console.log(now() + ":", key, "fulfilling request to", req.socket.remoteAddress, "asking for", req.url);
let html = "" + execSync(pages[key]);
stringsToRemove.forEach(string => {
html = html.replace(string, "");
});
html = await mini(html);
res.send(html);
});
/* Append the page to the sitemap variables.
*/
console.log(now() + ": * * Adding to sitemap.xml");
sitemap_xml = sitemap_xml + `
<url>
<loc>https://hyperling.com/`+key+`/</loc>
<lastmod>`+start_datetime_trimmed+`</lastmod>
<priority>0.80</priority>
</url>
`;
console.log(now() + ": * * Adding to sitemap.html");
sitemap_html = sitemap_html + `
<li>
<b>`+key+`</b>
(<a href="/`+key+`/">Local</a>)
(<a href="https://`+app_name+`/`+key+`/">Hardlink</a>)
</li>
`;
}
/* Close the sitemap variables
*/
sitemap_xml = sitemap_xml + `
</urlset>
`;
sitemap_html = sitemap_html + `
</ul></body></html>
`;
console.log(now() + ": * Creating routers for assets");
for (let key in assets) {
console.log(now() + ": * * Adding /" + key);
router.get('/' + key, function (req, res) {
console.log(now() + ": asset response to", req.socket.remoteAddress, "asking for", req.url)
// Build variables.
const file = "./assets/" + req.path;
const extensions = req.path.split(".");
const extension = extensions[extensions.length-1];
let mime = getMIME(extension);
// Return the file
res.contentType(mime);
let f = fs.createReadStream(file)
.on("error", function(e) {
res.contentType("text/plain");
res.send(404, "Asset Not Found");
})
.pipe(res)
;
});
}
// Provide sitemap.xml file for "SEO".
// TBD/TODO would be great to log this as "DATE: ADDRESS hit ROUTER requesting RESOURCE"
console.log(now() + ": * Creating router for sitemap.xml");
router.get('/sitemap.xml', function (req, res) {
console.log(now() + ": sitemap.xml being provided to", req.socket.remoteAddress)
res.contentType('text/xml');
res.send(sitemap_xml);
});
// Provide human-usable sitemap links.
// TBD/TODO would be great to log this as "DATE: ADDRESS hit ROUTER requesting RESOURCE"
console.log(now() + ": * Creating router for sitemap*");
router.get('/sitemap*', async function (req, res) {
console.log(now() + ": sitemap.html being provided to", req.socket.remoteAddress)
html = await mini(sitemap_html);
res.send(mini(html));
});
// Provide a robots.txt for where search engines should find things.
console.log(now() + ": * Creating router for robots.txt");
router.get('/robots.txt', function (req, res) {
console.log(now() + ": robots.txt being provided to", req.socket.remoteAddress)
res.send(robots_txt);
});
// Return a resource from the files folder.
// TBD/TODO would be great to log this as "DATE: ADDRESS hit ROUTER requesting RESOURCE"
console.log(now() + ": * Creating router for files");
router.get('/files*', function (req, res) {
console.log(now() + ": file response to", req.socket.remoteAddress, "asking for", req.url)
// Build variables.
const file = "." + req.path;
const extensions = req.path.split(".");
const extension = extensions[extensions.length-1];
let mime = getMIME(extension);
// Return the file
res.contentType(mime);
let f = fs.createReadStream(file)
.on("error", function(e) {
res.contentType("text/plain");
res.send(404, "File Not Found");
})
.pipe(res)
;
});
// Originally a test, now a catch-all redirection to Home!
console.log(now() + ": * Creating router for redirection");
router.get('/*', async function (req, res) {
// WARNING: These are huge so only look when you need to.
//console.log(req);
//console.log(res);
console.log(now() + ": *wildcard* replying to", req.socket.remoteAddress, "asking for", req.url)
let html = "" + execSync("./pages/home.php");
stringsToRemove.forEach(string => {
html = html.replace(string, "");
});
html = await mini(html);
res.send(html);
});
app.use('', router);
console.log(now() + ": ...Adding Ports...");
ports.forEach(port => {
app.listen(port);
console.log(now() + ": * Now listening on port " + port + ".");
console.log(now() + ": * URL: http://localhost:" + port);
});
console.log(now() + ": Done! Now we wait...");
}
//// Program Execution ////
main();