0000# 👋👋👋 hi. this is blog from zac skalko.
0006I have started way too many blogs and got caught up on the making it and not the writing.
0008this is just plain text for that reason.
0010---
0012i started setting up my droplet today.
0014here's a log.
0016i created a droplet on digital ocean today.
00181 GB Memory / 25 GB Disk + 10 GB / SFO3 - Docker 23.0.6 on Ubuntu 22.04
0020I mounted the storage separately because i can easily move to another droplet if i choose.
0022with docker on it, i can just try a bunch of languages and images.
0024after getting it set up with ssh by public key from my mac mini at home, i logged in.
0026the lockfile for apt-get was broken on the first install.
0028i tried to remove the lock file and retry. no dice. i restarted the machine and it was fine.
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.
0032after that, opened up http and https with ufw
0040i wanted a vert simple way to serve something and make sure i could reach the device from the internet, not just over ssh
0042i installed bun and made a very simple http handler.
0049dang zip was missing. i installed with
0055i figured i should install build essential too
0061now i could install bun.
0063and run
0076started it. i couldn't hit it from the internet.
0078digital ocean required that i also create a firewall rule
0080so i did. 22, 80, 443
0082hit it from the internet and got a response 💪
0084great. now i needed to set up dns.
0086i went to my trusty https://freedns.afraid.org/
0088set the old one that was pointing to my home server to the droplet's address
0090it took some time to propagate.
0092from chrome though, all i was getting was ERR_CONNECTION_REFUSED
0094i tried to curl it from my local machine
0101i got back the expected response.
0103so what is going on with chrome.
0105i went to the network tab and copied the request as curl.
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.
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```
0121i realized that chrome was trying to reach the https port 443. on which i had nothing running yet
0123but it was hitting the machine
0125just not in a way that i could see.
0127i could now set up certbot.
0133i dont really have a proxy or load balancer decided on yet, so i was going to just use certbot without auto configuration.
0135https://www.digitalocean.com/community/tutorials/how-to-use-certbot-standalone-mode-to-retrieve-let-s-encrypt-ssl-certificates-on-ubuntu-20-04
0137i created the certs manually
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```
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`
0156now i have to look up how to serve with the certs from bun.
0158the simplest thing in the world.
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```
0178now i just need to handle directing traffic to https
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```
0208https://regexr.com/ is my trusty regex helper site.
0210this was substantially easier than any proxy config i have ever used.
0212it's alive.
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: */*
0223>
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
0229<
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: */*
0262>
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
0268<
0269* Connection #1 to host zapplebee.prettybirdserver.com left intact
0270h e l l o w o r l d%
0272```
0274---
0276the first bug was discovered.
0278by default, bun did not read the encoding of the file that was read from disk.
0280i guess i assumed it would.
0282my good friend Ryan Rampersad https://twitter.com/ryanmr pointed out that the unicode characters where buggy.
0284i looked and saw that the server was responding with a `Content-type` header of `text/markdown`.
0286Bun had identified this but did not automatically set the additional encoding information.
0288it was changes to `text/markdown; charset=utf-8`
0292now it works as expected.
0294---
0296i need a way to run this locally that ignores the certs
0298the way it will work is based on the NODE_ENV, i'll create the configuration differently.
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.
0302in the .bashrc file i'll export the NODE_ENV.
0308that way, anytime i am running something on this machine it will automatically be set.
0310right now the service is just being run in the background.
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.
0318to find the running process in the background, I'm running
0326while this is good, i'm going to need to trim that output so i can get just the process id and kill it
0328i can use `awk` for this.
0336this skips the first line, the table headers, and then takes the sixth column value.
0338still not quite just the process id.
0340a couple cuts will do the trick
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.
0349next i have to pass that var into a `kill` command.
0351I can do that with a command substitution.
0357after that i just need to start the service again.
0365i'll add that as an alias in the `.bashrc` file
0371this gives me a couple advantages right now.
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.
0376---
0378okay, now i can finally set up the application code to serve certs if we're in production mode.
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'
0409 }
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 });
0426}
0429```
0431here's the changes
0435---
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.
0439// TODO set up prettier and git hooks i guess
0441---
0443okay now that i have a dev mode i can start to actually add some features to this thing.
0445first of all, i would prefer some minimal styling...
0447and i do mean minimal.
0449and to actually serve this as html
0451so far i don't have any dependencies and it might be good to keep it that way for a while.
0453bun gives me a lot out of the box and would like to keep dependencies to a minimum
0455first, i'll read up all the markdown paths.
0472then i'll mash em together for now
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>`;
0485}
0487```
0489at last i'll import it in the index.
0491since i want this to fail early, ie at start up, i'll use bun's macro capability to do it.
0497this runs the getting and escaping of the files at start up and in-lines the result into the AST.
0501not super useful now since i am not bundling, but an appropriate use of a macro
0503i also added gzip it was very simple.
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```
0526---
0528although I do really like this just one long file method,
0529there's something not quite right about it.
0531and that's the ability to link to a specific piece of content.
0533I think that the best course of action is just to create id anchors for every line.
0535then one can easily deep link directly to a place on the page.
0537there's a couple things that need to happen in order for that to work as expected.
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.
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
0552i sure could just let people do a git blame. but they might just be reading this as plain text.
0554a few edits to the html file and bang we we have direct links.
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.
0558as my friend ardeshir (https://hachyderm.io/@sepahsalar) said:
0560"Yep, you start adding A 🏷️ s and next thing you know, you’ve written a new RSC framework"
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"> </span><span>${line}</span></span>`;
0580 })
0581 .join("\n")}</pre></body></html>`;
0582}
0585```
0587the next thing i want to do is enable a line wrap mode for mobile.
0589but i dont want the markdown style codeblocks wrap.
0591so i first need to make the codeblocks wrapped in a new node.
0593i'll have to do some refactoring of the page builder.
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.
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.
0599i was really trying defer design decisions because, well, i dont want to think about it too hard.
0601this is supposed to be a fun little project.
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
0605It's time that I actually messed with these Link headers.
0607This should allow the network requests to start as soon as the document loads.
0611That will keep the doc in cache until it used by the page.
0613I need to modify the response that the html request returns
0625Then, in the actual CSS in the HTML, I can import it for free.
0627and it's already been loaded
0629---
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.
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;
0660 }
0662 const inCode = Boolean(inCodeBlock || isBackticks);
0664 return `<div class="line" id="${lineId}"><a class="line-link" href="#${lineId}">${lineNumber}</a><span class="space"> </span><span class="${inCode ? "codeblock" : ""}">${inCode ? line : linkedLine}</span></div>`;
0665 })
0666 .join("\n")}</pre></body></html>`;
0667}
0669````
0671just a little magic
0673---
0675whoops i wrote a bunch of code without documenting it.
0677i just needed to experiment with the styling.
0679I actually should review and refactor at some point because there is too many
0680dom nodes for what i am trying to do here.
0682---
0684I refactored the render function a little to make it more readable.
0686Broke the massive template string into a few variables and mashed them together.
0688Variable names are free documentation.
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>
0705`;
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;
0723 }
0725 if (isBackticks) {
0726 inCodeBlock = !inCodeBlock;
0727 }
0729 if (!inCodeBlock && isBackticks) {
0730 addCodeBlockCloseTag = true;
0731 }
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("");
0760}
0762````
0764as for the styles. i found a new property that i have never run into before
0772I could not figure out what was making my fonts all messed up
0773despite the `!important` tag.
0775This is what I get for using somebody else's CSS reset all the time.
0777Finally made the CSS into something i could tolerate.
0778It's interesting how dependant I have become on CSS-in-JS or tailwind
0780This was harder than I remember it being.
0782```
0784:root {
0785 --textwidth: 40ch;
0786 --linenumberwidth: 3ch;
0787 --gapwidth: 2ch;
0788}
0790@media (min-width: 500px) {
0791 :root {
0792 --textwidth: 80ch;
0793 }
0794}
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;
0807}
0809body {
0810 background-color: black;
0811 display: block;
0812}
0814main {
0815 font-family: monospace;
0816 width: calc(var(--textwidth) + var(--linenumberwidth) + var(--gapwidth));
0817 margin: auto;
0818 display: block;
0819}
0821.line {
0822 display: flex;
0823 flex-direction: row;
0824 gap: var(--gapwidth);
0825}
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);
0833}
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);
0840}
0842.codeblock {
0843 color: rgb(215, 246, 152);
0844 white-space: pre;
0845}
0847.prose {
0848 width: var(--textwidth);
0849 white-space: pre-wrap;
0850 overflow-wrap: break-word;
0851}
0853::-webkit-scrollbar {
0854 height: 1rem;
0855 width: 1ch;
0856 background: rgb(63, 63, 63);
0857}
0859::-webkit-scrollbar-thumb {
0860 background: rgb(215, 246, 152);
0861}
0863::-webkit-scrollbar-corner {
0864 background: rgb(63, 63, 63);
0865}
0867```
0869---
0871the next thing i am going to have to figure out for this is images.
0873they're tricky for a couple reasons.
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
0882I have a bit of an idea
0884I could load like, two lines of the image, and add a button to expand.
0886like this
0888<!-- prettier-ignore-start -->
0890imageurl: an-image.jpg
0891███████████████████████
0892███████████████████████
0893███ click to expand ███
0896imageurl: an-image.jpg
0897██████████████████████
0898██████████████████████
0899██████████████████████
0900███ █████████ █████
0901██████████████████████
0902█ ██████████████ ███
0903███ ██████████ ████
0904████ ██████
0905██████████████████████
0906██████████████████████
0907███ click to close ███
0909<!-- prettier-ignore-end -->
0911that could be done with just a checkbox and some custom styles
0913but it wouldn't be very accessible.
0915so far, as strange as the set of this site is, it is rather accessible.
0917though i could bump up the contrast from the bg and the text,
0919there's not a busy menu to navigate through via keyboard.
0921there's no animations.
0923theres' no dynamic loading of content.
0925the other accessibility thing I should do is probably add image roles for the emoji
0929(exactly unlike above...)
0931i would also like a way to be able to add content from my phone.
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.
0936i tried mastodon, but i just didn't get it or something.
0938so the ability to add content whenever, whereever would be nice.
0940stretch goal would be to add the ability to comment on lines.
0942one of the goals of this site is to practice progressive by incremental improvement of this feed.
0943backwards compatibility too.
0945since right now this is just a giant markdown file,
0946it could obviously be replatformed.
0948the other goal is that all the changes to the site get logged here.
0950think of it as a git log + architecture decision record + readme + bullet journal + social media
0952the simple form of it has let me write almost 1000 lines already!
0954in fact
0956TODO: fix the fact that right now the line number width is fixed to three characters
0958i'd also like some kind of convention for being able to write long form segments that get their own page
0960something like
0969honestly, i think that i'm happy enought with that as a pattern
0971the landing page of the site should show something like the last 10 lines of
0972the log with a link to the full log
0974and then posts as links.
0976---
0978i was trying to set some html meta tags.
0980as usual, i stole from ryan
0984---
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.
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.
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>
1115as i was writing this post i got a little distracted by making it fancy.
1117but i needed someway to deep link it.
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
1121so instead now i one single line of javascript on the client.
1130---
1132while trying to get meta tags working i discovered that there was a certificate problem that was preventing crawlers from getting to the page.
1134so i decided it was time to move from the https://freedns.afraid.org/ subdomain i was using and buy my own
1138i didnt wait long enough for DNS to propagate before trying to set up certs.
1140so now i have to wait an hour
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```
1152once that's passed, i should be able to reinstall new certs.
1154_AND_ then i'll have to figure out all the places i hard coded the domain.
1156it was a good exercise to change it this early for that reason.
1158i bought the domain from https://www.namecheap.com/
1160"bought". I lease the domain from namecheap.
1162that's how a registrar works.
1164I set an A record to point to my droplet
1166A @ 159.223.202.91
1168this points all root level traffic to the droplet.
1170a little sad to leave behind the old prettybird url, but i didn't have really to many rights over it with freedns.
1172I am interested in Bun will allow me to serve multiple domains from a single process.
1174It does include a `hostname` param in the set up for the listener, so i dont see why not.
1176it would just have to be in the same process because they'll both be using ports 80 and 443
1178---
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.
1187---
1189after making the React post, i really wanted to enable comments.
1191considering just asking people to make a PR.
1193thinking about adding dates to the lines.
1200that would give me the date modified of all the lines on the file.
1202---
1204i shared one of my internal-at-work blog posts with someone today.
1206it was exactly what they needed for the problem they were trying to figure out.
1208i need to write in there more.
1210<📚 title="Ada's Algorithm: How Lord Byron's Daughter Ada Lovelace Launched the Digital Age" >
1212I am liking this book a lot.
1214I'm about 2/3rd through it.
1216It's a semi epistolary biography of Ada Lovelace.
1218It focuses on her work with Charles Babbage on his Differential and Analytical Engine.
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.
1222Still, I am enjoying it.
1224Reading the letters between the people in the book was a bit of an inspiration for me to start writing this log.
1226</📚>
1228NEW KEYBOARD DAY!
1230It's a new Keychron.
1232This one is a C2 Full Size Mechanical Keyboard.
1234Totally classic color scheme. All white/gray with no lights. And nice feeling brown switches.
1236I really have to figure out how i want to upload and render images on this log.
1238---
1240I added structured logs to the webserver.
1242The docker agent on the machine already records them to a file
1244this will show them, since i am just running one container
1250here's a sample output
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```
1256Someone trying to read my `/wp-login.php` 😂😂😂
1258I tried to curl them back but no dice.
1260---
1262Interesting.
1264I am seeing that that Bun is having trouble parsing a URL that got passed to the request.
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```
1276I don't know how it was able to receive this.
1278It was easy to see logs from the container that were not published by the formatted logger.
1285This specifically inverts the match of "helloworld" that is a piece of default metadata that should always get posted in the log.
1287Then uses `jq` to get the log value.
1289---
1291I was looking for a simple way to use JSX without using React.
1293I have been playing around with [Hono](https://hono.dev/) kind of a lot recently.
1295But I didn't really want to use it to describe my routes here.
1297At the moment, I like experimenting with raw Bun.
1299But it does have a really nice and simple JSX implementation.
1301In the docs, it doesn't really articulate how to use JSX outside of the response context.
1303So I took a look at its source code.
1305https://github.com/honojs/hono/blob/1722144302b3b537111e7b9a5e18873420f05e07/src/context.ts#L362
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
1319 }
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 })
1328 }
1329 }
1330```
1332This means that it simply calls `.toString()` on a JSX node.
1342It just works.
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.
1350I asked about this in the discord
1354---
1356"Artists are only creative for 10 years. We engineers are no different. Live your years well..." - The Wind Rises (Hayao Miyazaki)
1358😶😶😶
1360---
1362TODO: now that chrome supports these scrollbar colors, is should update the css
1366---
1368I have to put this here because i need to get around a paywall.
1372---
1374wow.
1376i tried using Hono with MDX.
1378it was so incredibly easy.
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.
1382_including_ loading a base file of components for the built-ins.
1384I create an `mdxloader.ts` file
1400a `components/index.ts` file
1414and in my `bunfig.toml` file just defined a little bit
1420Then it just worked as expected.
1430I could pass additional props to thing but i did not.
1432I could return this in a hono request to a `context.html` but i did not.
1434I am just trying to transform the markdown to html and this is working wonders.
1436---
1438hi. it's been a while.
1440i started thinking about building something again. but i really should probably
1441just use the thing i built.
1443So I guess I should write an entry.
1445I let the certs lapse on this site because, as stated at the top, I knew this was a little holiday hobby project that wasn't going to last.
1447And usually I do such a poor job of documenting those that there was no way I presumed I was going to be able to get it back in place.
1449But this project is literally self documenting.
1451So instead of trying to read the docs, I asked ChatGPT to do it.
1453It said that it couldn't read the website. I presumed this was due to certs.
1455So instead I had to try and think for a moment.
14571. Where the hell does the app actually run?
14582. If I stop it, will I remember how to restart it?
14593. Will I remember how to regenerate certs?
1461Ok so first instinct was to run a `ps` and see what was running. Not a lot
1470Next, check if I have this running in Docker.
1478Perfect.
1484That one is commited to memory. Kill all the running Docker images.
1486Congrats. I killed the website.
1488Second. Refresh the certs.
1490Thankfully even though ChatGPT couldn't read my website, it still spit out the most common command to refresh certs.
1496Boom, done. Now I just need to restart that Docker image...
1498Where did I do that?
1500Where are the apps on this machine?
1502`ls /` `ls ~` `su apollo && ls ~`
1504Nothing.
1506Uh oh. Did I do something stupid? Where the hell is it?
1508I usually prefer to keep apps in directory in `$HOME/{github_domain}/{gh_user}/{gh_project}. Never any guess work with being standard.
1510But nothing was around.
1512I finally moved to `/mnt` and remembered that I keep the apps in attached storage so that if the Droplet needs to get nuked, I have everything.
1514Well there it.
1516A simple `docker compose up` in the apps dir and I'm back.
1518I decided I should ask ChatGPT if it can read my website again now that the certs are healthy.
1520Some logs come through. They tried.
1522> I couldn't access the website due to a restriction in place on my browsing tool. Some websites may block automated tools from fetching their pages, or they might have a robots.txt file that prevents bots from accessing certain content. (From 🤖)
1524No hits to `robots.txt` though. You liar.
1526---
1528<📚 title="The Wager: A Tale of Shipwreck, Mutiny and Murder" >
1530This book is very good. Compelling to read.
1532David Grann does an excellent job of characterizing these real people though their selection of quotations and contextualizing.
1534Grann described the logs that the ships would take. Reminding me to keep logging here.
1536Thinking about how those kinds of logs too have been automated in technology.
1538Just read raw machine logs to figure out what happened. Grann's book could never be written with just
1545Though some moments could be enriched with this level of quantitative data.
1547But the logs that he drew from were created by people. They included the sentiment of the crew and synthesized the raw data with the human experience at the time of recording.
1549We should all do more writing.
1551It is crazy that the two books that I have mentioned on this log so far both include members of the Byron Family.
1553John Byron (the main subject) was the grandfather of the poet Lord Byron, the father of Ada Lovelace.
1554</📚>
1556---
1558So even though I generated new certs with certbot, I failed to actually use them.
1560based on my docker compose file
1562```
1563version: "3"
1565services:
1566 helloworld:
1567 build:
1568 context: ./helloworld
1569 dockerfile: Dockerfile
1570 ports:
1571 - "80:80"
1572 - "443:443"
1573 volumes:
1574 - ./notes:/mdlogdir
1575 - /etc/letsencrypt/archive/:/hostcerts
1576 environment:
1577 - NODE_ENV=production
1578 - FDQN=zapplebee.online
1579 - CERTS_DIR=/hostcerts
1580 - NOTES_DIR=/mdlogdir
1581 restart: always
1583```
1585I am loading certs from the archive directory. I seem to remember chosing this was due to some file permission conflicts with mounting the files in docker.
1587anyway. inside of the archive directory the certs are created with an increment.
1594I forgot to increment these.
1598Done.