Compare commits

...

No commits in common. "cb7682c777b0100e7764a611726a2e2fba28138d" and "cdc0f71a0f78b7c28a53e1506d7303a757443338" have entirely different histories.

11 changed files with 4833 additions and 11 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
prod
.scannerwork

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"]
}

View File

@ -1,16 +1,10 @@
# New Repository Template # Website Headers
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository. This project generates a JavaScript file to be uploaded to our CDN. This file automatically generates metadata, favicons, styles, and scripts for all of our production pages.
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose. To work on the file locally, use `pnpm dev`. This command will compile the TypeScript into JavaScript and simultaneously run the resulting dev server in `src/develop.ts`. This allows you to iterate over changes quickly and watch them update in real time.
## Readme When building the file, it is important to use `pnpm build` so that the minification step is run. Running `tsc` directly will bypass this step - while this is perfectly acceptable for debugging locally, the file MUST be minified before uploading to our CDN.
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
<!-- # Project Name
Project Description
## Live Version ## Live Version
@ -36,4 +30,4 @@ Copyright held by Naomi Carrigan.
## Contact ## Contact
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. --> We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.

16
build.ts Normal file
View File

@ -0,0 +1,16 @@
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { minify } from "terser";
const code = (
await readFile(join(process.cwd(), "prod", "index.js"), "utf-8")
).replace("{{ version }}", process.env.npm_package_version ?? "0.0.0");
const result = await minify(code, {
format: {
comments: /@license|@preserve|@copyright/,
},
});
if (!result.code) {
throw new Error("Failed to minify code.");
}
await writeFile(join(process.cwd(), "prod", "index.js"), result.code);

18
eslint.config.js Normal file
View File

@ -0,0 +1,18 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
import globals from "globals";
export default [
...NaomisConfig,
{
languageOptions: {
globals: {
...globals.browser
}
}
},
{
rules: {
"no-console": "off"
}
}
];

1
index.html Normal file

File diff suppressed because one or more lines are too long

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "website-headers",
"version": "1.1.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "concurrently \"pnpm build:watch\" \"pnpm start\"",
"prebuild": "pnpm lint",
"build": "tsc && tsx build.ts",
"build:watch": "tsc --watch",
"lint": "eslint src --max-warnings 0",
"start": "node --watch prod/develop.js",
"test": "echo \"Error: no test specified\" && exit 1",
"scan": "SONAR_TOKEN='op://Environment Variables - Development/SonarCloud/website-headers' op run -- sonar-scanner -Dsonar.organization=nhcarrigan -Dsonar.projectKey=nhcarrigan_website-headers -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.0.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "22.10.2",
"concurrently": "9.1.0",
"eslint": "9.17.0",
"globals": "15.14.0",
"terser": "5.37.0",
"tsx": "4.19.2",
"typescript": "5.7.2"
}
}

4393
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

31
src/develop.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { readFile } from "node:fs/promises";
import http from "node:http";
import { join } from "node:path";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const server = http.createServer(async(request, response) => {
if (request.url === "/") {
const file = await readFile(join(process.cwd(), "index.html"), "utf-8");
// eslint-disable-next-line @typescript-eslint/naming-convention
response.writeHead(200, { "Content-Type": "text/html" });
response.end(file);
}
if (request.url === "/prod/index.js") {
const file = await
readFile(join(process.cwd(), "prod", "index.js"), "utf-8");
// eslint-disable-next-line @typescript-eslint/naming-convention
response.writeHead(200, { "Content-Type": "application/javascript" });
response.end(file);
}
});
server.listen(8080, () => {
console.log("Server listening on port 8080");
});

320
src/index.ts Normal file
View File

