Skip to content

Commit b9e8959

Browse files
committed
i
1 parent 60ec07b commit b9e8959

13 files changed

Lines changed: 249 additions & 181 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ node serv.js dev
2626
<h3 id="SPA Navigation & Content Loading">SPA Navigation & Content Loading</h3>
2727

2828
- [spa.js](/src/js/spa.js) For SPA (Single Page App) routing across different hosting environments, the app uses [netlify.toml](/netlify.toml) on Netlify, or
29-
- [404.html](/404.html) & [github-pages.js](/src/js/github-pages.js) on [GitHub Pages](https://i1li.github.io/), or
29+
- [404.html](/404.html) is cloned from index.html by build.js for SPA on [GitHub Pages](https://i1li.github.io/), or
3030
- [serv.js](/serv.js) elsewhere.
3131
- Enables seamless navigation within the application without full page reloads, improving user experience without complication of a framework.
3232
- Default "display all posts" view at root directory, with posts auto-expanding upon scroll, shows a welcome intro message & navigation links at top of page.
@@ -107,10 +107,10 @@ The hybrid shuffle function combines two different techniques to optimize perfor
107107

108108
<h3 id="Custom Build Script">Custom Build Script</h3>
109109

110-
- [build.js](build.js) minifies and bundles files from /src to /dist
110+
- [build.js](build.js) minifies and bundles files from /src to /dist, and adds event listeners to html.
111111
To use:
112112
```javascript
113-
npm i esbuild
113+
npm i esbuild cheerio
114114
node build.js
115115
```
116116

build.js

Lines changed: 67 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
const esbuild = require('esbuild');
22
const fs = require('fs');
33
const path = require('path');
4-
const crypto = require('crypto');
5-
const cheerio = require('cheerio');
64

7-
let html = fs.readFileSync('src/index.html', 'utf8');
5+
let htmlContent = fs.readFileSync('src/index.html', 'utf8');
86
const itemsToCopy = ['favicon.ico', 'img'];
9-
const cacheBuster = () => crypto.randomBytes(3).toString('hex').slice(0, 5);
10-
const uniqueHash = cacheBuster();
7+
const cacheBuster = Math.random().toString(36).slice(2, 8);
118
const addComment = (content, fileType) => {
129
const comment = "Hello and God bless! Christ is King! https://github.com/i1li/i";
1310
switch(fileType) {
@@ -29,82 +26,87 @@ const resolvePath = (filePath) => {
2926
}
3027
return path.join(__dirname, 'src', filePath);
3128
};
32-
const jsFiles = [...html.matchAll(scriptRegex)].map(match => resolvePath(match[1]));
33-
const cssFiles = [...html.matchAll(linkRegex)].map(match => resolvePath(match[1]));
34-
29+
const jsFiles = [...htmlContent.matchAll(scriptRegex)].map(match => resolvePath(match[1]));
30+
const cssFiles = [...htmlContent.matchAll(linkRegex)].map(match => resolvePath(match[1]));
3531
if (fs.existsSync('dist')) {
3632
fs.rmSync('dist', { recursive: true, force: true });
3733
}
3834
fs.mkdirSync('dist');
3935

40-
// NEW: Process links in HTML at build time
41-
const $ = cheerio.load(html);
42-
$('a').each((i, el) => {
43-
const href = $(el).attr('href');
44-
if (!href) return; // Skip anchors without href
45-
46-
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
47-
// External: Add target/rel
48-
$(el).attr('target', '_blank');
49-
$(el).attr('rel', 'noreferrer');
50-
} else {
51-
// Local: Add onclick with navigate call (assumes href starts with /)
52-
const path = href.startsWith('/') ? href.substring(1) : href;
53-
const escapedPath = path.replace(/'/g, "\\'"); // Escape for JS string
54-
$(el).attr('onclick', `navigateSPA(event, '${escapedPath}')`); // Pass event explicitly
55-
}
56-
});
57-
58-
// Add onclick to overlay-eligible images
59-
$('img:not(.img-footer,.img-header,.to-top,.no-overlay)').each((i, el) => {
60-
const src = $(el).attr('src');
61-
if (src) { // Skip images without src
62-
const escapedSrc = src.replace(/'/g, "\\'");
63-
$(el).attr('onclick', `openImageOverlay('${escapedSrc}');return false;`);
64-
}
65-
});
66-
67-
html = $.html(); // Update html with modifications
36+
// Helper function to process <a> tags with regex
37+
function processLinks(html) {
38+
return html.replace(/<a\s+([^>]*?)>/gi, (match, attrs) => {
39+
const hrefMatch = attrs.match(/href\s*=\s*["']([^"']+)["']/i);
40+
if (!hrefMatch) return match; // Skip if no href
41+
const href = hrefMatch[1];
42+
let newAttrs = attrs;
43+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
44+
// External: Add target and rel if not present
45+
if (!/target\s*=\s*["']_blank["']/i.test(newAttrs)) {
46+
newAttrs += ` target="_blank"`;
47+
}
48+
if (!/rel\s*=\s*["']noreferrer["']/i.test(newAttrs)) {
49+
newAttrs += ` rel="noreferrer"`;
50+
}
51+
} else {
52+
// Local: Add onclick
53+
const path = href.startsWith('/') ? href.substring(1) : href;
54+
const escapedPath = path.replace(/'/g, "\\'");
55+
if (!/onclick\s*=\s*["'][^"']*["']/i.test(newAttrs)) {
56+
newAttrs += ` onclick="navigateSPA(event, '${escapedPath}')"`;
57+
}
58+
}
59+
return `<a ${newAttrs.trim()}>`;
60+
});
61+
}
62+
// Helper function to process <img> tags with regex
63+
function processImages(html) {
64+
return html.replace(/<img\s+([^>]*?)>/gi, (match, attrs) => {
65+
// Check for excluded classes
66+
if (/\b(img-footer|img-header|to-top|no-overlay)\b/i.test(attrs)) {
67+
return match; // Skip excluded images
68+
}
69+
const srcMatch = attrs.match(/src\s*=\s*["']([^"']+)["']/i);
70+
if (!srcMatch) return match; // Skip if no src
71+
const src = srcMatch[1];
72+
const escapedSrc = src.replace(/'/g, "\\'");
73+
let newAttrs = attrs;
74+
if (!/onclick\s*=\s*["'][^"']*["']/i.test(newAttrs)) {
75+
newAttrs += ` onclick="openImageOverlay('${escapedSrc}');return false;"`;
76+
}
77+
return `<img ${newAttrs.trim()}>`;
78+
});
79+
}
80+
htmlContent = processLinks(htmlContent);
81+
htmlContent = processImages(htmlContent);
6882

69-
async function bundleJS() {
70-
let concatenatedJS = '';
71-
for (const file of jsFiles) {
83+
async function bundleFiles(type, files) {
84+
let concatenated = '';
85+
for (const file of files) {
7286
const result = await esbuild.build({
7387
entryPoints: [file],
7488
bundle: false,
7589
minify: true,
7690
write: false,
7791
});
78-
concatenatedJS += result.outputFiles[0].text.trim();
92+
concatenated += result.outputFiles[0].text.trim();
7993
}
80-
concatenatedJS = concatenatedJS.replace(/\s+/g, ' ');
81-
concatenatedJS = addComment(concatenatedJS, 'js');
82-
fs.writeFileSync(`dist/bundle-${uniqueHash}.js`, concatenatedJS);
83-
}
84-
async function bundleCSS() {
85-
let concatenatedCSS = '';
86-
for (const file of cssFiles) {
87-
const result = await esbuild.build({
88-
entryPoints: [file],
89-
bundle: false,
90-
minify: true,
91-
write: false,
92-
});
93-
concatenatedCSS += result.outputFiles[0].text.trim();
94+
if (type === 'js') {
95+
concatenated = concatenated.replace(/\s+/g, ' ');
9496
}
95-
concatenatedCSS = addComment(concatenatedCSS, 'css');
96-
fs.writeFileSync(`dist/bundle-${uniqueHash}.css`, concatenatedCSS);
97+
concatenated = addComment(concatenated, type);
98+
fs.writeFileSync(`dist/bundle-${cacheBuster}.${type}`, concatenated);
9799
}
98-
Promise.all([bundleJS(), bundleCSS()])
100+
Promise.all([bundleFiles('js', jsFiles), bundleFiles('css', cssFiles)])
99101
.then(() => {
100-
html = html.replace(/<script.*?<\/script>/g, '');
101-
html = html.replace('</body>', `<script src="/bundle-${uniqueHash}.js"></script></body>`);
102-
html = html.replace(/<link.*?stylesheet.*?>/g, '');
103-
html = html.replace('</head>', `<link rel="stylesheet" href="/bundle-${uniqueHash}.css"></head>`);
104-
html = html.replace(/\s+/g, ' ');
105-
html = addComment(html, 'html');
106-
fs.writeFileSync('dist/index.html', html);
107-
fs.writeFileSync('dist/404.html', html);
102+
htmlContent = htmlContent.replace(/<script.*?<\/script>/g, '');
103+
htmlContent = htmlContent.replace('</body>', `<script src="/bundle-${cacheBuster}.js"></script></body>`);
104+
htmlContent = htmlContent.replace(/<link.*?stylesheet.*?>/g, '');
105+
htmlContent = htmlContent.replace('</head>', `<link rel="stylesheet" href="/bundle-${cacheBuster}.css"></head>`);
106+
htmlContent = htmlContent.replace(/\s+/g, ' ');
107+
htmlContent = addComment(htmlContent, 'html');
108+
fs.writeFileSync('dist/index.html', htmlContent);
109+
fs.writeFileSync('dist/404.html', htmlContent);
108110
console.log('Build complete');
109111
})
110112
.catch((error) => {

dist/404.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/bundle-0e7a0.css

Lines changed: 0 additions & 2 deletions
This file was deleted.

dist/bundle-0e7a0.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

dist/bundle-arrj5j.css

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/bundle-arrj5j.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/css/style.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ header {position:fixed;
5252
overflow:hidden;
5353
background:linear-gradient(to top, rgba(255, 0, 0, .25) 0%, rgba(0, 22, 0, .3) 2%, rgba(128, 0, 128, 0.2) 98%, rgba(0, 0, 255, .5) 100%);
5454
backdrop-filter:brightness(115%) contrast(90%) saturate(270%);
55-
border-bottom:solid .18rem rgba(0,0,255,.22);
56-
border-left:solid .2rem rgba(255,0,0,.15);
57-
border-right:solid .2rem rgba(111,0,111,.4);
55+
border-bottom:dotted .17rem rgba(0,0,255,.2);
56+
border-left:dotted .2rem rgba(255,0,0,.15);
57+
border-right:dotted .22rem rgba(111,0,111,.18);
5858
border-radius:0 0 2rem 2rem;}
5959
.scrolled-down:hover {background:linear-gradient(to top, rgba(0, 0, 0, .6), rgba(128, 128, 128, 0.6));opacity:1;}
6060
.scrolled-down {height:3rem;
@@ -210,6 +210,7 @@ article {min-height:80vh;border:dotted 1px rgba(255,255,0,.1);}
210210
border-left:inset .7rem rgba(100,50,255,.3);
211211
border-right:inset .7rem rgba(11,100,100,.3);
212212
scale:1.02 1.011;}
213+
section {padding-bottom:2rem;}
213214
.section-title {all:unset !important;cursor:pointer !important;text-align:center !important;}
214215
.section-title h3:hover {text-decoration:underline;}
215216
.section-nav {background:linear-gradient(to right, rgb(22 222 28 / 25%) 0%, rgb(110 0 145 / 45%) 50%, rgb(222 114 22 / 25%) 100%);
@@ -256,14 +257,17 @@ article {min-height:80vh;border:dotted 1px rgba(255,255,0,.1);}
256257
border:inset .15rem rgba(100,100,100,.2);
257258
border-bottom:solid .2rem rgba(255,0,0,.2);
258259
border-top:0;
259-
margin-bottom:.07rem;}
260+
margin-bottom:.07rem;
261+
padding:.3rem 0 .5rem 0;
262+
border-radius:.9rem;}
260263
.article-content {border:solid 1px rgba(255,0,0,.2);
261264
background-image:url("../img/bginvert.png");
262265
background-attachment:fixed;
263266
background-size:10000px;
264267
background-position-x:-200px;
265-
padding:0 0 1rem .2rem;
268+
padding:0 0 3rem .2rem;
266269
margin:0 0 .07rem 0;
270+
border-radius:.75rem;
267271
backdrop-filter:brightness(90%) contrast(250%) grayscale(50%) blur(.1em) saturate(120%);}
268272
.article-content:hover {background:radial-gradient(rgba(128, 128, 128, 0.1), rgba(0, 0, 0, 0.1));
269273
backdrop-filter:brightness(95%) contrast(200%) grayscale(50%) blur(.1em) saturate(120%);}

0 commit comments

Comments
 (0)