Compare commits

...

3 Commits

Author SHA1 Message Date
25ce2716a5 Update /wish 2026-01-16 11:15:17 -06:00
d356ff0a55 Update article 2026-01-16 11:15:08 -06:00
83fc299b7a Add speedlify widget 2026-01-16 11:15:00 -06:00
5 changed files with 277 additions and 1 deletions

View File

@@ -16,3 +16,11 @@ If you'd like to inspect the source for this site, you can [find the repo here](
[^1]: With contributions by Ethan Cohen, and Andy Clymer.
[^2]: With contributions by Mirko Velimirovic.
## Lighthouse / speedlify score
<script src="/js/speedlify-score.js"></script>
<speedlify-score speedlify-url="https://www.11ty.dev/speedlify" hash="45f6110a" score weight rank rank-change></speedlify-score>
<a href="https://www.11ty.dev/speedlify/nathanupchurch-com/">
See more info on speedlify.
</a>

View File

@@ -10,7 +10,7 @@ imageAlt: "What appears to be a pack of cigarettes labeled 11:11. There is also
synopsis: "Taking a look at Boy Vienna's viral cigarette incense sticks."
mastodon_id: "114462578542598320"
---
[Boy Vienna](https://boyvienna.com/) is a brand from fashion designer and multi-media artist [Afaf Fi Seyam](https://www.instagram.com/zeopatra) that has been receiving attention on [TikTok](https://www.tiktok.com/@boyvienna/video/7366977382508514603) and [Instagram](https://www.instagram.com/zeopatra/reel/DAyIy2Lv0RQ/) for its incense cigarettes. I knew I was going to have to try these sticks the minute they found their way onto my screen—it would seem that [everyone else felt the same way](https://www.instagram.com/zeopatra/p/DJHP0a3NnlI/), as when I made my way to the web store most of Boy Vienna's incense varieties were sold out. For 35 {{ "USD" | abbr("United States Dollars") | safe }}, I was able to snag a box of the 11:11 variety, listed as containing a blend of sage, lavender, and rosemary.
[Boy Vienna](https://boyvienna.com/) is a brand from fashion designer and multi-media artist [Afaf Fi Seyam](https://www.instagram.com/zeopatra) that has been receiving attention on [TikTok](https://www.tiktok.com/@boyvienna/video/7366977382508514603) and [Instagram](https://www.instagram.com/zeopatra/reel/DAyIy2Lv0RQ/) for its incense cigarettes. As opposed to the tobacco variety, these "cigarettes" are designed to be lit and allowed to burn like an incense-stick; they are not to be inhaled. I knew I was going to have to try these sticks the minute they found their way onto my screen—it would seem that [everyone else felt the same way](https://www.instagram.com/zeopatra/p/DJHP0a3NnlI/), as when I made my way to the web store most of Boy Vienna's incense varieties were sold out. For 35 {{ "USD" | abbr("United States Dollars") | safe }}, I was able to snag a box of the 11:11 variety, listed as containing a blend of sage, lavender, and rosemary.
[![What appears to be a pack of cigarettes labeled 11:11. There is also a card featuring the brand name Boy Vienna and a temporary tattoo featuring an image of a lipstick-print and the brand name.](/img/boy_vienna_11_11/boy_vienna_11_11_incense_cigarette_sticks_2.webp "The pack also came with a wee temporary tattoo. Fun!")](/img/boy_vienna_11_11/boy_vienna_11_11_incense_cigarette_sticks_2.webp)

View File

@@ -4,6 +4,12 @@ title: Nathan Upchurch | Changelog
structuredData: none
---
# Changelog
* 2026-01-16
* Updated [/wish](/wish).
* 2026-01-15
* Embedded lighthouse score on [/about/colophon](/about/colophon).
* 2026-01-13
* Updated [/wish](/wish).
* 2026-01-11
* Added markdown parsing to [status](/status) entries.
* 2026-01-09

View File

@@ -42,6 +42,8 @@ However if abstention seems unconscionable, I would be delighted if you were to
* [Kunmeido Shin Tokusen Reiryokoh Incense](https://kikohincense.com/collections/kunmeido-incense-kikoh/products/kunmeido-shin-tokusen-reiryokoh-incense)
* [Minorien Kyara Fu-In](https://kikohincense.com/collections/minorien-incense-kikoh/products/minorien-kyara-fu-in-incense)
* [Kin Objects Red Soil Aloeswood](https://kinobjects.com/products/red-soil-aloeswood-agarwood-incense-sticks?variant=40432647929879)
* [Kokando Kunpūshi Aloeswood](https://kikohincense.com/products/kokando-kunpushi-aloeswood)
* [Kunmeido Shin Tokusen Reiryokoh](https://kikohincense.com/collections/kunmeido-incense-kikoh/products/kunmeido-shin-tokusen-reiryokoh-incense)
* [Shoyeido Horin Assortment](https://shoyeido.com/products/horin-incense-assortment-sampler)
* [Shoyeido Kohbai Pressed Incense](https://shoyeido.com/products/kohbai-red-plum-blossoms?variant=41714738921590)
* [Shoyeido Kunro Incense Assortment](https://shoyeido.com/products/kunro-incense-assortment)

View File

@@ -0,0 +1,260 @@
class SpeedlifyUrlStore {
constructor() {
this.fetches = {};
this.responses = {};
this.urls = {};
}
static normalizeUrl(speedlifyUrl, path) {
let host = `${speedlifyUrl}${speedlifyUrl.endsWith("/") ? "" : "/"}`
return host + (path.startsWith("/") ? path.substr(1) : path);
}
async fetchFromApi(apiUrl) {
if(!this.fetches[apiUrl]) {
this.fetches[apiUrl] = fetch(apiUrl);
}
let response = await this.fetches[apiUrl];
if(!this.responses[apiUrl]) {
this.responses[apiUrl] = response.json();
}
let json = await this.responses[apiUrl];
return json;
}
async fetchHash(speedlifyUrl, url) {
if(this.urls[speedlifyUrl]) {
return this.urls[speedlifyUrl][url] ? this.urls[speedlifyUrl][url].hash : false;
}
let apiUrl = SpeedlifyUrlStore.normalizeUrl(speedlifyUrl, "api/urls.json");
let json = await this.fetchFromApi(apiUrl);
return json[url] ? json[url].hash : false;
}
async fetchData(speedlifyUrl, hash) {
let apiUrl = SpeedlifyUrlStore.normalizeUrl(speedlifyUrl, `api/${hash}.json`);
return this.fetchFromApi(apiUrl);
}
}
// Global store
const urlStore = new SpeedlifyUrlStore();
class SpeedlifyScore extends HTMLElement {
static register(tagName) {
customElements.define(tagName || "speedlify-score", SpeedlifyScore);
}
static attrs = {
url: "url",
speedlifyUrl: "speedlify-url",
hash: "hash",
rawData: "raw-data",
requests: "requests",
weight: "weight",
rank: "rank",
rankChange: "rank-change",
score: "score",
}
static css = `
:host {
--_circle: var(--speedlify-circle);
display: flex;
align-items: center;
gap: 0.375em; /* 6px /16 */
}
.circle {
font-size: 0.8125em; /* 13px /16 */
min-width: 2.6em;
height: 2.6em;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 0.15384615em solid currentColor; /* 2px /13 */
color: var(--_circle, #666);
}
.circle-good {
color: var(--_circle, #088645);
border-color: var(--_circle, #0cce6b);
}
.circle-ok {
color: var(--_circle, #ffa400);
border-color: var(--_circle, currentColor);
}
.circle-bad {
color: var(--_circle, #ff4e42);
border-color: var(--_circle, currentColor);
}
.meta {
display: flex;
align-items: center;
gap: 0.625em; /* 10px /16 */
}
.circle + .meta {
margin-left: 0.25em; /* 4px /16 */
}
.rank:before {
content: "Rank #";
}
.rank-change:before {
line-height: 1;
}
.rank-change.up {
color: green;
}
.rank-change.up:before {
content: "⬆";
}
.rank-change.down {
color: red;
}
.rank-change.down:before {
content: "⬇";
}
`;
connectedCallback() {
if (!("replaceSync" in CSSStyleSheet.prototype) || this.shadowRoot) {
return;
}
this.speedlifyUrl = this.getAttribute(SpeedlifyScore.attrs.speedlifyUrl);
this.shorthash = this.getAttribute(SpeedlifyScore.attrs.hash);
this.rawData = this.getAttribute(SpeedlifyScore.attrs.rawData);
this.url = this.getAttribute(SpeedlifyScore.attrs.url) || window.location.href;
if(!this.rawData && !this.speedlifyUrl) {
console.error(`Missing \`${SpeedlifyScore.attrs.speedlifyUrl}\` attribute:`, this);
return;
}
// async
this.init();
}
_initTemplate(data, forceRerender = false) {
if(this.shadowRoot && !forceRerender) {
return;
}
if(this.shadowRoot) {
this.shadowRoot.innerHTML = this.render(data);
return;
}
let shadowroot = this.attachShadow({ mode: "open" });
let sheet = new CSSStyleSheet();
sheet.replaceSync(SpeedlifyScore.css);
shadowroot.adoptedStyleSheets = [sheet];
let template = document.createElement("template");
template.innerHTML = this.render(data);
shadowroot.appendChild(template.content.cloneNode(true));
}
async init() {
if(this.rawData) {
let data = JSON.parse(this.rawData);
this.setDateAttributes(data);
this._initTemplate(data);
return;
}
let hash = this.shorthash;
let forceRerender = false;
if(!hash) {
this._initTemplate(); // skeleton render
forceRerender = true;
// Its much faster if you supply a `hash` attribute!
hash = await urlStore.fetchHash(this.speedlifyUrl, this.url);
}
if(!hash) {
console.error( `<speedlify-score> could not find hash for URL (${this.url}):`, this );
return;
}
// Hasnt already rendered.
if(!forceRerender) {
this._initTemplate(); // skeleton render
forceRerender = true;
}
let data = await urlStore.fetchData(this.speedlifyUrl, hash);
this.setDateAttributes(data);
this._initTemplate(data, forceRerender);
}
setDateAttributes(data) {
if(!("Intl" in window) || !Intl.DateTimeFormat || !data.timestamp) {
return;
}
const date = new Intl.DateTimeFormat().format(new Date(data.timestamp));
this.setAttribute("title", `Results from ${date}`);
}
getScoreClass(score) {
if(score === "" || score === undefined) {
return "circle";
}
if(score < .5) {
return "circle circle-bad";
}
if(score < .9) {
return "circle circle-ok";
}
return "circle circle-good";
}
getScoreHtml(title, value = "") {
return `<span title="${title}" class="${this.getScoreClass(value)}">${value ? parseInt(value * 100, 10) : "…"}</span>`;
}
render(data = {}) {
let attrs = SpeedlifyScore.attrs;
let content = [];
// no extra attributes
if(!this.hasAttribute(attrs.requests) && !this.hasAttribute(attrs.weight) && !this.hasAttribute(attrs.rank) && !this.hasAttribute(attrs.rankChange) || this.hasAttribute(attrs.score)) {
content.push(this.getScoreHtml("Performance", data.lighthouse?.performance));
content.push(this.getScoreHtml("Accessibility", data.lighthouse?.accessibility));
content.push(this.getScoreHtml("Best Practices", data.lighthouse?.bestPractices));
content.push(this.getScoreHtml("SEO", data.lighthouse?.seo));
}
let meta = [];
let summarySplit = data.weight?.summary?.split(" • ") || [];
if(this.hasAttribute(attrs.requests) && summarySplit.length) {
meta.push(`<span class="requests">${summarySplit[0]}</span>`);
}
if(this.hasAttribute(attrs.weight) && summarySplit.length) {
meta.push(`<span class="weight">${summarySplit[1]}</span>`);
}
if(data.ranks?.cumulative) {
if(this.hasAttribute(attrs.rank)) {
let rankUrl = this.getAttribute("rank-url");
meta.push(`<${rankUrl ? `a href="${rankUrl}"` : "span"} class="rank">${data.ranks?.cumulative}</${rankUrl ? "a" : "span"}>`);
}
if(this.hasAttribute(attrs.rankChange) && data.previousRanks) {
let change = data.previousRanks?.cumulative - data.ranks?.cumulative;
meta.push(`<span class="rank-change ${change > 0 ? "up" : (change < 0 ? "down" : "same")}">${change !== 0 ? Math.abs(change) : ""}</span>`);
}
}
if(meta.length) {
content.push(`<span class="meta">${meta.join("")}</span>`)
}
return content.join("");
}
}
if(("customElements" in window) && ("fetch" in window)) {
SpeedlifyScore.register();
}