@ -0,0 +1,320 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
// #region Version
const version = "{{ version }}";
console.log(`
========================================
Loading NHCarrigan library v${version}.
Copyright (c) ${new Date().getFullYear().
toString()} NHCarrigan
Changelog: https://codeberg.org/nhcarrigan/website-headers/releases
Licensed under our public license: https://docs.nhcarrigan.com/legal/license
Questions? Contact us at https://docs.nhcarrigan.com/about/contact
========================================
`);
// #endregion
// #region Query Selectors
const head = document.querySelector("head");
const body = document.querySelector("body");
const title = document.querySelector("title");
const description = document.querySelector(`meta[name="description"]`);
const { href: url, hostname } = window.location;
// #endregion
// #region Metadata and Open Graph
/**
* The title and description are set by each website. This should
* only load things like open graph data and favicons.
*/
const openGraphTitle = document.createElement("meta");
openGraphTitle.setAttribute("property", "og:title");
openGraphTitle.setAttribute("content", title?.innerText ?? "NHCarrigan");
const openGraphDescription = document.createElement("meta");
openGraphDescription.setAttribute("property", "og:description");
openGraphDescription.setAttribute(
"content",
description?.getAttribute("content")
// eslint-disable-next-line stylistic/max-len
?? "We are a software engineering and community management consulting firm.",
);
const openGraphImage = document.createElement("meta");
openGraphImage.setAttribute("property", "og:image");
openGraphImage.setAttribute(
"content",
"https://cdn.nhcarrigan.com/og-image.png",
);
const openGraphUrl = document.createElement("meta");
openGraphUrl.setAttribute("property", "og:url");
openGraphUrl.setAttribute("content", url);
const openGraphType = document.createElement("meta");
openGraphType.setAttribute("property", "og:type");
openGraphType.setAttribute("content", "website");
const twitterCard = document.createElement("meta");
twitterCard.setAttribute("name", "twitter:card");
twitterCard.setAttribute("content", "summary_large_image");
const twitterDomain = document.createElement("meta");
twitterDomain.setAttribute("name", "twitter:domain");
twitterDomain.setAttribute("content", hostname);
const twitterUrl = document.createElement("meta");
twitterUrl.setAttribute("name", "twitter:url");
twitterUrl.setAttribute("content", url);
const twitterTitle = document.createElement("meta");
twitterTitle.setAttribute("name", "twitter:title");
twitterTitle.setAttribute("content", title?.innerText ?? "NHCarrigan");
const twitterDescription = document.createElement("meta");
twitterDescription.setAttribute("name", "twitter:description");
twitterDescription.setAttribute(
"content",
description?.getAttribute("content")
// eslint-disable-next-line stylistic/max-len
?? "We are a software engineering and community management consulting firm.",
);
const twitterImage = document.createElement("meta");
twitterImage.setAttribute("name", "twitter:image");
twitterImage.setAttribute("content", "https://cdn.nhcarrigan.com/og-image.png");
// #endregion
// #region Favicon
const favicon = document.createElement("link");
favicon.rel = "icon";
favicon.type = "image/x-icon";
favicon.href = "https://cdn.nhcarrigan.com/favicon/favicon.ico";
const appleTouchIcon = document.createElement("link");
appleTouchIcon.rel = "apple-touch-icon";
appleTouchIcon.href = "https://cdn.nhcarrigan.com/favicon/apple-touch-icon.png";
const smallIcon = document.createElement("link");
smallIcon.rel = "icon";
smallIcon.href = "https://cdn.nhcarrigan.com/favicon/favicon-16x16.png";
const largeIcon = document.createElement("link");
largeIcon.rel = "icon";
largeIcon.href = "https://cdn.nhcarrigan.com/favicon/favicon-32x32.png";
// #endregion
// #region Styles
const styles = document.createElement("style");
styles.innerHTML = `
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.nhcarrigan.com/fonts/OpenDyslexicMono-Regular.otf') format('opentype');
}
:root {
--foreground: #04624f;
--background: #abfcecdd;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: 'OpenDyslexic', monospace;
cursor: url('https://cdn.nhcarrigan.com/cursors/cursor.cur'), auto;
min-height: 100vh;
min-width: 100vw;
}
body::before {
background: url(https://cdn.nhcarrigan.com/background.png);
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
z-index: -1;
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
pointer-events: none;
}
main {
color: var(--foreground);
background-color: var(--background);
text-align: center;
border-radius: 10px;
width: 95%;
max-width: 1080px;
margin: auto;
margin-bottom: 100px;
padding: 10px;
}
footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--foreground);
background-color: var(--background);
position: fixed;
bottom: 0;
height: 75px;
padding-left: 100px;
}
a {
color: unset;
cursor: url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer;
}
#tree-nation-offset-website {
display: flex;
align-items: center;
}
#audio-theme-button {
background: none;
border: none;
cursor: url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer;
color: var(--foreground);
}
@media screen and (max-width: 600px) {
#tree-nation-offset-website {
display: none;
}
footer {
padding-right: 100px;
}
}
`;
// #endregion
// #region Components
const footer = document.createElement("footer");
footer.innerHTML = `
<p>&copy; Naomi Carrigan</p>
<a href="https://chat.nhcarrigan.com" target="_blank" rel="noreferrer">
<i class="fa-solid fa-comments"></i>
</a>
<button id="audio-theme-button" type="button">
<i class="fa-solid fa-play"></i>
</button>
<div id="tree-nation-offset-website"></div>
`;
const videoOverlay = document.createElement("video");
videoOverlay.autoplay = true;
videoOverlay.loop = true;
videoOverlay.muted = true;
videoOverlay.playsInline = true;
videoOverlay.src = "https://cdn.nhcarrigan.com/overlay.webm";
videoOverlay.style.pointerEvents = "none";
videoOverlay.style.position = "fixed";
videoOverlay.style.top = "0";
videoOverlay.style.left = "0";
videoOverlay.style.opacity = "0.25";
videoOverlay.style.width = "100vw";
videoOverlay.style.height = "100vh";
videoOverlay.style.objectFit = "cover";
// #endregion
// #region Scripts
const treeNation = document.createElement("script");
treeNation.src
= "https://widgets.tree-nation.com/js/widgets/v1/widgets.min.js?v=1.0";
const treeNationBottom = document.createElement("script");
treeNationBottom.defer = true;
treeNationBottom.async = true;
treeNationBottom.innerHTML = `
const interval = setInterval(() => {
const tree = document.querySelector("#tree-nation-offset-website");
if (!tree) {
console.log("DOM has not hydrated yet, cannot load TreeNation badge.");
return;
}
TreeNationOffsetWebsite({
code: "a17464e0cd351220",
lang: "en",
theme: "dark",
}).render("#tree-nation-offset-website");
clearInterval(interval);
}, 1000);
`;
const fontAwesome = document.createElement("script");
fontAwesome.src = "https://kit.fontawesome.com/f949111719.js";
const hubspot = document.createElement("script");
hubspot.src = "https://js.hs-scripts.com/47086586.js";
hubspot.async = true;
hubspot.defer = true;
hubspot.id = "hs-script-loader";
const analytics = document.createElement("script");
analytics.defer = true;
analytics.setAttribute("domain", "nhcarrigan.com");
analytics.src
// eslint-disable-next-line stylistic/max-len
= "https://analytics.nhcarrigan.com/js/script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js";
const analytics2 = document.createElement("script");
analytics2.innerHTML = `
window.plausible = window.plausible ??
function() {
(window.plausible.q = window.plausible.q ?? []).push(arguments)
}
`;
// #endregion
// #region Inject Elements
head?.appendChild(openGraphTitle);
head?.appendChild(openGraphDescription);
head?.appendChild(openGraphImage);
head?.appendChild(openGraphUrl);
head?.appendChild(openGraphType);
head?.appendChild(twitterCard);
head?.appendChild(twitterDomain);
head?.appendChild(twitterUrl);
head?.appendChild(twitterTitle);
head?.appendChild(twitterDescription);
head?.appendChild(twitterImage);
head?.appendChild(favicon);
head?.appendChild(appleTouchIcon);
head?.appendChild(smallIcon);
head?.appendChild(largeIcon);
head?.appendChild(styles);
head?.appendChild(treeNation);
head?.appendChild(fontAwesome);
head?.appendChild(hubspot);
head?.appendChild(analytics);
head?.appendChild(analytics2);
body?.appendChild(footer);
body?.appendChild(videoOverlay);
body?.appendChild(treeNationBottom);
// #endregion
// #region Audio
const playButton = document.querySelector("#audio-theme-button");
const audio = new Audio("https://cdn.nhcarrigan.com/theme.mp3");
let playing = false;
playButton?.addEventListener("click", () => {
if (playing) {
audio.pause();
playing = false;
playButton.innerHTML = "<i class=\"fa-solid fa-play\"></i>";
} else {
void audio.play();
playing = true;
playButton.innerHTML = "<i class=\"fa-solid fa-pause\"></i>";
}
});

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"outDir": "./prod",
"rootDir": "./src",
"lib": ["DOM", "ESNext"],
},
"exclude": ["build.ts"]
}