0000# 👋👋👋 hi. this is blog from zac skalko.
0001
0002https://twitter.com/zapplebee
0003
0004https://github.com/zapplebee/droplet
0005
0006I have started way too many blogs and got caught up on the making it and not the writing.
0007
0008this is just plain text for that reason.
0009
0010---
0011
0012i started setting up my droplet today.
0013
0014here's a log.
0015
0016i created a droplet on digital ocean today.
0017
00181 GB Memory / 25 GB Disk + 10 GB / SFO3 - Docker 23.0.6 on Ubuntu 22.04
0019
0020I mounted the storage separately because i can easily move to another droplet if i choose.
0021
0022with docker on it, i can just try a bunch of languages and images.
0023
0024after getting it set up with ssh by public key from my mac mini at home, i logged in.
0025
0026the lockfile for apt-get was broken on the first install.
0027
0028i tried to remove the lock file and retry. no dice. i restarted the machine and it was fine.
0029
0030the first real tool i installed was vs code server. it made it super easy to just drive this machine like it's my local machine.
0031
0032after that, opened up http and https with ufw
0033
0034```
0035ufw open http
0036ufw open https
0038```
0039
0040i wanted a vert simple way to serve something and make sure i could reach the device from the internet, not just over ssh
0041
0042i installed bun and made a very simple http handler.
0043
0044```
0045curl -fsSL https://bun.sh/install | bash
0047```
0048
0049dang zip was missing. i installed with
0050
0051```
0052apt-get install zip
0053```
0054
0055i figured i should install build essential too
0056
0057```
0058apt-get install build-essential
0059```
0060
0061now i could install bun.
0062
0063and run
0064
0065```
0066Bun.serve({
0067 port: 80, // defaults to $BUN_PORT, $PORT, $NODE_PORT otherwise 3000
0068 hostname: "0.0.0.0", // defaults to "0.0.0.0"
0069 fetch(req) {
0070 return new Response("h e l l o w o r l d");
0071 },
0072 });
0074```
0075
0076started it. i couldn't hit it from the internet.
0077
0078digital ocean required that i also create a firewall rule
0079
0080so i did. 22, 80, 443
0081
0082hit it from the internet and got a response 💪
0083
0084great. now i needed to set up dns.
0085
0086i went to my trusty https://freedns.afraid.org/
0087
0088set the old one that was pointing to my home server to the droplet's address
0089
0090it took some time to propagate.
0091
0092from chrome though, all i was getting was ERR_CONNECTION_REFUSED
0093
0094i tried to curl it from my local machine
0095
0096```
0097➜ ~ curl zapplebee.prettybirdserver.com
0098h e l l o w o r l d%
0099```
0100
0101i got back the expected response.
0102
0103so what is going on with chrome.
0104
0105i went to the network tab and copied the request as curl.
0106
0107i decided that it might be something a browser call is enforcing, so i should try all the same everything (cookies headers etc) from another spot.
0108
0109```
0110➜ ~ curl 'https://zapplebee.prettybirdserver.com/' \
0111 -H 'Upgrade-Insecure-Requests: 1' \
0112 -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
0113 -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
0114 -H 'sec-ch-ua-mobile: ?0' \
0115 -H 'sec-ch-ua-platform: "macOS"' \
0116 --compressed
0117curl: (7) Failed to connect to zapplebee.prettybirdserver.com port 443: Connection refused
0119```
0120
0121i realized that chrome was trying to reach the https port 443. on which i had nothing running yet
0122
0123but it was hitting the machine
0124
0125just not in a way that i could see.
0126
0127i could now set up certbot.
0128
0129```
0130snap install --classic certbot
0131```
0132
0133i dont really have a proxy or load balancer decided on yet, so i was going to just use certbot without auto configuration.
0134
0135https://www.digitalocean.com/community/tutorials/how-to-use-certbot-standalone-mode-to-retrieve-let-s-encrypt-ssl-certificates-on-ubuntu-20-04
0136
0137i created the certs manually
0138
0139```
0140certbot certonly --standalone -d zapplebee.prettybirdserver.com
0142...
0143Successfully received certificate.
0144Certificate is saved at: /etc/letsencrypt/live/zapplebee.prettybirdserver.com/fullchain.pem
0145Key is saved at: /etc/letsencrypt/live/zapplebee.prettybirdserver.com/privkey.pem
0146This certificate expires on 2024-04-12.
0148```
0149
0150once i set up a service that needs to restart on renewal i can update this line in the conf file, `/etc/letsencrypt/renewal/zapplebee.prettybirdserver.com.conf`
0151
0152```
0153renew_hook = systemctl reload your_service
0154```
0155
0156now i have to look up how to serve with the certs from bun.
0157
0158the simplest thing in the world.
0159
0160```
0161Bun.serve({
0162 port: 443,
0163 hostname: "0.0.0.0",
0164 fetch(req) {
0165 return new Response("h e l l o w o r l d");
0166 },
0167 tls: {
0168 cert: Bun.file(
0169 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/cert.pem",
0170 ),
0171 key: Bun.file(
0172 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/privkey.pem",
0173 ),
0174 },
0175});
0176```
0177
0178now i just need to handle directing traffic to https
0179
0180```
0182Bun.serve({
0183 port: 443,
0184 hostname: "0.0.0.0",
0185 fetch(req) {
0186 return new Response("h e l l o w o r l d");
0187 },
0188 tls: {
0189 cert: Bun.file(
0190 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/cert.pem",
0191 ),
0192 key: Bun.file(
0193 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/privkey.pem",
0194 ),
0195 },
0196});
0198Bun.serve({
0199 port: 80,
0200 hostname: "0.0.0.0",
0201 fetch(req) {
0202 return Response.redirect(`${req.url.replace(/^http:/gi, "https:")}`, 302);
0203 },
0204});
0206```
0207
0208https://regexr.com/ is my trusty regex helper site.
0209
0210this was substantially easier than any proxy config i have ever used.
0211
0212it's alive.
0213
0214```
0216➜ ~ curl zapplebee.prettybirdserver.com -L --verbose
0217* Trying 159.223.202.91:80...
0218* Connected to zapplebee.prettybirdserver.com (159.223.202.91) port 80 (#0)
0219> GET / HTTP/1.1
0220> Host: zapplebee.prettybirdserver.com
0221> User-Agent: curl/7.77.0
0222> Accept: */*
0224* Mark bundle as not supporting multiuse
0225< HTTP/1.1 302 Found
0226< Location: https://zapplebee.prettybirdserver.com/
0227< Date: Sat, 13 Jan 2024 03:08:35 GMT
0228< Content-Length: 0
0230* Connection #0 to host zapplebee.prettybirdserver.com left intact
0231* Issue another request to this URL: 'https://zapplebee.prettybirdserver.com/'
0232* Trying 159.223.202.91:443...
0233* Connected to zapplebee.prettybirdserver.com (159.223.202.91) port 443 (#1)
0234* ALPN, offering h2
0235* ALPN, offering http/1.1
0236* successfully set certificate verify locations:
0237* CAfile: /etc/ssl/cert.pem
0238* CApath: none
0239* TLSv1.2 (OUT), TLS handshake, Client hello (1):
0240* TLSv1.2 (IN), TLS handshake, Server hello (2):
0241* TLSv1.2 (IN), TLS handshake, Certificate (11):
0242* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
0243* TLSv1.2 (IN), TLS handshake, Server finished (14):
0244* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
0245* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
0246* TLSv1.2 (OUT), TLS handshake, Finished (20):
0247* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
0248* TLSv1.2 (IN), TLS handshake, Finished (20):
0249* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
0250* ALPN, server did not agree to a protocol
0251* Server certificate:
0252* subject: CN=zapplebee.prettybirdserver.com
0253* start date: Jan 13 01:40:23 2024 GMT
0254* expire date: Apr 12 01:40:22 2024 GMT
0255* subjectAltName: host "zapplebee.prettybirdserver.com" matched cert's "zapplebee.prettybirdserver.com"
0256* issuer: C=US; O=Let's Encrypt; CN=R3
0257* SSL certificate verify ok.
0258> GET / HTTP/1.1
0259> Host: zapplebee.prettybirdserver.com
0260> User-Agent: curl/7.77.0
0261> Accept: */*
0263* Mark bundle as not supporting multiuse
0264< HTTP/1.1 200 OK
0265< content-type: text/plain;charset=utf-8
0266< Date: Sat, 13 Jan 2024 03:08:35 GMT
0267< Content-Length: 21
0269* Connection #1 to host zapplebee.prettybirdserver.com left intact
0270h e l l o w o r l d%
0272```
0273
0274---
0275
0276the first bug was discovered.
0277
0278by default, bun did not read the encoding of the file that was read from disk.
0279
0280i guess i assumed it would.
0281
0282my good friend Ryan Rampersad https://twitter.com/ryanmr pointed out that the unicode characters where buggy.
0283
0284i looked and saw that the server was responding with a `Content-type` header of `text/markdown`.
0285
0286Bun had identified this but did not automatically set the additional encoding information.
0287
0288it was changes to `text/markdown; charset=utf-8`
0289
0290https://github.com/zapplebee/droplet/commit/bb3e0c4b7f4860ed59d7c6789d2ba8752b24614a
0291
0292now it works as expected.
0293
0294---
0295
0296i need a way to run this locally that ignores the certs
0297
0298the way it will work is based on the NODE_ENV, i'll create the configuration differently.
0299
0300though i don't love that i am going to bake this directly into the entrypoint file, the app is still very simple and it's okay. the goal is to make some thing that can be changed.
0301
0302in the .bashrc file i'll export the NODE_ENV.
0303
0304```
0305export NODE_ENV=production
0306```
0307
0308that way, anytime i am running something on this machine it will automatically be set.
0309
0310right now the service is just being run in the background.
0311
0312```
0313bun index.ts &
0314```
0315
0316While bun does support a watch mode, i dont really want to think about memory leaks yet so i am just going to make a handy alias to stop and restart it.
0317
0318to find the running process in the background, I'm running
0319
0320```
0321ss -lptn 'sport = :80'
0322State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0323LISTEN 0 512 0.0.0.0:80 0.0.0.0:* users:(("bun",pid=1562,fd=14))
0324```
0325
0326while this is good, i'm going to need to trim that output so i can get just the process id and kill it
0327
0328i can use `awk` for this.
0329
0330```
0331ss -lptn 'sport = :80' | awk 'NR > 1 {print $6}'
0332users:(("bun",pid=1562,fd=14))
0334```
0335
0336this skips the first line, the table headers, and then takes the sixth column value.
0337
0338still not quite just the process id.
0339
0340a couple cuts will do the trick
0341
0342```
0343ss -lptn 'sport = :80' | awk 'NR > 1 {print $6}' | cut -d= -f2 | cut -d, -f1
03441562
0345```
0346
0347this gives me just the process id of the server that is listening on port 80. i could have used the https port 443 instead but this is fine since it has to serve both for the redirects.
0348
0349next i have to pass that var into a `kill` command.
0350
0351I can do that with a command substitution.
0352
0353```
0354kill $(ss -lptn 'sport = :80' | awk 'NR > 1 {print $6}' | cut -d= -f2 | cut -d, -f1)
0355```
0356
0357after that i just need to start the service again.
0358
0359```
0361kill $(ss -lptn 'sport = :80' | awk 'NR > 1 {print $6}' | cut -d= -f2 | cut -d, -f1) & bun /mnt/volume_sfo3_01/apps/helloworld/index.ts &
0363```
0364
0365i'll add that as an alias in the `.bashrc` file
0366
0367```
0368alias restartbun="kill $(ss -lptn 'sport = :80' | awk 'NR > 1 {print $6}' | cut -d= -f2 | cut -d, -f1) & bun /mnt/volume_sfo3_01/apps/helloworld/index.ts &"
0369```
0370
0371this gives me a couple advantages right now.
0372
03731. there's no auto deploy. i have to pull from git or change the files on the server and restart. this will let me make a mess of things and not have an auto watcher restarting the server if it's not in a good state.
03742. it's based on what port is exposed, so if i change the actual edge listener to a proxy or something, i can restart it with pretty much the same command.
0375
0376---
0377
0378okay, now i can finally set up the application code to serve certs if we're in production mode.
0379
0380```
0382const PRODUCTION_CONFIG = {
0383 port: 443,
0384 tls: {
0385 cert: Bun.file(
0386 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/cert.pem",
0387 ),
0388 key: Bun.file(
0389 "/etc/letsencrypt/live/zapplebee.prettybirdserver.com/privkey.pem",
0390 ),
0391 },
0392} as const;
0394const DEV_CONFIG = {
0395 port: 3100
0396} as const;
0398const IS_PRODUCTON = process.env.NODE_ENV === 'production';
0400const LIVE_CONFIG = IS_PRODUCTON ? PRODUCTION_CONFIG : DEV_CONFIG;
0402Bun.serve({
0404 hostname: "0.0.0.0",
0405 fetch(req) {
0406 return new Response(Bun.file("/mnt/volume_sfo3_01/apps/notes/setting-up-the-droplet.md"), {
0407 headers: {
0408 'Content-type': 'text/markdown; charset=utf-8'
0410 });
0411 },
0412 ...LIVE_CONFIG
0413});
0416if(IS_PRODUCTON) {
0417 // no need to serve the redirects if we're not in prod
0418 Bun.serve({
0419 port: 80,
0420 hostname: "0.0.0.0",
0421 fetch(req) {
0422 return Response.redirect(`${req.url.replace(/^http:/gi, "https:")}`, 302);
0423 },
0424 });
0429```
0430
0431here's the changes
0432
0433https://github.com/zapplebee/droplet/commit/73933eee9417f10c9139d4875cc6f292696ff9ab
0434
0435---
0436
0437after setting this up i realized i dont have prettier installed in the droplet, and since i am doing a lot of authoring directly on it via ssh, i might want to do that in the future.
0438
0439// TODO set up prettier and git hooks i guess
0440
0441---
0442
0443okay now that i have a dev mode i can start to actually add some features to this thing.
0444
0445first of all, i would prefer some minimal styling...
0446
0447and i do mean minimal.
0448
0449and to actually serve this as html
0450
0451so far i don't have any dependencies and it might be good to keep it that way for a while.
0452
0453bun gives me a lot out of the box and would like to keep dependencies to a minimum
0454
0455first, i'll read up all the markdown paths.
0456
0457```
0458export async function getFilePaths(): Promise<Array<string>> {
0459 const glob = new Glob("*.md");
0461 const filepaths: Array<string> = [];
0463 for await (const file of glob.scan(NOTES_DIRECTORY)) {
0464 filepaths.push(`${NOTES_DIRECTORY}${file}`);
0467 return filepaths;
0470```
0471
0472then i'll mash em together for now
0473
0474```
0475export async function getAsHtml(): Promise<string> {
0476 const filepaths = await getFilePaths();
0477 const files = filepaths.map((e) => Bun.file(e));
0479 const fileContents = await Promise.all(files.map((e) => e.text()));
0481 const rawBody = fileContents.join("\n---\n");
0483 return `<!DOCTYPE html>
0484<html><body><style>* {background-color: black; color: green;}</style><pre>${Bun.escapeHTML(rawBody)}</pre></body></html>`;
0487```
0488
0489at last i'll import it in the index.
0490
0491since i want this to fail early, ie at start up, i'll use bun's macro capability to do it.
0492
0493```
0494import { getAsHtml } from "./files" with { type: "macro" };
0495```
0496
0497this runs the getting and escaping of the files at start up and in-lines the result into the AST.
0498
0499https://bun.sh/docs/bundler/macros
0500
0501not super useful now since i am not bundling, but an appropriate use of a macro
0502
0503i also added gzip it was very simple.
0504
0505```
0506const HTML_CONTENT = await getAsHtml();
0508const data = Buffer.from(HTML_CONTENT);
0509const compressed = Bun.gzipSync(data);
0511Bun.serve({
0512 hostname: "0.0.0.0",
0513 fetch(req) {
0514 return new Response(compressed, {
0515 headers: {
0516 "Content-Encoding": "gzip",
0517 "Content-type": "text/html; charset=utf-8",
0518 },
0519 });
0520 },
0521 ...LIVE_CONFIG,
0522});
0524```
0525
0526---
0527
0528although I do really like this just one long file method,
0529there's something not quite right about it.
0530
0531and that's the ability to link to a specific piece of content.
0532
0533I think that the best course of action is just to create id anchors for every line.
0534
0535then one can easily deep link directly to a place on the page.
0536
0537there's a couple things that need to happen in order for that to work as expected.
0538
05391. i need to split the file line by line.
05402. map those back together with a link at the start of the line.
05413. create a line count column on the left site that has a link to the line itself.
0542
0543as i was doing this i thought i might need to start to tag where i am in the git repo so that if someone (me) wanted to follow along in the future, it would be easy
0544
0545```
0546git show-ref
05473590fff8df7ecfcebd7f9379637977403549f62e refs/heads/main
05483590fff8df7ecfcebd7f9379637977403549f62e refs/remotes/origin/HEAD
05493590fff8df7ecfcebd7f9379637977403549f62e refs/remotes/origin/main
0550```
0551
0552i sure could just let people do a git blame. but they might just be reading this as plain text.
0553
0554a few edits to the html file and bang we we have direct links.
0555
0556i am fighting my a lot of my instincts to not bring in react at this stage as the markup is getting just a bit complex.
0557
0558as my friend ardeshir (https://hachyderm.io/@sepahsalar) said:
0559
0560"Yep, you start adding A 🏷️ s and next thing you know, you’ve written a new RSC framework"
0561
0562```
0564export async function getAsHtml(): Promise<string> {
0565 const filepaths = await getFilePaths();
0566 const files = filepaths.map((e) => Bun.file(e));
0567 const fileContents = await Promise.all(files.map((e) => e.text()));
0568 const rawBody = fileContents.join("\n---\n").replaceAll("\r", "");
0569 const escapedBody = Bun.escapeHTML(rawBody);
0570 const bodyLines = escapedBody.split("\n");
0571 const maxCharactersInLineNumber = String(bodyLines.length).length;
0573 return `<!DOCTYPE html>
0574<html><body><style>* {background-color: black; color: #4d9c25;} .line-link {color: #2f5c19;} .space, .line-link { -webkit-user-select: none; -ms-user-select: none; user-select: none;}</style><pre>${bodyLines
0575 .map((line, index) => {
0576 const lineNumber = String(index).padStart(maxCharactersInLineNumber, "0");
0577 const lineId = `line-${lineNumber}`;
0579 return `<span class="line" id="${lineId}"><a class="line-link" href="#${lineId}">${lineNumber}</a><span class="space">&nbsp;&nbsp;&nbsp;&nbsp;</span><span>${line}</span></span>`;
0580 })
0581 .join("\n")}</pre></body></html>`;
0585```
0586
0587the next thing i want to do is enable a line wrap mode for mobile.
0588
0589but i dont want the markdown style codeblocks wrap.
0590
0591so i first need to make the codeblocks wrapped in a new node.
0592
0593i'll have to do some refactoring of the page builder.
0594
0595as soon as I change styles in there to a static CSS file, i was already starting to get irritated by the lack of structure in code.
0596
0597there's literally one handler. it should be easy to change, and it is, the hard part is thinking about what I want to change it to.
0598
0599i was really trying defer design decisions because, well, i dont want to think about it too hard.
0600
0601this is supposed to be a fun little project.
0602
0603i guess to keep this as simple as possible, i should just write vanilla css and i really need to vanilla js and just author and host them from a public folder
0604
0605It's time that I actually messed with these Link headers.
0606
0607This should allow the network requests to start as soon as the document loads.
0608
0609https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
0610
0611That will keep the doc in cache until it used by the page.
0612
0613I need to modify the response that the html request returns
0614
0615```
0616return new Response(HTML_RESPONSE_BODY, {
0617 headers: {
0618 "Content-Encoding": "gzip",
0619 "Content-type": "text/html; charset=utf-8",
0620 Link: `</public/main.css>; rel="prefetch"; as="style";`,
0621 },
0622});
0623```
0624
0625Then, in the actual CSS in the HTML, I can import it for free.
0626
0627and it's already been loaded
0628
0629---
0630
0631added a little magic to the html renderer. not my best code ever. but it does the job until i find a real framework i want to use for this.
0632
0633````
0635export async function getAsHtml(): Promise<string> {
0636 const filepaths = await getFilePaths();
0637 const files = filepaths.map((e) => Bun.file(e));
0638 const fileContents = await Promise.all(files.map((e) => e.text()));
0639 const rawBody = fileContents.join("\n---\n").replaceAll("\r", "");
0640 const escapedBody = Bun.escapeHTML(rawBody);
0641 const bodyLines = escapedBody.split("\n");
0642 const maxCharactersInLineNumber = String(bodyLines.length).length;
0644 let inCodeBlock = false;
0646 return `<!DOCTYPE html>
0647<html><head></head><body><style>@import "/public/main.css";</style><pre>${bodyLines
0648 .map((line, index) => {
0649 const linkedLine = line.replaceAll(
0650 /(https:\/\/[^\s\)]+)/gi,
0651 '<a href="$&">$&</a>'
0652 );
0653 const lineNumber = String(index).padStart(maxCharactersInLineNumber, "0");
0654 const lineId = `line-${lineNumber}`;
0656 const isBackticks = line.trim() === "```";
0658 if (isBackticks) {
0659 inCodeBlock = !inCodeBlock;
0662 const inCode = Boolean(inCodeBlock || isBackticks);
0664 return `<div class="line" id="${lineId}"><a class="line-link" href="#${lineId}">${lineNumber}</a><span class="space">&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="${inCode ? "codeblock" : ""}">${inCode ? line : linkedLine}</span></div>`;
0665 })
0666 .join("\n")}</pre></body></html>`;
0669````
0670
0671just a little magic
0672
0673---
0674
0675whoops i wrote a bunch of code without documenting it.
0676
0677i just needed to experiment with the styling.
0678
0679I actually should review and refactor at some point because there is too many
0680dom nodes for what i am trying to do here.
0681
0682---
0683
0684I refactored the render function a little to make it more readable.
0685
0686Broke the massive template string into a few variables and mashed them together.
0687
0688Variable names are free documentation.
0689
0690````
0691export async function getAsHtml(): Promise<string> {
0692 const filepaths = await getFilePaths();
0693 const files = filepaths.map((e) => Bun.file(e));
0694 const fileContents = await Promise.all(files.map((e) => e.text()));
0695 const rawBody = fileContents.join("\n---\n").replaceAll("\r", "");
0696 const escapedBody = Bun.escapeHTML(rawBody);
0697 const bodyLines = escapedBody.split("\n");
0698 const maxCharactersInLineNumber = String(bodyLines.length).length;
0700 let inCodeBlock = false;
0702 const headTags = `
0703<title>zapplebee.prettybirdserver.com</title>
0704<style>@import "/public/main.css";</style>
0707 const head = `<!DOCTYPE html>
0708<html><head>${headTags}</head><body><main>`;
0710 const tail = `</main></body></html>`;
0712 const main = bodyLines.map((line, index) => {
0713 const lineNumber = String(index).padStart(maxCharactersInLineNumber, "0");
0714 const lineId = `line-${lineNumber}`;
0716 const isBackticks = line.startsWith("```");
0718 let addCodeBlockOpenTag = false;
0719 let addCodeBlockCloseTag = false;
0721 if (isBackticks && !inCodeBlock) {
0722 addCodeBlockOpenTag = true;
0725 if (isBackticks) {
0726 inCodeBlock = !inCodeBlock;
0729 if (!inCodeBlock && isBackticks) {
0730 addCodeBlockCloseTag = true;
0733 const inCode = Boolean(inCodeBlock || isBackticks);
0735 const lineText = inCode
0736 ? line
0737 : line.replaceAll(/(https:\/\/[^\s\)]+)/gi, '<a href="$&">$&</a>');
0739 const containerOpenTag = addCodeBlockOpenTag
0740 ? `<div class="codeblock-container"><div class="codeblock-wrapper">`
0741 : "";
0743 const openingLineTag = `<div class="line" id="${lineId}">`;
0744 const lineIdxElement = `<a class="line-link" href="#${lineId}">${lineNumber}</a>`;
0745 const lineStrElement = `<span class="${inCode ? "codeblock" : "prose"}">${lineText}</span>`;
0746 const closingLineTag = `</div>`;
0747 const containerCloseTag = addCodeBlockCloseTag ? `</div></div>` : "";
0749 return [
0750 containerOpenTag,
0751 openingLineTag,
0752 lineIdxElement,
0753 lineStrElement,
0754 closingLineTag,
0755 containerCloseTag,
0756 ].join("");
0757 }).join("");
0759 return [head, main, tail].join("");
0762````
0763
0764as for the styles. i found a new property that i have never run into before
0765
0766```
0767 -moz-text-size-adjust: none;
0768 -webkit-text-size-adjust: none;
0769 text-size-adjust: none;
0770```
0771
0772I could not figure out what was making my fonts all messed up
0773despite the `!important` tag.
0774
0775This is what I get for using somebody else's CSS reset all the time.
0776
0777Finally made the CSS into something i could tolerate.
0778It's interesting how dependant I have become on CSS-in-JS or tailwind
0779
0780This was harder than I remember it being.
0781
0782```
0784:root {
0785 --textwidth: 40ch;
0786 --linenumberwidth: 3ch;
0787 --gapwidth: 2ch;
0790@media (min-width: 500px) {
0791 :root {
0792 --textwidth: 80ch;
0796* {
0797 padding: 0;
0798 margin: 0;
0799 box-sizing: border-box;
0800 line-height: 1.2rem;
0801 font-size: 16px;
0802 font-weight: 400;
0803 color: #4d9c25;
0804 -moz-text-size-adjust: none;
0805 -webkit-text-size-adjust: none;
0806 text-size-adjust: none;
0809body {
0810 background-color: black;
0811 display: block;
0814main {
0815 font-family: monospace;
0816 width: calc(var(--textwidth) + var(--linenumberwidth) + var(--gapwidth));
0817 margin: auto;
0818 display: block;
0821.line {
0822 display: flex;
0823 flex-direction: row;
0824 gap: var(--gapwidth);
0826.line-link {
0827 color: #2f5c19;
0828 -webkit-user-select: none;
0829 -ms-user-select: none;
0830 user-select: none;
0831 white-space: pre;
0832 width: var(--linenumberwidth);
0835.codeblock-container {
0836 width: calc(var(--textwidth) + var(--linenumberwidth) + var(--gapwidth));
0837 overflow-x: auto;
0838 overflow-y: hidden;
0839 background-color: rgb(63, 63, 63);
0842.codeblock {
0843 color: rgb(215, 246, 152);
0844 white-space: pre;
0847.prose {
0848 width: var(--textwidth);
0849 white-space: pre-wrap;
0850 overflow-wrap: break-word;
0853::-webkit-scrollbar {
0854 height: 1rem;
0855 width: 1ch;
0856 background: rgb(63, 63, 63);
0859::-webkit-scrollbar-thumb {
0860 background: rgb(215, 246, 152);
0863::-webkit-scrollbar-corner {
0864 background: rgb(63, 63, 63);
0867```
0868
0869---
0870
0871the next thing i am going to have to figure out for this is images.
0872
0873they're tricky for a couple reasons.
0874
08751. the style of this website doesn't really have anything that spans too
0876 much vertical space
08772. they cost bandwidth in a way that the plain text just doesn't.
08783. i am trying to not load and js in the client for performance reasons
0879 if i want to do any tricks to save bandwidth or just allow a peek at the image
0880 that's going to be tough without js
0881
0882I have a bit of an idea
0883
0884I could load like, two lines of the image, and add a button to expand.
0885
0886like this
0887
0888<!-- prettier-ignore-start -->
0889
0890imageurl: an-image.jpg
0891███████████████████████
0892███████████████████████
0893███ click to expand ███
0894
0895
0896imageurl: an-image.jpg
0897██████████████████████
0898██████████████████████
0899██████████████████████
0900███ █████████ █████
0901██████████████████████
0902█ ██████████████ ███
0903███ ██████████ ████
0904████ ██████
0905██████████████████████
0906██████████████████████
0907███ click to close ███
0908
0909<!-- prettier-ignore-end -->
0910
0911that could be done with just a checkbox and some custom styles
0912
0913but it wouldn't be very accessible.
0914
0915so far, as strange as the set of this site is, it is rather accessible.
0916
0917though i could bump up the contrast from the bg and the text,
0918
0919there's not a busy menu to navigate through via keyboard.
0920
0921there's no animations.
0922
0923theres' no dynamic loading of content.
0924
0925the other accessibility thing I should do is probably add image roles for the emoji
0926
0927https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role
0928
0929(exactly unlike above...)
0930
0931i would also like a way to be able to add content from my phone.
0932
0933this is meant, in part, as a replacement for _centralized microblogging platform formerly known as twitter_
0934in case you couldn't tell from my typing voice. haha.
0935
0936i tried mastodon, but i just didn't get it or something.
0937
0938so the ability to add content whenever, whereever would be nice.
0939
0940stretch goal would be to add the ability to comment on lines.
0941
0942one of the goals of this site is to practice progressive by incremental improvement of this feed.
0943backwards compatibility too.
0944
0945since right now this is just a giant markdown file,
0946it could obviously be replatformed.
0947
0948the other goal is that all the changes to the site get logged here.
0949
0950think of it as a git log + architecture decision record + readme + bullet journal + social media
0951
0952the simple form of it has let me write almost 1000 lines already!
0953
0954in fact
0955
0956TODO: fix the fact that right now the line number width is fixed to three characters
0957
0958i'd also like some kind of convention for being able to write long form segments that get their own page
0959
0960something like
0961
0962<POST slug="first-actual-post" summary="This is the first real post other than the endless log">
0963# hello world.
0965this is the first actual blog post of the site. the log is there for everything but
0966sometimes i will want to be able to write specific content.
0967</POST>
0968
0969honestly, i think that i'm happy enought with that as a pattern
0970
0971the landing page of the site should show something like the last 10 lines of
0972the log with a link to the full log
0973
0974and then posts as links.
0975
0976---
0977
0978i was trying to set some html meta tags.
0979
0980as usual, i stole from ryan
0981
0982https://github.com/ryanmr/blog/blob/main/src/components/BaseHead.astro
0983
0984---
0985
0986<POST slug="the-react-empire" description="Thoughts on the React ecosystem as it enters its naidir of simplicity">
0988I recently read this post from [Cassidy Williams](https://twitter.com/cassidoo) on [being a little disappointed with React](https://blog.cassidoo.co/post/annoyed-at-react/)
0990An awful lot of it resonated with me so I thought I would try to put my own thoughts into words on it.
0992When I was just a little computer boy, there was _no_ good way to write real application code that lived in the browser.
0994It was either a ton of boilerplate `createElement` or JQuery. And while both were really powerful, they required a lot of code to do some pretty plain things by today's standards.
0996And they were imperative, rather than declarative.
0998Meaning instead of indicating how you would want the DOM to look, you were giving instructions about how to mutate the DOM.
1000Eventually, I discovered AngularJS. At this time I was still in my adolescence with real software. So it was an amazing journey to be able to write JavaScript apps by just adding a new script tag from a CDN url.
1002I was still writing apps in Notepad++ (Or maybe it was already Sublime Text by then) and FTPing them to the server.
1004Templates for the way the DOM was described in AngularJS was [based on strings](https://docs.angularjs.org/guide/component#components-as-route-templates) or XHR requests to other HTML documents with custom directives specifically to attach into the AngularJS state and events.
1006This was a huge win for me. I was very tired of creating elements, or homebrewing my own abstractions for creating elements.
1008It did have one weakness though. There wasn't a lot of help to make sure what you were writing would work as expected since the templates were just strings. You kind of just had to run it and make sure that you connected your state and handlers correctly.
1010And I was still writing a lot of code to manage data going up and down the tree.
1012Ultimately, it did still make it much easier to work with. AND I STILL DIDN'T NEED ANYTHING BUT TO UPLOAD THE FILES.
1014I built a lot in AngularJS.
1016At the time I was running a couple PHP servers and with some local databases.
1018I had tried with Java and a few other languages, but as an independently taught dev, it was really hard to go from zero knowledge to a running server.
1020🫠 figuring out a build toolchain and a compiler when you are still working hard to remember that environmental variables are set per shell.
1021😎 file makes webpage.
1023It was simple. Simple was King (and still is).
1025Enter React.
1027By now, I was pretty comfortable in my space and had started to experiment with NodeJS.
1029Wouldn't it be cool if the whole stack was the same language? That would make skills I learned in one place make me even stronger. I was still leaning about the language after all (and still am -- it's constantly evolving).
1031I went to a local conference and everyone was talking about React.
1033It was mesmerizing what they could build with a little bit of code. But surely I could never get a foothold into this, I mean that wasn't JS they were writing, right?
1035Yes but no.
1037JSX was transformative. Despite it being just a syntax over a `createElement` function, it totally changes the way that one could reason about the DOM tree.
1039It was a good abstraction. It made the important information very visible and made the irrelevant (read as boilerplate) almost disappear.
1041The transformation that it enabled in reasoning with templates in JS also required another transformation. Source JSX -> JavaScript ready for the browser.
1043Today, there is a lot more in the ecosystem that could help you do this kind of transform. At the time there really wasn't, and even if there was I would don't think I could set that up from scratch at the time. Even by following step-by-step instructions.
1045The real magic, was that React also published `create-react-app`. An all-things-included bundler, application scaffold, example application that could easily start to grow from a single page to whatever you wanted it to be -- all while hiding the magic that transformed JSX to JS.
1047A person could get addicted to the ergonomics of the framework before ever having to think about the how.
1049Eventually, like all computer people, I became too curious for my own good and ejected a React App.
1051Ejecting a `create-react-app` would take all the behind-the-scenes magic that was happening and plop it into your work directory.
1053I couldn't get enough -- webpack, babel, the whole thing. I learned about ASTs, I learned about compilers. It was a magical time of growth for me. It didn't hurt that at the time I was working at a newish job where the team's ethic was "let the people focus".
1055Despite all this being in front of me, it would still take a ton to learn before I could really effect change with that configuration.
1057And while I was figuring that out, my app would still just build and run. **The app still felt simple**. I was never more productive in my life.
1059Then came hooks.
1061We no longer had to utilize so many language features to interact with the state and the DOM. In fact we barely had to learn about the framework and its lifecycle to be able to write code for it.
1063An even better refinement on the abstraction. A truly good abstraction should enable you to look at a piece of code and see what its intent is.
1065```js
1066const [isOn, setIsOn] = useState(false);
1067```
1069Even if you've never seen JavaScript, you may be able to reason about that.
1071The patterns let you think about what you're doing. Not what the framework is doing.
1073---
1075There is unquestionably a new age for React. It's not just React Server Components. It's a new world that it lives in.
1077The primary, suggested. ways to start a React project is a tool is not maintained as side-by-side with React itself.
1081React is no longer suggesting that you start with the simplest way to build and deploy and gradually add more complexity. Instead, you get all that complexity on day one.
1083---
1085Let's take Next as the focal point of the rest of this discussion. Because, like it or not, it is become a defacto _next_ step.
1087We now have must specify a boundry about where our components can run.
1089```js
1090"use client";
1091```
1093Meaning that we have to constantly think about if a piece of code has access to the DOM in a browser or not.
1095This is all at the file level and not the component level meaning that the handy co-location of mini components gets broken.
1097Routing is based on a filesystem convention, or is it? Next already has two divergent page path conventions.
1099And while it purports to be a full-stack framework, God help you if you want to use something you learned in Node, because sometimes you have access to that API and sometimes you don't.
1101Next, and the other full-stack React frameworks have taken us back to having to think about what the framework is doing. It's a step backwards for development experience in a big way.
1103---
1105Complexity should only ever be added to an application as a decision of the product that is building that application. React has abandoned this mentality.
1107It's still totally possible to build or use React for little pieces of your website, but it's not even approached as a concept in the docs.
1109I still reach for a React app (not `create-react-app`, usually with Vite now) when I want to build something with any amount of rapidity. But less so every day -- and certainly not in the way that it is described in it's documentation.
1111JSX will survive this curve away from the simplicty of a client-side React application. Whether or not React remains king is uncertain. Remember, simplicity is king.
1113</POST>
1114
1115as i was writing this post i got a little distracted by making it fancy.
1116
1117but i needed someway to deep link it.
1118
1119i decided that while the hashes did work, it wasn't great for the future if i ever want to capture where traffic is going
1120
1121so instead now i one single line of javascript on the client.
1122
1123```
1124 let scrollToScript = "";
1125 if (focusId && ids.has(focusId)) {
1126 scrollToScript = `<script>window['${focusId}'].scrollIntoView(true);</script>`;
1128```
1129
1130---
1131
1132while trying to get meta tags working i discovered that there was a certificate problem that was preventing crawlers from getting to the page.
1133
1134so i decided it was time to move from the https://freedns.afraid.org/ subdomain i was using and buy my own
1135
1136https://zapplebee.online.com
1137
1138i didnt wait long enough for DNS to propagate before trying to set up certs.
1139
1140so now i have to wait an hour
1141
1142```
1144certbot certonly --standalone -d zapplebee.online
1145Saving debug log to /var/log/letsencrypt/letsencrypt.log
1146Requesting a certificate for zapplebee.online
1147An unexpected error occurred:
1148Error creating new order :: too many failed authorizations recently: see https://letsencrypt.org/docs/failed-validation-limit/
1150```
1151
1152once that's passed, i should be able to reinstall new certs.
1153
1154_AND_ then i'll have to figure out all the places i hard coded the domain.
1155
1156it was a good exercise to change it this early for that reason.
1157
1158i bought the domain from https://www.namecheap.com/
1159
1160"bought". I lease the domain from namecheap.
1161
1162that's how a registrar works.
1163
1164I set an A record to point to my droplet
1165
1166A @ 159.223.202.91
1167
1168this points all root level traffic to the droplet.
1169
1170a little sad to leave behind the old prettybird url, but i didn't have really to many rights over it with freedns.
1171
1172I am interested in Bun will allow me to serve multiple domains from a single process.
1173
1174It does include a `hostname` param in the set up for the listener, so i dont see why not.
1175
1176it would just have to be in the same process because they'll both be using ports 80 and 443
1177
1178---
1179
1180okay i finally got through some real mess on getting this started in docker.
1181i was mounting the `/etc/letsencrypt/live` into the container so that i could serve certs.
1182but it turns out, that those live certs are symlinked from `/etc/letsencrypt/archive`
1183(with an additional change of name fullchain.pem => fullchain1.pem).
1184after mounted the archive, docker operate on the certs just fine.
1185https://github.com/zapplebee/droplet/commit/6aca9c3291bf0727227c81ebb552c55cfae20dd6
1186
1187---
1188
1189after making the React post, i really wanted to enable comments.
1190
1191considering just asking people to make a PR.
1192
1193thinking about adding dates to the lines.
1194
1195```
1196git blame -L 1,1000 -- notes/2024-01.md | awk '{print $5}'
1198```
1199
1200that would give me the date modified of all the lines on the file.
1201
1202---
1203
1204i shared one of my internal-at-work blog posts with someone today.
1205
1206it was exactly what they needed for the problem they were trying to figure out.
1207
1208i need to write in there more.
1209
1210<📚 title="Ada's Algorithm: How Lord Byron's Daughter Ada Lovelace Launched the Digital Age" >
1211
1212I am liking this book a lot.
1213
1214I'm about 2/3rd through it.
1215
1216It's a semi epistolary biography of Ada Lovelace.
1217
1218It focuses on her work with Charles Babbage on his Differential and Analytical Engine.
1219
1220It did spend a little too much time on her family before she really became the focus of the story. Even then it is spending a lot of time with Babbage.
1221
1222Still, I am enjoying it.
1223
1224Reading the letters between the people in the book was a bit of an inspiration for me to start writing this log.
1225
1226</📚>
1227
1228NEW KEYBOARD DAY!
1229
1230It's a new Keychron.
1231
1232This one is a C2 Full Size Mechanical Keyboard.
1233
1234Totally classic color scheme. All white/gray with no lights. And nice feeling brown switches.
1235
1236I really have to figure out how i want to upload and render images on this log.
1237
1238---
1239
1240I added structured logs to the webserver.
1241
1242The docker agent on the machine already records them to a file
1243
1244this will show them, since i am just running one container
1245
1246```
1247docker inspect --format='{{.LogPath}}' $(docker ps -q)
1248```
1249
1250here's a sample output
1251
1252```
1253{"log":"{\"hostname\":\"zapplebee.online\",\"ipAddress\":{\"address\":\"███REDACTED███\",\"family\":\"IPv4\",\"port\":███REDACTED███},\"level\":\"http\",\"message\":\"GET: /wp-login.php\",\"method\":\"GET\",\"pathname\":\"/wp-login.php\",\"search\":\"\",\"service\":\"helloworld\",\"timestamp\":1705453727099,\"userAgent\":\"Mozilla/5.0\"}\n","stream":"stdout","time":"2024-01-17T01:08:47.100782281Z"}
1254```
1255
1256Someone trying to read my `/wp-login.php` 😂😂😂
1257
1258I tried to curl them back but no dice.
1259
1260---
1261
1262Interesting.
1263
1264I am seeing that that Bun is having trouble parsing a URL that got passed to the request.
1265
1266```
1267"10 | export async function mainFetchHandler(req: Request, server: Server) {\n"
1268"11 | const requestUrl = new URL(req.url);\n"
1269" ^\n"
1270"TypeError: \"stager64\" cannot be parsed as a URL.\n"
1271" at /apps/helloworld/main.ts:11:22\n"
1272" at mainFetchHandler (/apps/helloworld/main.ts:10:40)\n"
1274```
1275
1276I don't know how it was able to receive this.
1277
1278It was easy to see logs from the container that were not published by the formatted logger.
1279
1280```
1281cat $(docker inspect --format='{{.LogPath}}' $(docker ps -q)) | grep "helloworld" --invert-match -B 3 -A 8 | jq ".log"
1283```
1284
1285This specifically inverts the match of "helloworld" that is a piece of default metadata that should always get posted in the log.
1286
1287Then uses `jq` to get the log value.
1288
1289---
1290
1291I was looking for a simple way to use JSX without using React.
1292
1293I have been playing around with [Hono](https://hono.dev/) kind of a lot recently.
1294
1295But I didn't really want to use it to describe my routes here.
1296
1297At the moment, I like experimenting with raw Bun.
1298
1299But it does have a really nice and simple JSX implementation.
1300
1301In the docs, it doesn't really articulate how to use JSX outside of the response context.
1302
1303So I took a look at its source code.
1304
1305https://github.com/honojs/hono/blob/1722144302b3b537111e7b9a5e18873420f05e07/src/context.ts#L362
1306
1307```
1308 html: HTMLRespond = (
1309 html: string | Promise<string>,
1310 arg?: StatusCode | ResponseInit,
1311 headers?: HeaderRecord
1312 ): Response | Promise<Response> => {
1313 this.#preparedHeaders ??= {}
1314 this.#preparedHeaders['content-type'] = 'text/html; charset=UTF-8'
1316 if (typeof html === 'object') {
1317 if (!(html instanceof Promise)) {
1318 html = (html as string).toString() // HtmlEscapedString object to string
1320 if ((html as string | Promise<string>) instanceof Promise) {
1321 return (html as unknown as Promise<string>)
1322 .then((html) => resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}))
1323 .then((html) => {
1324 return typeof arg === 'number'
1325 ? this.newResponse(html, arg, headers)
1326 : this.newResponse(html, arg)
1327 })
1330```
1331
1332This means that it simply calls `.toString()` on a JSX node.
1333
1334```
1335import type { FC } from "hono/jsx";
1336const Bold: FC = ({ children }) => {
1337 return <span class="bold">{children}</span>;
1339console.log((<Bold>a bold string</Bold>).toString());
1340```
1341
1342It just works.
1343
1344```
1345<span class="bold">a bold string</span>
1346```
1347
1348I'm a bit unsure if I am depending on a current implementation detail or if this is how it is designed and will stay designed, especially because it not documented.
1349
1350I asked about this in the discord
1351
1352https://discord.com/channels/1011308539819597844/1012485912409690122/1197381829666291813
1353
1354---
1355
1356"Artists are only creative for 10 years. We engineers are no different. Live your years well..." - The Wind Rises (Hayao Miyazaki)
1357
1358😶😶😶
1359
1360---
1361
1362TODO: now that chrome supports these scrollbar colors, is should update the css
1363
1364https://developer.chrome.com/docs/css-ui/scrollbar-styling
1365
1366---
1367
1368I have to put this here because i need to get around a paywall.
1369
1370https://timdeschryver.dev/blog/intercepting-http-requests-with-playwright
1371
1372---
1373
1374wow.
1375
1376i tried using Hono with MDX.
1377
1378it was so incredibly easy.
1379
1380I thought for sure I was going to run into an absolute nightmare of trying to point all the JSX mappings to it, but it just worked with esbuild's mdx loader.
1381
1382_including_ loading a base file of components for the built-ins.
1383
1384I create an `mdxloader.ts` file
1385
1386```
1388import { plugin } from "bun";
1389import mdx from "@mdx-js/esbuild";
1391plugin(
1392 mdx({
1393 jsxImportSource: "hono/jsx",
1394 providerImportSource: "./components/index.tsx",
1395 }) as any
1398```
1399
1400a `components/index.ts` file
1401
1402```
1404export function useMDXComponents() {
1405 return {
1406 strong: ({ children }: any) => <span class="bigboi">{children}</span>,
1407 p: ({ children }: any) => <span class="pal">{children}</span>,
1408 POST: ({ children }: any) => <div class="post">{children}</div>,
1409 };
1412```
1413
1414and in my `bunfig.toml` file just defined a little bit
1415
1416```
1417preload = ["./mdxloader.ts"]
1418```
1419
1420Then it just worked as expected.
1421
1422```
1424import thing from "./foo.mdx";
1426console.log(thing({}).toString());
1428```
1429
1430I could pass additional props to thing but i did not.
1431
1432I could return this in a hono request to a `context.html` but i did not.
1433
1434I am just trying to transform the markdown to html and this is working wonders.
1435