Compare commits
31 Commits
2a49b92643
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6621b20829 | |||
| 72c0fddf02 | |||
| 6d6302bf5a | |||
| 4fd2e413f1 | |||
| aafed45d0d | |||
| beb45844cc | |||
| 5c4c547a45 | |||
| 95106a8899 | |||
| dedc1394cf | |||
| 3d57ad82c5 | |||
| 598ec3fd38 | |||
| 7e41536b4f | |||
| 8434a9dae5 | |||
| c0def2f0a3 | |||
| 25ce2716a5 | |||
| d356ff0a55 | |||
| 83fc299b7a | |||
| c68cda65d4 | |||
| c6c83fcff3 | |||
| 76472ada91 | |||
| d948f3f52c | |||
| eefcb2f5a3 | |||
| 7aa863f9c3 | |||
| 2c5efe83d9 | |||
| 28b657d871 | |||
| 42ccfdea7d | |||
| 57cc65e31e | |||
| ba661426c0 | |||
| 2aa1fdb6c0 | |||
| 9f8b81435f | |||
| 41b8d4bc13 |
@@ -1,6 +1,6 @@
|
||||
TODO:
|
||||
---------------------
|
||||
Integrate recipe structured data for recipe type articles
|
||||
Move hard-coded copy to metadata.
|
||||
|
||||
Quiz Ideas:
|
||||
---------------------
|
||||
|
||||
@@ -575,7 +575,7 @@ export default {
|
||||
},
|
||||
{
|
||||
title: "Dom Corriveau",
|
||||
feedUrl: "https://blog.ctms.me/index.xml",
|
||||
feedUrl: "https://blog.ctms.me/posts/index.xml",
|
||||
url: "https://blog.ctms.me/",
|
||||
description:
|
||||
"Thoughts, opinions, wild speculation, and haphazard technical advice from Dom.",
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
profilePic: "/img/CN20191025_301_Srt_SQUARE_crop.jpg",
|
||||
},
|
||||
blogrollUrl: "/blogroll/nathanUpchurchBlogroll.opml",
|
||||
copyrightNotice: "© Nathan Upchurch 2022 - 2025",
|
||||
copyrightNotice: "© Nathan Upchurch 2022 - 2026",
|
||||
defaultPostImageURL: "/img/logo_post.svg",
|
||||
defaultPostImageAlt: "The logo for this blog: a capital letter N.",
|
||||
mastodonHost: "lounge.town",
|
||||
@@ -71,6 +71,11 @@ export default {
|
||||
linkDisplay: "Sitemap",
|
||||
linkURL: "/sitemap/",
|
||||
},
|
||||
{
|
||||
iconURL: "/img/icons/breeze/clock-symbolic.svg",
|
||||
linkDisplay: "Status",
|
||||
linkURL: "/status/",
|
||||
},
|
||||
{
|
||||
iconURL: "/img/icons/breeze/tag.svg",
|
||||
linkDisplay: "Topics",
|
||||
@@ -185,6 +190,7 @@ export default {
|
||||
iconURL: "/img/wafrn.svg",
|
||||
},
|
||||
],
|
||||
weatherOnByDefault: false,
|
||||
weatherSymbol: "❅",
|
||||
webrings: [
|
||||
{
|
||||
@@ -200,4 +206,5 @@ export default {
|
||||
nextURL: "http://geekring.net/site/350/next",
|
||||
},
|
||||
],
|
||||
wooModeOnByDefault: false,
|
||||
};
|
||||
|
||||
@@ -12,3 +12,9 @@
|
||||
async
|
||||
src="//gc.zgo.at/count.js"
|
||||
></script>
|
||||
|
||||
<!-- Mochi -->
|
||||
<script
|
||||
src="https://mochi.meadow.cafe/reaper/mochi@upchur.ch/embed/1.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
78
_includes/issoCommenting.njk
Normal file
@@ -0,0 +1,78 @@
|
||||
<script data-isso="https://isso.upchur.ch" src="https://isso.upchur.ch/js/embed.min.js"></script>
|
||||
|
||||
<section id="isso-thread">
|
||||
<noscript>Javascript needs to be activated to view comments.</noscript>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
h4.isso-thread-heading {
|
||||
color: var(--text-color) !important;
|
||||
font-size: var(--step-2) !important;
|
||||
font-variation-settings: "opsz" 50, "wght" 350, "SOFT" 20, "WONK" 1 !important;
|
||||
line-height: calc(var(--step-2) * 0.25 + var(--step-2)) !important;
|
||||
margin-bottom: var(--space-m) !important;
|
||||
margin-top: var(--space-m) !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.isso-text > h1, .isso-text > h2 {
|
||||
padding: 0 0 0 0 !important;
|
||||
}
|
||||
|
||||
.isso-form-wrapper > .isso-auth-section > .isso-post-action > input {
|
||||
background-color: var(--contrast-color) !important;
|
||||
border: none !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
color: var(--background-color) !important;
|
||||
font-family: var(--font-family-ui) !important;
|
||||
font-size: var(--step--2) !important;
|
||||
font-variation-settings: var(--font-variation-ui) !important;
|
||||
height: var(--space-m-l) !important;
|
||||
letter-spacing: var(--ui-letter-spacing) !important;
|
||||
margin: var(--space-xs) 0 0 0 !important;
|
||||
padding: 0 var(--space-xs) !important;
|
||||
text-transform: uppercase !important;
|
||||
transition: var(--transition-normal) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
p.isso-input-wrapper {
|
||||
display: block !important;
|
||||
max-width: 100% !important;
|
||||
& > label, & > input {
|
||||
font-family: var(--font-family-ui) !important;
|
||||
font-size: var(--step--2) !important;
|
||||
font-variation-settings: var(--font-variation-ui) !important;
|
||||
}
|
||||
& input {
|
||||
margin-block: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.isso-post-action {
|
||||
display: inline-block !important;
|
||||
float: inherit !important;
|
||||
margin: 0 var(--space-2xs) 0 0 !important;
|
||||
max-width: var(--space-3xl) !important;
|
||||
&::after {
|
||||
content: "" !important;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.isso-textarea {
|
||||
background-color: var(--background-color) !important;
|
||||
border: var(--border-details) !important;
|
||||
border-color: var(--contrast-color) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
color: var(--text-color) !important;
|
||||
font-family: var(--font-family) !important;
|
||||
font-size: var(--step-0) !important;
|
||||
font-variation-settings: var(--font-variation-default) !important;
|
||||
margin-block: 0 1lh !important;
|
||||
min-height: var(--space-l) !important;
|
||||
padding: 0 var(--space-3xs) !important;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -5,11 +5,15 @@
|
||||
<link rel="stylesheet" type="text/css" href="/css/index.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/webfonts/webfonts.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/code.css" />
|
||||
<!-- Mochi webmentions -->
|
||||
<link rel="webmention" href="https://mochi-webmentions.meadow.cafe/webmention/mochi@upchur.ch/1/receive" />
|
||||
<!-- Indieweb profile links -->
|
||||
{% if not excludeProfilesFromHead %}{% for link in metadata.socialLinks %}{% if not link.excludeFromHead %}<link {% if link.customAttribute %} {{ link.customAttribute | safe }} {% endif %} href="{{ link.linkURL }}" />{% endif %}{% endfor %}{% endif %}
|
||||
<!-- /Indieweb profile links -->
|
||||
{% include "structuredData.njk" %}
|
||||
{% include "analytics.html" %}
|
||||
{% include "weatherStyle.njk" %}
|
||||
{% include "wooModeStyle.njk" %}
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.njk" %}
|
||||
@@ -20,5 +24,6 @@
|
||||
</main>
|
||||
{% include "footer.njk" %}
|
||||
{% include "weather.njk" %}
|
||||
{% include "wooMode.njk" %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,7 @@ layout: layouts/linksPage.njk
|
||||
<div class="links-container h-card">
|
||||
<img class="profilePic u-photo" src="{{ metadata.author.profilePic }}">
|
||||
<h1 class="socialTitle p-name">Nathan Upchurch</h1>
|
||||
<p class="page-block nodropcap">Here's where you can find me on the internet:</p>
|
||||
<p class="page-block nodropcap">Beside what I do to earn a crust, among other things, I’m an incense maker and enthusiast, a classical trombonist, vegan cook, writer, mediocre photographer, and a fan of, advocate for, and occasional contributor to free and open source software. Here’s where you can find me on the internet:</p>
|
||||
<div class="socialLinks">
|
||||
{% for link in metadata.socialLinks %}
|
||||
<a class="link-button u-url" {% if link.customAttribute %} {{ link.customAttribute | safe }} {% endif %} href="{{ link.linkURL }}">
|
||||
|
||||
@@ -3,23 +3,24 @@ layout: layouts/base.njk
|
||||
---
|
||||
<article class="post">
|
||||
<h1>{{ title | safe }}</h1>
|
||||
{% include "mastodonComments.njk" %}
|
||||
{% if not hideMetadata %}
|
||||
<div class="post-metadata h-card">
|
||||
<div class="post-metadata">
|
||||
{% if author %}
|
||||
{% if author.profilePic %}
|
||||
<img class="profilePic u-photo" src="{{ author.profilePic }}">
|
||||
<img class="profilePic" src="{{ author.profilePic }}">
|
||||
{% endif %}
|
||||
<div class="post-metadata-copy">
|
||||
<p>{% if author.url %}<a class="u-url" href="{{ author.url }}">{% endif %}
|
||||
{% if author.name %}By <span class="p-name">{{ author.name }}</span>{% endif %}{% if author.url %}</a> • {% endif %}<time datetime="{{ page.date | htmlDateString }}">{{ page.date | niceDate }}</time> • {{ content | emojiReadTime }}</p>
|
||||
<p>{% if author.url %}<a href="{{ author.url }}">{% endif %}
|
||||
{% if author.name %}By {{ author.name }}{% endif %}{% if author.url %}</a> • {% endif %}<time datetime="{{ page.date | htmlDateString }}">{{ page.date | niceDate }}</time> • {{ content | emojiReadTime }}</p>
|
||||
|
||||
{% else %}
|
||||
{% if metadata.author.profilePic %}
|
||||
<img class="profilePic u-photo" src="{{ metadata.author.profilePic }}">
|
||||
<img class="profilePic" src="{{ metadata.author.profilePic }}">
|
||||
{% endif %}
|
||||
<div class="post-metadata-copy">
|
||||
<p>{% if metadata.author.url %}<a hclass="u-url" ref="{{ metadata.author.url }}">{% endif %}
|
||||
{% if metadata.author.name %}By <span class="p-name">{{ metadata.author.name }}</span>{% endif %}{% if metadata.author.url %}</a> • {% endif %}<time datetime="{{ page.date | htmlDateString }}">{{ page.date | niceDate }}</time> • {{ content | emojiReadTime }}</p>
|
||||
<p>{% if metadata.author.url %}<a href="{{ metadata.author.url }}">{% endif %}
|
||||
{% if metadata.author.name %}By {{ metadata.author.name }}{% endif %}{% if metadata.author.url %}</a> • {% endif %}<time datetime="{{ page.date | htmlDateString }}">{{ page.date | niceDate }}</time> • {{ content | emojiReadTime }}</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -41,4 +42,4 @@ layout: layouts/base.njk
|
||||
{% endif %}
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
{% include "mastodonComments.njk" %}
|
||||
{% include "issoCommenting.njk" %}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{% if mastodon_id %}
|
||||
<section class="" id="comment-section">
|
||||
<div class="continue-discussion">
|
||||
<a class="link-button" href="https://{{ metadata.mastodonHost }}/@{{ metadata.mastodonUser }}/{{ mastodon_id }}">
|
||||
<button type="button">
|
||||
<img src="/img/mastodon.svg">
|
||||
Discuss on Mastodon »
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<div class="continue-discussion">
|
||||
<a class="link-button" href="https://{{ metadata.mastodonHost }}/@{{ metadata.mastodonUser }}/{{ mastodon_id }}">
|
||||
<button type="button">
|
||||
<img src="/img/mastodon.svg">
|
||||
Discuss on Mastodon »
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
<style>
|
||||
#siteSettingsContainer {
|
||||
& button:not(#settingsDone) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="siteSettingsContainer">
|
||||
<button onclick="siteSettings.showModal();">Site Settings</button>
|
||||
<dialog id="siteSettings">
|
||||
<h2>Site Settings</h2>
|
||||
{% include "weatherController.njk" %}
|
||||
{% include "wooModeController.njk" %}
|
||||
<button id="settingsDone" onclick="siteSettings.close();">Done</button>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
36
_includes/statusList.njk
Normal file
@@ -0,0 +1,36 @@
|
||||
<section class="postlist microblog-list">
|
||||
{% if postlistHeaderText %}<h2>{{ postlistHeaderText }}</h2>{% endif %}
|
||||
<div class="postlist-item-container">
|
||||
{% for status in postslist %}
|
||||
<article class="post microblog-post">
|
||||
<div class="microblog-status card">
|
||||
<span class="microblog-emoji">{{ status.data.emoji }}</span>
|
||||
|
||||
<div class="microblog-status-copy">
|
||||
<p>
|
||||
<span class="status-metadata">
|
||||
{% if metadata.author.url %}
|
||||
<a href="{{ metadata.author.url }}">
|
||||
{% endif %}
|
||||
|
||||
{% if metadata.author.name %}
|
||||
{{ metadata.author.name }}
|
||||
{% endif %}
|
||||
|
||||
{% if metadata.author.url %}
|
||||
</a><br />
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{{ status.data.comment | markdownify | safe }}<br />
|
||||
|
||||
<span class="status-metadata">
|
||||
{{ status.date | niceDate }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,117 +1,6 @@
|
||||
<!-- weather -->
|
||||
<!-- Based on https://codepen.io/codeconvey/pen/xRzQay -->
|
||||
{# This include causes a symbol (text, emoji, et cetera; from metadata.weatherSymbol) to fall from the top of the viewport like snow. #}
|
||||
|
||||
<style>
|
||||
.fallingObject {
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-family: Arial;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 0 1px #000;
|
||||
}
|
||||
@keyframes fallingObjects-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes fallingObjects-shake {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
.fallingObject {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
z-index: 9999;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
animation-name: fallingObjects-fall, fallingObjects-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-play-state: running, running;
|
||||
}
|
||||
.fallingObject:nth-of-type(0) {
|
||||
left: 1%;
|
||||
animation-delay: 0s, 0s;
|
||||
& > div {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
& > div {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
& > div {
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
& > div {
|
||||
transform: rotate(84deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
& > div {
|
||||
transform: rotate(267deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
& > div {
|
||||
transform: rotate(200deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
& > div {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
& > div {
|
||||
transform: rotate(78deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
& > div {
|
||||
transform: rotate(3120deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
& > div {
|
||||
transform: rotate(123deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="fallingObjects" id="weather" aria-hidden="true">
|
||||
<div class="fallingObject">
|
||||
<div>{{ metadata.weatherSymbol }}</div>
|
||||
@@ -151,7 +40,7 @@
|
||||
const weatherPreference = localStorage.getItem("weather");
|
||||
|
||||
// Initial weather preference check on page load
|
||||
if (weatherPreference == 0) {
|
||||
if ({% if metadata.weatherOnByDefault == false %}!weatherPreference || {% endif %}weatherPreference == 0) {
|
||||
weather.classList.add("hidden");
|
||||
weatherToggle.checked = false;
|
||||
} else {
|
||||
|
||||
@@ -1,30 +1,4 @@
|
||||
<style>
|
||||
#weatherController {
|
||||
color: var(--text-color);
|
||||
font-size: var(--step--2);
|
||||
font-variation-settings: var(--font-variation-ui);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--ui-letter-spacing);
|
||||
font-family: var(--font-family-ui);
|
||||
& label {
|
||||
display: inline;
|
||||
}
|
||||
& input {
|
||||
accent-color: var(--contrast-color);
|
||||
background-color: var(--background-color);
|
||||
border: var(--border-details);
|
||||
border-color: var(--contrast-color);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text-color);
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--contrast-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<form id="weatherController">
|
||||
<input type="checkbox" id="weatherToggle" checked />
|
||||
<form class="siteSettingsToggle" id="weatherController">
|
||||
<input type="checkbox" id="weatherToggle" {% if metadata.weatherOnByDefault %}checked{% endif %} />
|
||||
<label for="weatherToggle">Show weather?</label>
|
||||
</form>
|
||||
|
||||
112
_includes/weatherStyle.njk
Normal file
@@ -0,0 +1,112 @@
|
||||
<!-- Weather style -->
|
||||
<style>
|
||||
.fallingObject {
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-family: Arial;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 0 1px #000;
|
||||
}
|
||||
@keyframes fallingObjects-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes fallingObjects-shake {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
.fallingObject {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
z-index: 9999;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
animation-name: fallingObjects-fall, fallingObjects-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-play-state: running, running;
|
||||
}
|
||||
.fallingObject:nth-of-type(0) {
|
||||
left: 1%;
|
||||
animation-delay: 0s, 0s;
|
||||
& > div {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
& > div {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
& > div {
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
& > div {
|
||||
transform: rotate(84deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
& > div {
|
||||
transform: rotate(267deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
& > div {
|
||||
transform: rotate(200deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
& > div {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
& > div {
|
||||
transform: rotate(78deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
& > div {
|
||||
transform: rotate(3120deg);
|
||||
}
|
||||
}
|
||||
.fallingObject:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
& > div {
|
||||
transform: rotate(123deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- / Weather style -->
|
||||
222
_includes/wooMode.njk
Normal file
@@ -0,0 +1,222 @@
|
||||
<!-- Woo Mode -->
|
||||
<!-- Based on https://codepen.io/tommyho/pen/JjgoZLK -->
|
||||
{# This include replaces the page background with a crazy rainbow animated shader #}
|
||||
|
||||
<canvas id="shaderCanvas"></canvas>
|
||||
<script src="/js/three.min.js"></script>
|
||||
<script>
|
||||
let scene, camera, renderer, uniforms, material, mesh;
|
||||
|
||||
function initWoo() {
|
||||
scene = new THREE.Scene();
|
||||
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('shaderCanvas'), antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
const vertexShader = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
uniform float time;
|
||||
uniform vec2 resolution;
|
||||
varying vec2 vUv;
|
||||
|
||||
#define PI 3.14159265358979323846
|
||||
|
||||
vec2 rotate(vec2 v, float a) {
|
||||
float s = sin(a);
|
||||
float c = cos(a);
|
||||
mat2 m = mat2(c, -s, s, c);
|
||||
return m * v;
|
||||
}
|
||||
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
float fbm(vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 0.0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
value += amplitude * noise(st);
|
||||
st *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
|
||||
return a + b * cos(6.28318 * (c * t + d));
|
||||
}
|
||||
|
||||
vec3 vibrancePalette(float t) {
|
||||
vec3 a = vec3(0.5, 0.5, 0.5);
|
||||
vec3 b = vec3(0.5, 0.5, 0.5);
|
||||
vec3 c = vec3(1.0, 1.0, 1.0);
|
||||
vec3 d = vec3(0.0, 0.33, 0.67);
|
||||
return palette(t, a, b, c, d);
|
||||
}
|
||||
|
||||
vec3 warmPalette(float t) {
|
||||
vec3 a = vec3(0.5, 0.5, 0.5);
|
||||
vec3 b = vec3(0.5, 0.5, 0.5);
|
||||
vec3 c = vec3(1.0, 1.0, 1.0);
|
||||
vec3 d = vec3(0.0, 0.10, 0.20);
|
||||
return palette(t, a, b, c, d);
|
||||
}
|
||||
|
||||
vec3 coolPalette(float t) {
|
||||
vec3 a = vec3(0.5, 0.5, 0.5);
|
||||
vec3 b = vec3(0.5, 0.5, 0.5);
|
||||
vec3 c = vec3(1.0, 1.0, 1.0);
|
||||
vec3 d = vec3(0.3, 0.20, 0.20);
|
||||
return palette(t, a, b, c, d);
|
||||
}
|
||||
|
||||
vec3 rainbowPalette(float t) {
|
||||
vec3 a = vec3(0.5, 0.5, 0.5);
|
||||
vec3 b = vec3(0.5, 0.5, 0.5);
|
||||
vec3 c = vec3(1.0, 1.0, 1.0);
|
||||
vec3 d = vec3(0.0, 0.33, 0.67);
|
||||
return palette(t, a, b, c, d);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 st = gl_FragCoord.xy / resolution.xy;
|
||||
st.x *= resolution.x / resolution.y;
|
||||
|
||||
vec2 q = vec2(0.);
|
||||
q.x = fbm(st + 0.1 * time);
|
||||
q.y = fbm(st + vec2(1.0));
|
||||
|
||||
vec2 r = vec2(0.);
|
||||
r.x = fbm(st + 1.0 * q + vec2(1.7, 9.2) + 0.15 * time);
|
||||
r.y = fbm(st + 1.0 * q + vec2(8.3, 2.8) + 0.126 * time);
|
||||
|
||||
float f = fbm(st + r);
|
||||
|
||||
vec2 p = st * 2.0 - 1.0;
|
||||
float a = atan(p.y, p.x);
|
||||
float r2 = length(p);
|
||||
|
||||
vec2 uv = vec2(a / PI, r2);
|
||||
uv = rotate(uv, time * 0.1);
|
||||
|
||||
vec3 color1 = vibrancePalette(f + time * 0.1);
|
||||
vec3 color2 = warmPalette(length(q));
|
||||
vec3 color3 = coolPalette(length(r.x));
|
||||
vec3 color4 = rainbowPalette(f * 2.0 + time * 0.2);
|
||||
|
||||
vec3 color = mix(color1, color2, 0.5);
|
||||
color = mix(color, color3, 0.3);
|
||||
color = mix(color, color4, sin(time * 0.1) * 0.5 + 0.5);
|
||||
|
||||
color += 0.05 * vec3(1.0) * smoothstep(0.1, 0.2, fbm(10.0 * uv + time * 0.5));
|
||||
|
||||
// Add some extra vibrancy
|
||||
color = pow(color, vec3(0.8));
|
||||
color *= 1.1;
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
uniforms = {
|
||||
time: { value: 1.0 },
|
||||
resolution: { value: new THREE.Vector2() }
|
||||
};
|
||||
|
||||
material = new THREE.ShaderMaterial({
|
||||
uniforms: uniforms,
|
||||
vertexShader: vertexShader,
|
||||
fragmentShader: fragmentShader
|
||||
});
|
||||
|
||||
mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
|
||||
scene.add(mesh);
|
||||
|
||||
onWindowResize();
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
uniforms.resolution.value.x = renderer.domElement.width;
|
||||
uniforms.resolution.value.y = renderer.domElement.height;
|
||||
}
|
||||
|
||||
function animate(timestamp) {
|
||||
requestAnimationFrame(animate);
|
||||
uniforms.time.value = timestamp * 0.001;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const wooCanvas = document.getElementById("shaderCanvas");
|
||||
const wooToggle = document.getElementById("wooToggle");
|
||||
const wooPreference = localStorage.getItem("wooMode");
|
||||
let forcedWoo = false;
|
||||
|
||||
// Initial preference check on page load
|
||||
if ({% if metadata.wooModeOnByDefault == false %}!wooPreference || {% endif %}wooPreference == 0) {
|
||||
forcedWoo ? null : wooCanvas.classList.add("hidden");
|
||||
wooToggle.checked = false;
|
||||
} else {
|
||||
wooCanvas.classList.remove("hidden");
|
||||
wooToggle.checked = true;
|
||||
initWoo();
|
||||
animate(0);
|
||||
};
|
||||
|
||||
// Handle setting toggle
|
||||
wooToggle.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
localStorage.setItem("wooMode", 1);
|
||||
|
||||
let wooAudio = new Audio("/audio/30995__unclesigmund__woo-2.mp3");
|
||||
wooAudio.volume = 0.4;
|
||||
wooAudio.play();
|
||||
|
||||
wooCanvas.classList.remove("hidden");
|
||||
initWoo();
|
||||
animate(0);
|
||||
} else {
|
||||
localStorage.setItem("wooMode", 0);
|
||||
forcedWoo ? null : wooCanvas.classList.add("hidden");
|
||||
};
|
||||
});
|
||||
|
||||
{% if forcedWoo %}
|
||||
// Handle forced woo
|
||||
forcedWoo = true;
|
||||
console.log("Forcing woo mode: woo!");
|
||||
wooCanvas.classList.remove("hidden");
|
||||
initWoo();
|
||||
animate(0);
|
||||
{% endif %}
|
||||
|
||||
|
||||
</script>
|
||||
<!-- /weather -->
|
||||
4
_includes/wooModeController.njk
Normal file
@@ -0,0 +1,4 @@
|
||||
<form class="siteSettingsToggle" id="wooModeController">
|
||||
<input type="checkbox" id="wooToggle" {% if metadata.wooModeOnByDefault %}checked{% endif %} />
|
||||
<label for="wooToggle">Woo mode?</label>
|
||||
</form>
|
||||
16
_includes/wooModeStyle.njk
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- Woo mode style -->
|
||||
<style>
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
opacity: .35;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
<!-- / Woo mode style -->
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ structuredData: none
|
||||
# Privacy Statement
|
||||
|
||||
## Data collection and use
|
||||
I don’t collect any of your personal information, full-stop. All webfonts, icons, and images are hosted locally (these things can sometimes be used to [track people across the internet](https://www.firstpost.com/world/how-google-uses-fonts-to-track-what-users-do-online-and-sell-data-to-advertisers-12496552.html) otherwise). I use [umami](https://umami.is), a free and open source, privacy-respecting analytics tool, to see how many people visit this website. As of 2025-12-16 I also use [Goat Counter](https://goatcounter.com) (also FLOSS and privacy-respecting) as a backup to umami, as my self-hosted umami instance shat the bed the other day and I lost a bunch of data.
|
||||
I don’t collect any of your personal information, full-stop. All webfonts, icons, and images are hosted locally (these things can sometimes be used to [track people across the internet](https://www.firstpost.com/world/how-google-uses-fonts-to-track-what-users-do-online-and-sell-data-to-advertisers-12496552.html) otherwise). I use [umami](https://umami.is), a free and open source, privacy-respecting analytics tool, to see how many people visit this website. As of 2025-12-16 I also use [Goat Counter](https://goatcounter.com) and [Mochi](https://mochi.meadow.cafe/) (also FLOSS and privacy-respecting) as backups to umami, as my self-hosted umami instance shat the bed the other day and I lost a bunch of data.
|
||||
|
||||
410
content/blog/100-webmaster-questions.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
title: "100 Webmaster Questions"
|
||||
description: "Answering a lot of questions."
|
||||
date: 2026-02-05
|
||||
tags:
|
||||
- Questionnaire
|
||||
synopsis: "Answering a lot of questions."
|
||||
mastodon_id: "116021932929019975"
|
||||
---
|
||||
I found this questionnaire on the website of my [Geekring](https://geekring.net/) neighbor [Corvidae](https://corvidae.digital/100). [Original questions sheet on mousling.net](https://mouseling.net/100webmaster.txt).
|
||||
|
||||
## Please introduce yourself.
|
||||
|
||||
I'm Nathan. I live in Chicago, U.S.A., and have far too many interests. I play classical trombone for fun, make incense sticks, write things, cook vegan food, and more.
|
||||
|
||||
## How long have you been making websites?
|
||||
|
||||
Beside tinkering with HTML and CSS to customize my MySpace profile, I first started back in, oh, 2004 or 2005.
|
||||
|
||||
## And what got you into the hobby?
|
||||
|
||||
I just became so exhausted with both mainstream social media and the idea of a “personal brand” that I decided I needed a space on the internet where I could just sort of exist as a human honestly, and on my own terms.
|
||||
|
||||
## What kind of website are you most interested in?
|
||||
|
||||
Personal sites, featuring real people earnestly documenting their existence creatively. I'd like everyone to have one.
|
||||
|
||||
## What's your workflow? do you plan your websites out thoroughly or do you come up with the design as you go along?
|
||||
|
||||
If I'm making a site for a client, the whole thing is absolutely planned out first in order to nail down the scope. Then the design has to be done, from concepts to revisions and final approval, before any code can be written. If I'm making something for myself, I often just start with code and see where it goes.
|
||||
|
||||
## Please link to your biggest inspirations.
|
||||
|
||||
This might sound a bit conceited, but I don't know that I have any. I find that a lot of competition-winning design work is actually just ghastly to use and totally ignores accessibility, so I kind of have beef with many of my contemporaries. Really, the goal for my website was to try to make an excellent reading experience. It's not the most cutting-edge design, or the most interesting, but I think I've at least done that.
|
||||
|
||||
## What's your favourite part about making websites?
|
||||
|
||||
I love getting to that stage where you have systems in place and updates become really simple. I get a lot of satisfaction in refactoring, and trying to make things modular and reusable.
|
||||
|
||||
## And the thing you struggle with the most?
|
||||
|
||||
Getting over the blank page. Starting from nothing is always stressful.
|
||||
|
||||
## Do you keep the same layout on all of your pages? or do you use different ones?
|
||||
|
||||
Pretty much, yea. I use fluid spacing and type-sizing so the whole site is responsive and usable at any screen size without a single media query. I don't have much interest in tackling all of those problems again for a different layout haha.
|
||||
|
||||
## How confident are you with css?
|
||||
|
||||
I think I'm pretty solid. CSS is becoming more and more capable and easy to use. Christ, we have variables (custom properties) and nested selectors these days. It's not often I find myself banging my head against the wall anymore at any rate. Now if only I could keep my CSS a little more tidy…
|
||||
|
||||
## Do you know how to correctly use `<dl>`?
|
||||
|
||||
I've never needed it, but I'm glad to have learned about it here.
|
||||
|
||||
## What is your favorite html element?
|
||||
|
||||
The dialog element is really neat. It does so much for you too.
|
||||
|
||||
## If you're making a new web page from scratch, what is the first thing you do?
|
||||
|
||||
I like to use [Eleventy](https://www.11ty.dev/), so I'm not likely to be starting from scratch. If I had to though, I'd probably go remind myself what HTML boilerplate I'm supposed to be using these days.
|
||||
|
||||
## Do you know javascript?
|
||||
|
||||
Yes. I use it for desktop automation too, with Node.
|
||||
|
||||
## How about php?
|
||||
|
||||
I fear PHP.
|
||||
|
||||
## Does your website have a theme that you stick to?
|
||||
|
||||
For sure.
|
||||
|
||||
## Are you more focused on content or design?
|
||||
|
||||
Content. The reading experience is pretty good, so while I sometimes feel that my site is a little dull compared to some other personal sites, I'm happy with that.
|
||||
|
||||
## Do you own a domain name? if not, would you ever want to?
|
||||
|
||||
Absolutely. A few, actually.
|
||||
|
||||
## What do you think of nostalgia-focused or "retro" websites?
|
||||
|
||||
I think they're fun and I like to see them.
|
||||
|
||||
## Is your html valid? do you even check?
|
||||
|
||||
Lord, I haven't thought about that in a minute. I may have to fix a few things…
|
||||
|
||||
## What are your opinion on buttons and banners?
|
||||
|
||||
They're fun. I'd like to make a dedicated page for them at some point.
|
||||
|
||||
## What do you think of button walls in particular?
|
||||
|
||||
I think they're fun.
|
||||
|
||||
## If you started over again, would you make something similar or completely different?
|
||||
|
||||
I think it would be pretty similar, to be honest.
|
||||
|
||||
## Are you envious of other people's websites?
|
||||
|
||||
All the time! I love the cool stuff people build onto their sites: music players, et cetera. I just have to remind myself that my site has different goals.
|
||||
|
||||
## What text editor do you use?
|
||||
|
||||
I use the woefully underrated [Kate](https://kate-editor.org/). For quick edits I use [KWrite](https://apps.kde.org/kwrite/). In the terminal, I use Nano.
|
||||
|
||||
## Why do you use that one?
|
||||
|
||||
Kate is a lightweight native application with no electron bloat, and it has so may features. KWrite is also native, but it's really pared down, which is what you want sometimes. And as for Nano, well I just don't want to have to memorize esoteric commands to edit text.
|
||||
|
||||
## Do you host your image files on your web server, or on another host?
|
||||
|
||||
All images and typefaces are served locally. I'm careful to optimize images, and I don't want my visitors to be tracked by Google via Google Fonts. I do use a PeerTube instance for video.
|
||||
|
||||
## This might not be relevant to you, but what's your opinion on the neocities vs. nekoweb debate?
|
||||
|
||||
No idea. I host my own stuff.
|
||||
|
||||
## How much server space would you estimate your main website takes up?
|
||||
|
||||
At the time of writing, it's 90.2 MiB, with 564 files and 247 directories. Nuts!
|
||||
|
||||
## Do you keep local backups of your files?
|
||||
|
||||
I develop my site locally, so there's that. I also have a copy on my Gitea instance.
|
||||
|
||||
## Do you prefer simple or highly visual websites?
|
||||
|
||||
It really depends on the goals of the site! If it's designed to be a visually engaging website that encourages exploration, it makes sense to have some visual complexity going on. On the other hand, if I'm there to read an article, I don't want that to be hindered by the design.
|
||||
|
||||
## Do you stick to certain colours? do you do that on purpose, or is it your subconscious?
|
||||
|
||||
It varies from project to project.
|
||||
|
||||
## Have you ever thought about quitting? why?
|
||||
|
||||
No, at least where it concerns my personal projects. Client work isn't as fun, so I think I'm going to be pretty choosy about what I work on for the foreseeable future (outside of my day-job) because I really have come to value my free time very highly.
|
||||
|
||||
## Do you have many webmaster friends, or is it a solitary hobby?
|
||||
|
||||
Not many, but one or two.
|
||||
|
||||
## Do people in your real life know about your website?
|
||||
|
||||
Yes. It's got my name on it haha.
|
||||
|
||||
## Do you update your website very often? how often is "very often"?
|
||||
|
||||
I do; I have "status" and "now burning" sections on my site for micro-blogging and listing what incense sticks I'm burning. I wrote [a wee utility](https://nathanupchurch.com/blog/Solving-SSG-Microblogging-Ergonomics-with-KDialog-for-Incense-Posting/) to make it quick and easy to post these during the work-day, so updates usually happen from at least every other day to multiple times a day.
|
||||
|
||||
## And the overall design, do you change that much? why or why not?
|
||||
|
||||
No. I feel that I've met my design goals for my site, and getting the fluid type / spacing system and variable typefaces right took so. much. time. I have zero interest in doing all of that work again.
|
||||
|
||||
## Is your website more you-focused, hobby-focused, or outside world-focused?
|
||||
|
||||
It's a little of everything. I really want my website to represent me as a whole human, so I try to make sure that there's a good mix of creativity, life stuff, writing on hobbies and interests, et cetera.
|
||||
|
||||
## Do you do web design professionally?
|
||||
|
||||
Yes. I have a design firm that I operate with my business partner [Davey](https://daveydynamite.neocities.org/), although we don't do a lot with it at the minute. I also sometimes do web projects as a part of my role at my day job.
|
||||
|
||||
## If not, would you like to? and if you're comfortable answering, what do you do for work?
|
||||
|
||||
My title doesn't elucidate much, but my day job is a director-level role at a Chicago non-profit. It's a small organization, so we all have a hand in operations, but I also do design work, process work, and a little automation. I do everything from design collateral for events, to leading digital infrastructure integration projects, and building processes and tools for our team. I know a lot of people would complain about doing so many things, but I really value variety and enjoy project work so it fits me pretty well.
|
||||
|
||||
## Do you communicate with people by email very much?
|
||||
|
||||
Yea, I often email with other incense-heads and I get occasional emails from people who read my blog. I enjoy it when people get in touch with questions and comments.
|
||||
|
||||
## Some people reject social media and use websites as a replacement. do you keep social media outside of your website?
|
||||
|
||||
I try to avoid mainstream social media, but I am [all over the fediverse](https://nathan.contact).
|
||||
|
||||
## How about instant messengers? do you use a mainstream one like discord or telegram? or something like matrix? do you avoid them?
|
||||
|
||||
With great resentment I still have a Discord account that I rarely use. I use [Matrix and Signal](https://nathan.contact) often, and I also have an [XMPP](https://nathan.contact) account through the FSF that I have never once used haha. I stay signed in though, so if you have XMPP and want to get in touch, it would be a novelty to use it for once.
|
||||
|
||||
## Do you listen to music while you work on websites? if so, what kinds of artists?
|
||||
|
||||
Often! It depends on my mood, and my taste in music is hugely varied and slightly insane. It could be anything from Hindustani classical to deathcore, drum and bass, Russian ecclesiastical music, classical trombone concertos, black metal, or trip-hop. To throw some artists at the wall: Christian Lindberg, Ladytron, Mora Prokaza, Enei, Pandit Jasraj, Marie Keyrouz.
|
||||
|
||||
## Do you keep everything you make on one website, or do you have more than one?
|
||||
|
||||
Just the one right now. I eventually plan to make a "professional" website for all the corpo-speak that employers like to see when hiring.
|
||||
|
||||
## On a similar note, do you keep to one topic on your site, or many?
|
||||
|
||||
I write about many things, but I do try to keep incense at the fore, because it's something that very few people write about—especially when it comes to making incense. It's so hard to find information on incense-making and I want to do my part to keep the tradition alive.
|
||||
|
||||
## Do you present your real self, or at least try? or do you construct a persona on purpose?
|
||||
|
||||
Yes, with some caveats. I swear much more in the flesh. My philosophy is that I treat speech on my website as though I'm at a casual gathering with mixed company.
|
||||
|
||||
## Have you ever made a good friend thanks to your website?
|
||||
|
||||
Not really. Outside of the incense scene, I don't really make many online friends.
|
||||
|
||||
## Are you happy with the way html and css currently work?
|
||||
|
||||
Mostly. I do wish I could get a bit more programmatic with CSS. They're working on it, though.
|
||||
|
||||
## What are practices that you think people should avoid?
|
||||
|
||||
Ignoring accessibility: alt text, contrast ratios, et cetera. It's so much easier now with semantic HTML, too, and the WCAG is published online for anyone to read.
|
||||
|
||||
## What about under-utilised practices, or things you think people should do more?
|
||||
|
||||
Beside accessibility, using containers to encapsulate elements so that they can be more easily positioned with CSS grids and flexbox. Also using the proper elements. For example my comment form, which I *did not* write, has a bunch of `<p>` tags as containers and doesn't separate the buttons from the input fields in separate containers, making styling a nightmare.
|
||||
|
||||
## Do you use a lot of semantic html? or are you guilty of generic structure?
|
||||
|
||||
Absolutely. Why wouldn't you?
|
||||
|
||||
## Do you consider different browsers?
|
||||
|
||||
For pro work, you have to. I once had a subcontractor complaining that a site I built wasn't working only to find out that she was using a browser that hadn't been updated in seven years. (We found out that this was Apple's fault. After a point, they stop letting you upgrade your OS, and that means Safari too. What a nightmare.) You've got to decide where the cutoff is in order to know what features you can use. For personal stuff, I just target the most current version of Firefox. Usually, it's Chrome playing fast and loose with web standards, so if you target Chrome it seems like there's no guarantee that your site will work in Firefox, whereas the opposite usually isn't true.
|
||||
|
||||
## Speaking of, what's your preferred browser? convince your readers why they should use it.
|
||||
|
||||
Anything not using the Chromium engine, so basically Firefox and derivatives. Google uses their superior Chrome user numbers to justify making features outside of open web standards. This causes sites to break on non-Chromium browsers. This behavior is part of an ongoing pattern of [EEE](https://en.wikipedia.org/wiki/Embrace,_extend,_and_extinguish) on Google's part, and if it is allowed to continue, it risks making the web worse for everyone. I would encourage people to avoid Chrome, and any browser that uses its engine. If we don't, Google will use the leverage to kill competition and screw us all over like it has done time and time again (XMPP and RSS are some examples).
|
||||
|
||||
## And what os are you on?
|
||||
|
||||
EndeavourOS, an Arch GNU/Linux derivative.
|
||||
|
||||
## Do you have a strong opinion on that, or do you just happen to use it?
|
||||
|
||||
Well, I strongly feel that GNU/Linux is better than proprietary options. EndeavourOS just fits my needs really well with its up-to-date packages, frequent updates, and flexibility.
|
||||
|
||||
## Are your websites mobile-friendly?
|
||||
|
||||
Of course!
|
||||
|
||||
## What are your thoughts on autoplay?
|
||||
|
||||
It's for the best that it's blocked by default, but I still wish I could get away with using it on one or two special pages.
|
||||
|
||||
## What are your thoughts on webrings? are you in any?
|
||||
|
||||
Big fan. I'm in two at the moment: [Fediring](https://fediring.net/), and [Geekring](https://geekring.net/).
|
||||
|
||||
## Do you have any web shrines? what do you like to see in that sort of page?
|
||||
|
||||
No, nothing like that. I like discovering them though.
|
||||
|
||||
## Are your websites "cliche," in your opinion?
|
||||
|
||||
I hope not!
|
||||
|
||||
## What is your ideal website? are you striving for that, or for something else?
|
||||
|
||||
Real, fun, and ever-changing.
|
||||
|
||||
## Are you an artist? do you draw or design your own assets?
|
||||
|
||||
While I do graphic design, I'm not really an illustrator or anything like that—beyond the odd project for fun. So designer yes, artist no.
|
||||
|
||||
## What are your favourite resource sites?
|
||||
|
||||
I have an absolute ton of them on my [/links](https://nathanupchurch.com/links/) page.
|
||||
|
||||
## Is there a habit you just can't get away from no matter how hard you try?
|
||||
|
||||
Sloppy CSS. The cascade is tricky, but I think I'm improving.
|
||||
|
||||
## What's your biggest advice for a new webmaster?
|
||||
|
||||
Take your time, and learn git and *make a new branch* for any significant and complicated work so that it's easy to revert it if you make a mistake or get stuck.
|
||||
|
||||
## Do you keep all your styling in css? or do you hard-code some?
|
||||
|
||||
I use inline styles only as a last resort, usually for styling widgets that I have little control over such as the comment form.
|
||||
|
||||
## What do you think of frameset layouts?
|
||||
|
||||
It's not the 90s any more.
|
||||
|
||||
## How about table-based layouts?
|
||||
|
||||
Again, we're in 2026. There is no excuse for this haha.
|
||||
|
||||
## Do you subscribe to the ideas of "one-column", "two-column" and "three-column" layouts? do you use any of these?
|
||||
|
||||
I mean, when you look at eye-tracking studies, you see that the way people engage with websites is always changing. I think that, so long as you're applying gestalt principles in your design, you're probably doing an okay job no matter the layout.
|
||||
|
||||
## Do you spend longer on the html or the css?
|
||||
|
||||
The CSS, without question.
|
||||
|
||||
## Have you ever made a page with no css? it's useful for your thoughts.
|
||||
|
||||
No, never!
|
||||
|
||||
## Do you ever find yourself making layouts with nothing to put on them? or do you only make layouts when the need arises?
|
||||
|
||||
Only when necessary.
|
||||
|
||||
## Would you consider yourself a beginner? or advanced? somewhere in the middle?
|
||||
|
||||
I wouldn't go so far as to say advanced, but I think I know what I'm doing.
|
||||
|
||||
## Do you have a habit of looking at the source code of websites you visit?
|
||||
|
||||
Not often, as many sites have crazy obfuscated code these days. It's not often that you find well formatted, human-readable source anymore. More often I find myself reading documentation or Stack Overflow.
|
||||
|
||||
## How did you learn how to make websites?
|
||||
|
||||
I began in what was either a digital graphics or multimedia class in school where we learned to make websites using Adobe Dreamweaver. In adulthood I wound up tripping and falling into a career that eventually led to me making a number of websites professionally. I started with Adobe Muse, actually winning Adobe's Muse Site of the Day at one point. Once Adobe canned Muse, I realized I was going to have to get serious about learning HTML, CSS, and JavaScript, so I did.
|
||||
|
||||
## Do you ever force elements to do things they're not supposed to?
|
||||
|
||||
I do my best to avoid hacks.
|
||||
|
||||
## Thoughts on floating elements?
|
||||
|
||||
Again I say: it's not the 90s anymore.
|
||||
|
||||
## When you're sizing stuff, what do you use first? do you use px, em, %, or something else?
|
||||
|
||||
I'm using my responsive spacing system, so it's something like `var(--space-l)`, which will resolve to something like `clamp(2.5rem, 2.2183rem + 1.4085vw, 3rem)`. It makes things very easy and keeps everything nice and consistent.
|
||||
|
||||
## Do you have a favourite font?
|
||||
|
||||
I wouldn't know how to begin to choose!
|
||||
|
||||
## Would you run a website with another person? how would that work?
|
||||
|
||||
Why not? If it was interesting.
|
||||
|
||||
## Do you surf the web to find new personal websites very often?
|
||||
|
||||
Yes, I really enjoy clicking through web-rings and buttons.
|
||||
|
||||
## Do you bookmark other people's websites? how would you feel knowing someone else bookmarked yours?
|
||||
|
||||
Not often, no.
|
||||
|
||||
## What do you want people to be most impressed with when they see your website?
|
||||
|
||||
The fact that the layout uses no media queries.
|
||||
|
||||
## Are you interested in technology outside of websites? do you collect?
|
||||
|
||||
Yea, I love computers and FLOSS software. I do try to make good purchases that will last a long time, so I don't have a *ton* of tech, but I'm certainly not short on gadgets.
|
||||
|
||||
## How often and for how long are you online?
|
||||
|
||||
I work on a computer, so most of the day.
|
||||
|
||||
## When it comes to your website, who is your target audience?
|
||||
|
||||
Mostly incense nerds and people who want to keep up with my life. It's super easy to get an audience if you're on the fediverse and write about tech, but there are already plenty of blogs like that out there.
|
||||
|
||||
## Have you ever been interested in xhtml?
|
||||
|
||||
Not especially. It seems like one of those things that just never really took off.
|
||||
|
||||
## Do you program in general? have you ever written a program for use with or on your website, not counting simple javascript?
|
||||
|
||||
Yea, I wrote [Poaster](https://nathanupchurch.com/blog/Solving-SSG-Microblogging-Ergonomics-with-KDialog-for-Incense-Posting/) in Ruby for some reason. I have also done desktop automation stuff with Node.
|
||||
|
||||
## Speaking of programs that help you make websites, what do you think of static site generators (ssgs)? have you ever used one?
|
||||
|
||||
Big fan of Eleventy. I love being able to just get straight to the content when I want to, and I love that my site is modular and simple to update.
|
||||
|
||||
## Do you keep a hitcounter? why or why not?
|
||||
|
||||
No, but I do use some privacy respecting FLOSS analytics tools. I like to see how many people are reading my posts and where they are from.
|
||||
|
||||
## Do you frequent forums? which ones?
|
||||
|
||||
I wouldn't use the term frequent, but I do have a presence on some FLOSS forums, as well as [Dogs on Acid](https://www.dogsonacid.com/), and [Ouddict](https://www.ouddict.com/).
|
||||
|
||||
## Do you write your page content directly into the editor, or do you prepare it elsewhere, like a text document or a word document?
|
||||
|
||||
I often use [Marknote](https://apps.kde.org/marknote/). It's new and a little rough around the edges, but the potential is huge and I already enjoy using it despite its sometimes very annoying bugs.
|
||||
|
||||
## Do you think you appear cool to others? a more accurate answer now: do other people ever say you're cool?
|
||||
|
||||
I've been told that I write like a 50 year old academic, so, no haha. I also don't know that adults call each other cool very often. Certainly not the cool ones anyway!
|
||||
|
||||
## Are you embarrassed of your old work? have you ever deleted everything out of shame?
|
||||
|
||||
Always and forever.
|
||||
|
||||
## Would you close down your website if you couldn't update it, or would you leave an archive?
|
||||
|
||||
I think I'd like to leave it.
|
||||
|
||||
## So you reveal a lot about yourself on your website? or are you more secretive?
|
||||
|
||||
I try not to overshare too much, but I do keep it real, I think.
|
||||
|
||||
## Are you willing to reveal who your best online friend is, and/or if they have a website?
|
||||
|
||||
I will never tell.
|
||||
|
||||
## And do you optimise the images on your website?
|
||||
|
||||
Yes, with the utterly incredible [Converseen](https://converseen.fasterland.net/).
|
||||
|
||||
## We're out of time! how do you feel after answering 100 questions? ... other than exhausted.
|
||||
|
||||
Ready for bed!
|
||||
@@ -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.
|
||||
|
||||
[](/img/boy_vienna_11_11/boy_vienna_11_11_incense_cigarette_sticks_2.webp)
|
||||
|
||||
|
||||
297
content/blog/thoughts-on-incense-quality-price-and-snobbery.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
title: "Incense: Thoughts on Quality, Price, and Snobbery"
|
||||
description: "Some thoughts on how we think about quality, how incense pricing relates to it, snobbery, and my service-industry past."
|
||||
date: 2026-01-31
|
||||
tags:
|
||||
- Incense
|
||||
synopsis: "Some thoughts on how we think about quality, how incense pricing relates to it, snobbery, and my service-industry past."
|
||||
imageURL: /img/thoughts-on-incense-quality-price-and-snobbery/tennendo-kyara-incense-stick-macro-shot.webp
|
||||
imageAlt: "A macro shot of a burning stick of incense with shallow depth of field."
|
||||
mastodon_id: "115993146633109522"
|
||||
---
|
||||
Whether discussing wine, spirits, perfumes, or incense, there is much back and
|
||||
forth on the subject of quality. On the one hand, there are the connoisseurs
|
||||
flashing their three-plus digit purchases on enthusiast forums, and on the
|
||||
other, there are the humble, salt-of-the-earth naysayers gleefully reminding
|
||||
them of that time a bottle of supermarket swill beat out a premium bottle in a
|
||||
wine competition. From fractions of a penny per stick for "hand dipped" fare,
|
||||
to
|
||||
[over ten dollars each for premium Japanese sticks](https://kikohincense.com/collections/kyara-incense/products/gyokushodo-en-no-sho)
|
||||
, the world of incense has something for every budget. It seems that for every
|
||||
person opining on the sublime beauty of the .5mm green-oil kyara and musk
|
||||
sticks they picked up for a trifling four-figures, there is another insisting
|
||||
that dollar-store punks soaked in a pungent bath of
|
||||
[liquid plastic](https://en.wikipedia.org/wiki/Dipropylene_glycol) and
|
||||
industrial aroma chemicals are just as good, and that anyone enjoying incense
|
||||
that cost more than pennies per stick is either a poseur or a rube brainwashed
|
||||
by the flashy marketing[^1] and pretty boxes of the Japanese incense industry.
|
||||
Amidst the bickering, newcomers to this fragrant world want to understand what
|
||||
quality means in the context of incense. How do they know that they're buying
|
||||
high quality incense? Where do they find it? How does quality relate to price?
|
||||
The reality is that there are as many answers as there are people, but I hope
|
||||
that I can add some nuance to the conversation, address some misconceptions,
|
||||
and, if I'm lucky, provide a little clarity on the subject.
|
||||
|
||||
[](/img/flora_botanical_incense_abundance_oud/agarwood_skins_vs_white_kinam_bead_waste.webp)
|
||||
|
||||
## What is quality, anyway?
|
||||
|
||||
In order to talk about quality, we first have to come to some agreement as to
|
||||
what the word means. In the Tibetan and Chinese traditions, incense is used not
|
||||
only for fragrance, but also as medicine. Therefore, a stick made with a
|
||||
preponderance of very fresh and pungent material prized for its medicinal
|
||||
properties might be considered high quality, although to you and I it may smell
|
||||
like burning twigs with a hint of sulfur. If, like me, you understand that
|
||||
there is approximately zero compelling evidence that incense is of any
|
||||
medicinal value whatsoever, you will likely disagree with this assessment. I
|
||||
have also heard that consumers of Chinese incense value incense that uses few
|
||||
to no concentrates, whether natural extracts or synthetics. To this market, a
|
||||
dry and subtle sandalwood stick might be perceived as being of high quality,
|
||||
whereas consumers of Indian incense—today almost entirely a product of
|
||||
perfumery—may find it utterly underwhelming compared to their usual nag champa,
|
||||
powerful enough to fragrance a large open space during puja. In the west, there
|
||||
is significant consumer demand for natural products[^2], so incense marketed as
|
||||
"natural" will be perceived as being higher quality.
|
||||
|
||||
It's plain to see that quality means different things to different people. But
|
||||
I wonder if it might be simply described as the degree to which something meets
|
||||
the *multiple* goals or needs of the person assessing its quality. As we'll see,
|
||||
enjoyment comes from many places. I strongly believe that, where it relates to
|
||||
consumables, the hang-up on raw sensory pleasure as the stick by which quality
|
||||
is measured needs to be put to bed. Was the week in which you had the most
|
||||
orgasms or ate the tastiest meals the highest quality week in your life?
|
||||
Perhaps it was, but I think that's unlikely.
|
||||
|
||||
I rarely drink wine, but even I have become radically bored with hearing
|
||||
countless recitations of the time a handful of sommeliers roundly embarrassed
|
||||
themselves by preferring a glass of supermarket wine over the competing *Chateau
|
||||
Au Frou-Frou 1995*. Beyond the tiresome repetition, this sneering retort to
|
||||
those who enjoy wines priced beyond a box of Barefoot belies a fundamental
|
||||
misunderstanding of why people buy expensive wines in the first place. Sure,
|
||||
posturing happens, but an enthusiast will snag that $400 grand cru not because
|
||||
they want to show off on Instagram, nor because they necessarily think it will
|
||||
taste better than a cheap bottle, but because they want to know what the output
|
||||
of the estate tastes like. They want the 2008 vintage because they hear that
|
||||
the humidity that year had a unique effect on the grapes. They aren't familiar
|
||||
with the profile of César grapes, and would like to try a single-varietal
|
||||
bottle using them. They like the floral notes that biodynamic wine-making
|
||||
methods offer. And sure, if you put a glass of bottom-shelf Chardonnay in their
|
||||
left hand and a glass of "the good stuff" in the right, the left hand may well
|
||||
meet the lips more often, but that's beside the point.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/FOH-Work-Event.jpg)
|
||||
|
||||
In another life, I worked at an up-market cocktail lounge where we stocked
|
||||
high-end spirits. One whiskey sold for $7,000[^3] a bottle. Pours of another
|
||||
went for over $400 apiece[^4]. But the fifteen year old Pappy Van Winkle in the
|
||||
middle of the right-hand side of those bar-shelves was just as good as that
|
||||
$7,000 bottle. Although it was over $200 less per glass than our most expensive
|
||||
pour, most people agreed that it tasted better. This was irrelevant; people
|
||||
paid the extra money because those more expensive whiskeys were close to
|
||||
impossible to get. By tasting them, you were tasting history—a precious liquid
|
||||
that would, sooner or later, be lost to time. To the guests buying these
|
||||
whiskeys, they were not of the utmost quality because they tasted the best.
|
||||
They were of the utmost quality because they met desires beyond the want of a
|
||||
tasty drink: a desire for knowledge, for experience, for a connection with the
|
||||
past. All the same, after a long shift, a bartender I worked with once quipped:
|
||||
"At the end of the night, I'm not looking for nuance," as he took a shot of
|
||||
bottom-shelf whiskey and cracked open a can of lager.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/In-The-Kitchen.jpg)
|
||||
|
||||
## On snobbery
|
||||
|
||||
If we take a closer look at practices that are often dismissed as snobbery, we
|
||||
soon realize that, even if they are weaponized as rituals of the upper class,
|
||||
they nonetheless have working class origins.
|
||||
|
||||
Complex lists of flavor notes are a best-effort by those who produce a
|
||||
wine/spirit/coffee/what-have-you to describe the product of their labor to
|
||||
people who haven't tried it. Sticking one's nose into a Glencairn glass and
|
||||
breathing in through the mouth will keep alcohol from instantly nose-blinding a
|
||||
bartender-in-training, one who will be smelling hundreds of spirits over the
|
||||
coming weeks. The precise weighing of coffee beans packed into a portafilter
|
||||
provides consistency of flavor from drink to drink throughout a busy service.
|
||||
Even the haughtiest bottle of champagne has a team of *workers* behind it who,
|
||||
in pursuit of excellence, devise practices that will later be derided as
|
||||
pretensions because of their association with the class of people that can
|
||||
afford the product.
|
||||
|
||||
Working people are the taste-makers. They always have been. They create
|
||||
excellence every day, categorize it, describe it, devise the best ways to
|
||||
discern and appreciate the differences between one product and another. Working
|
||||
people are best positioned to take on these tasks. Their deep familiarity with
|
||||
what they produce is a far-cry from the shallow collection and consumption that
|
||||
has been rendered into a hobby by the affluent.
|
||||
|
||||
## Does price matter?
|
||||
|
||||
So, with all that said, what exactly does *price* tell us? Obviously it will
|
||||
give us a clue as to how the brand is positioned in the market but, uniquely to
|
||||
incense, pricing can give us a very good clue as to the ingredients used in a
|
||||
stick. Sure, there are differently priced coffee beans, but the sheer breadth
|
||||
of the range of prices for incense ingredients is perhaps paralleled only by
|
||||
natural perfumery.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/a-lot-of-coffee-beans.JPG)
|
||||
|
||||
Scarce does not begin to describe the dearth of highly fragrant and resinated
|
||||
agarwood in today's world. Oman's prized frankincense is so terribly
|
||||
over-exploited that the Omani government has all but taken over the industry in
|
||||
the country, only allowing a small amount of the precious resin to be released
|
||||
each year—at a premium price. Woe betide you if you are caught so much as
|
||||
looking at a sandalwood tree the wrong way in India these days, and as hard as
|
||||
they try, Indonesia and Australia are not yet able to match the quantity or
|
||||
quality of output by India's sandalwood industry in its heyday. Typically, as
|
||||
the price increases for East Asian incense, so too does the quality and/or
|
||||
quantity of these precious aromatics, and any incongruence here would quickly
|
||||
be noticed by enthusiasts. From
|
||||
[pennies per gram for eucalyptus leaf](https://web.archive.org/web/20250906194216/https://scents-of-earth.com/eucalyptus-leaf-eucalyptus-globulus-india/)
|
||||
to well beyond the price of gold for
|
||||
[top-end agarwood](https://web.archive.org/web/20250428184307/https://www.ensaroud.com/product/white-kinam/)
|
||||
, the range is extreme. While modern lifestyle brands market low-to-mid-range
|
||||
sticks for obscene prices, whole-botanical based East Asian incense from well
|
||||
known incense houses are all but forced align their pricing with the quality of
|
||||
the ingredients. When your incense uses whole-plant materials, the best-grown,
|
||||
freshest, rarest, and most fragrant plants come at a significant price premium
|
||||
due to their rarity and the labor involved in cultivating them.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/sifting-ground-sage-for-incense-sticks_copy.webp)
|
||||
|
||||
For Indian style incense this situation is a little different. While higher
|
||||
prices might reflect the use of expensive natural oils as opposed to aroma
|
||||
chemicals, unfortunately, and as far as I know, incense using top-end natural
|
||||
materials is all but extinct in Indian brands. I am sure there are small
|
||||
artisans making premium incense in India, but it seems to be mostly smaller
|
||||
western operations such as [Jeomra's Räucherwelt](https://raeucherwelt.de/) that offer Indian-style
|
||||
incense made with premium natural materials. More-so than in Japanese incense,
|
||||
however, pricing seems to indicate effort for Indian sticks. As opposed to
|
||||
Japan's extruded sticks, it isn't at all uncommon to find agarbatti that are
|
||||
hand-rolled. It's debatable as to what difference this makes to the final
|
||||
fragrance. Some contend that the density of machine-extruded sticks negatively
|
||||
impacts the fragrance. I have also heard that machine extrusion limits the
|
||||
ingredients and composition of the incense dough. Regardless, it is inevitable
|
||||
that, in very cheap commodity products, corners will be cut. Some of these
|
||||
missing corners will surely affect fragrance. And of course, individual
|
||||
artisans will not have the benefit of industrial equipment or processes, and
|
||||
will thus charge more for their incense as it takes significantly more time to
|
||||
make.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/extruded-incense-sticks.webp)
|
||||
|
||||
What do all of these pricing details say about quality, then? Little.
|
||||
|
||||
While price can tell us about market positioning, ingredients, and effort, this
|
||||
may not mean much to you or I when it comes to our own ideas of quality. As I
|
||||
look in my incense-drawer I see a $12 box of vanilla Morning Star sticks from
|
||||
Nippon Kodo beside a tube of Brunei agarwood sticks from Yi-Xin Craft Incense:
|
||||
$50 for two grams. I've been burning the former since I was 15 years old and
|
||||
first discovered Japanese incense, a vast improvement over the cheap dipped
|
||||
sticks available to me previously. It's one of the few things capable of
|
||||
soothing sadness or anxiety in me, and I've been relying on it for this purpose
|
||||
ever since that first encounter. On the other hand, the Brunei represents an
|
||||
opportunity to sample the work of a small artisan. It's a chance to experience
|
||||
an extremely rare natural material and understand how the agarwood from Brunei
|
||||
differs from that found in Cambodia. I also very much enjoy the fragrance
|
||||
before bed. I wouldn't dare say that one of these sticks is better than the
|
||||
other. They are both competently prepared, low on off-notes, and offer a
|
||||
pleasing (to me) aroma. If the prices were exchanged tomorrow, I'd still buy
|
||||
both.
|
||||
|
||||
## Have Americans been bamboozled?
|
||||
|
||||
There seems to be a stereotype that American incense enthusiasts have been
|
||||
bamboozled into preferring quiet Japanese incense over cheaper, more fragrant
|
||||
Indian-style sticks by flashy marketing, product positioning, and fancy
|
||||
packaging. As an incense enthusiast and half-American, I must object on this
|
||||
point. Stick incense in this country is largely associated with stoner culture.
|
||||
It's seen as a cheap, smoky way to disguise the smell of burning cannabis
|
||||
(which is still illegal in many states). The incense most commonly available is
|
||||
typically bottom-of-the-barrel commodity fare with all of the burning oil,
|
||||
sawdust, and wood glue off-notes that it entails. Better Indian sticks, if
|
||||
available, are very strong for our modern, hermetically-sealed homes. And in
|
||||
the rooms of my small Chicago apartment, the powerful fragrances of Indian
|
||||
incense can quickly begin to feel like suffering for my sensitive nose, even if
|
||||
I might otherwise like them. There is also history at play. According to
|
||||
Michael Cousineau in *The Fragrant Path: A Guide to the Art of Incense,*
|
||||
Shoyeido introduced Japanese incense to the U.S.A. when the company made its
|
||||
debut in the 1893 Chicago World's Fair, where the "fragrance of incense wafting
|
||||
from the bazaar filled the Japanese Pavilion." For the event, Japan had far
|
||||
outspent any other foreign countries in constructing Phoenix Hall, a permanent
|
||||
and stunning example of Japanese architecture modeled on an ancient Buddhist
|
||||
temple. The response to the exhibit was such that Shoyeido developed the
|
||||
incense cone, a shape more likely to survive the long journey at sea, and
|
||||
demand soon became greater than the company's production capacity.
|
||||
|
||||
[.")](/img/thoughts-on-incense-quality-price-and-snobbery/hooden-phoenix-hall.webp)
|
||||
|
||||
By describing the rationale for any perceived preference for Japanese incense
|
||||
in the U.S.A., I don't mean to make any sort of value statement with respect to
|
||||
the incense of either India or Japan. But I will say that, for my needs,
|
||||
quality is largely to be found in Japanese sticks. That said, the Indian
|
||||
incense sent over by [Irene](https://blog.rauchfahne.de/en/) has been something
|
||||
of a revelation for me: well-balanced fragrances from well-made sticks that
|
||||
(mostly) speak up without becoming overpowering. I have been enjoying these
|
||||
sticks tremendously and I will almost certainly buy more. Nonetheless, they
|
||||
fulfill a different role than my usual choices. Japanese sticks give me the
|
||||
opportunity to experience genuine high-end botanicals in a way that Indian
|
||||
incense rarely does. And, at least so far, no Indian sticks have come to soothe
|
||||
my soul like those boring, beige little vanilla sticks from Nippon
|
||||
Kodo—although I'm sure they may, given time.
|
||||
|
||||
## Is natural better?
|
||||
|
||||
Perhaps, depending on your goals, but not inherently. People have very strong
|
||||
opinions on the topic of natural botanicals versus synthetic aroma chemicals,
|
||||
but here's the truth: when it comes to health, natural botanicals are no better
|
||||
tested for burning than synthetics. If anything, the opposite is true. I also
|
||||
suspect that most people who get headaches from strong incense are reacting to
|
||||
the strength of fragrance, not its constituent ingredients. After all, many
|
||||
aroma chemicals are identical to the compounds found in nature.
|
||||
|
||||
Another harsh truth is that consumers have no way of knowing whether the
|
||||
incense they burn is natural or not. Very few companies publish ingredients.
|
||||
Fewer publish all of them. There are also a wide variety of fragrances that
|
||||
you're simply not going to get without synthetics. Violet notes are practically
|
||||
never naturally derived, and whether or not synthetics are used, you're
|
||||
certainly not going to be getting any real kyara in your $14.99 box of
|
||||
[Tennendo Kyara](https://kikohincense.com/products/tennendo-kyara-incense) (as
|
||||
good as it is). The fact is that any respectable incense collection is going to
|
||||
contain a mixture of aroma chemicals and natural botanicals, so it's worth
|
||||
getting over this particular hangup early on.
|
||||
|
||||
That said, if you want to understand what, for instance, Australian sandalwood
|
||||
smells like in incense, you'll likely reach for a stick that at least
|
||||
prominently features the wood itself. Likewise, faux-and-low-agarwood sticks
|
||||
scratch an entirely different itch than those that make liberal use of high-end
|
||||
wood. They're both nice for different reasons.
|
||||
|
||||
[](/img/thoughts-on-incense-quality-price-and-snobbery/tennendo-kyara-incense-stick-macro-shot.webp)
|
||||
|
||||
## Putting it all together
|
||||
|
||||
I recognize that I haven't offered any concrete answers here, but I hope that I
|
||||
might have been able to provide a little context for the discussion around
|
||||
quality in incense. We know that price indicates, at very least, market
|
||||
positioning and, so long as we're not dealing with a lifestyle brand, it also
|
||||
gives us a clue as to the ingredients and effort that went into an incense,
|
||||
although to what degree depends on its origin. What represents quality to us
|
||||
depends on our preferences and goals. Are we interested in experiencing and
|
||||
understanding the fragrances of natural materials? Do we want to analyze the
|
||||
work of our favorite Indian perfumer? Are we looking for something that reduces
|
||||
anxiety? Do we simply want to perfume a space as efficiently as possible?
|
||||
Physical, emotional, intellectual, and yes, sometimes social desires will all
|
||||
contribute to our degree of satisfaction and perception of quality, regardless
|
||||
as to whether an incense is predominantly natural or not.
|
||||
|
||||
[^1]: I would like to point out that Japanese incense companies do close to no
|
||||
marketing at all here in the U.S.A., these days and what does occur is
|
||||
[not especially compelling](https://www.instagram.com/shoyeido_incense_usa/).
|
||||
|
||||
[^2]: Which often conflicts with your average consumer's exposure to highly
|
||||
concentrated synthetic fragrances and the expectations that this exposure
|
||||
implants in them when it comes to incense.
|
||||
|
||||
[^3]: For the curious, it was a Pappy 17 with the wax-dipped bottle.
|
||||
|
||||
[^4]: This was years ago; I dread to think what they'd go for now.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Woo-Woo Incense Description Generator and Other Site Updates"
|
||||
description: "New on this website: a handy tool to generate woo, comments, and weather."
|
||||
date: 2026-02-05
|
||||
tags:
|
||||
- Site Updates
|
||||
imageURL: /img/woo-woo-incense-description-generator-and-other-updates/woo-woo-incense-description-generator_copy.webp
|
||||
imageAlt: "A screenshot of the woo generator page. Swirling rainbow colors form the background."
|
||||
synopsis: "New on this website: a handy tool to generate woo, comments, and weather."
|
||||
mastodon_id: "116021705122518903"
|
||||
---
|
||||
## Woo-woo incense description generator
|
||||
Whether they are spiritually inclined or mere earth-bound fragrance-heads, all sorts of people are attracted to the fragrant world of incense. It's no surprise, then, that the language people use to describe the incense they make can get pretty colorful. If you've ever wished you could write so… *interestingly* about incense, my brand new [Woo-woo incense description generator](/special/woo-woo-incense-description-generator/) is just the ticket! Occupying the "special" section of [my sitemap](/sitemap/) along-side the flying toasters, with but one click of a button it spits out a fresh serving of woo, complete with ingredients and "medicinal effects."[^1] Why not [give it a try?](/special/woo-woo-incense-description-generator/)
|
||||
|
||||
[](/img/woo-woo-incense-description-generator-and-other-updates/woo-woo-incense-description-generator_copy.webp)
|
||||
|
||||
## Woo mode
|
||||
You may have noticed a "site settings" button at the bottom of the page. I set this up after the holidays with a "show weather" toggle to allow visitors who miss the snowflakes to enable them once again. In addition to this, there is now a "woo mode" toggle that allows you to have the psychedelic woo-woo incense description generator background on every page of the site! Both settings save to your browser's local storage, handily retaining the setting between visits for you with no cookies.
|
||||
|
||||
[](/img/woo-woo-incense-description-generator-and-other-updates/site-settings.webp)
|
||||
|
||||
## Guestbook back up
|
||||
For a while, the service that powers my [guestbook](/guestbook/) went down due to [some drama with Azure](https://meadow.cafe/blog/0008-azure-disabled-my-account-trip-to-the-cabin/) and lost some data. Therefore, if you notice that your message is missing, know that I didn't delete it on purpose and feel free to leave another.
|
||||
|
||||
## Comments now available
|
||||
I have, at long last, set up commenting via [Isso](https://isso-comments.de/) on blog posts and, as an unintended side effect, [quizzes](/quizzes), which I kind of like so I left it (there's only one quiz at the minute anyway). I wanted to do this ages ago, but I couldn't get Isso working on my [YunoHost](https://yunohost.org/index.en.html) server until recently. YunoHost is fantastic, but support can be scarce as hen's teeth, so we'll see how things go. Hopefully, it'll be rock solid and serve me well for years to come. Hopefully people will leave polite, thoughtful comments. Hopefully, spam is thin on the ground. Time will tell all!
|
||||
|
||||
[^1]: For the love of all that is good in this world do not take these seriously.
|
||||
@@ -4,6 +4,29 @@ title: Nathan Upchurch | Changelog
|
||||
structuredData: none
|
||||
---
|
||||
# Changelog
|
||||
* 2026-02-07
|
||||
* Updated [/wish](/wish).
|
||||
* 2026-02-04
|
||||
* Added post comments via [Isso](https://isso-comments.de/). Please don't make me regret this.
|
||||
* Add [Mochi](https://mochi.meadow.cafe/) privacy respecting analytics.
|
||||
* Implement togglable site-wide Woo-Mode™ in site settings.
|
||||
* 2026-02-01
|
||||
* Added [Woo-Woo Incense Description Generator](/special/woo-woo-incense-description-generator).
|
||||
* 2026-01-21
|
||||
* Updated [blogroll](/blogroll).
|
||||
* 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
|
||||
* Updated [/wish](/wish).
|
||||
* Updated [/links](/links).
|
||||
* 2026-01-07
|
||||
* Added [status](/status) function.
|
||||
* 2026-01-01
|
||||
* Added [/slashes](/slashes).
|
||||
* Updated [/sitemap](/sitemap).
|
||||
|
||||
@@ -70,6 +70,7 @@ Here are some links to pages and resources that I believe are worth sharing.
|
||||
## Eleventy resources
|
||||
* [11tyBundle](https://11tybundle.dev/)—Learn how others are making the most of 11ty, an exceptionally simple, flexible, and performant, open-source static site generator
|
||||
* [11tyCMS](https://11tycms.com/)—A local, serverless, dependable, and FLOSS CMS for websites made with Eleventy
|
||||
* [Pagefind](https://pagefind.app/)—A fully static search library that runs after Hugo, Eleventy, Jekyll, Next, Astro, SvelteKit, or any other website framework
|
||||
|
||||
## Free/libre software
|
||||
* [Free Software Foundation](https://fsf.org)—A nonprofit with a worldwide mission to promote computer user freedom
|
||||
@@ -83,7 +84,7 @@ Here are some links to pages and resources that I believe are worth sharing.
|
||||
## Health / Medical
|
||||
* [smelltrainingapp.com](https://smelltrainingapp.com/)—A free tool from Stockholms Universitet and and Karolinska Institutet to help patients with hyposmia or anosmia improve their sense of smell.
|
||||
|
||||
## Indieweb: discovery
|
||||
## Indieweb / personal web: discovery
|
||||
* [blogroll.org](https://blogroll.org/)—Because blogs are the soul of the web
|
||||
* [blogs.hn](https://blogs.hn/)—A directory of tech sites, primarily sourced from HackerNews
|
||||
* [blogscroll.com](https://blogscroll.com/)—An open directory of personal sites and blogs
|
||||
@@ -96,10 +97,12 @@ Here are some links to pages and resources that I believe are worth sharing.
|
||||
* [Mydora](https://mydora.restorativland.org/)—A continuous streaming player that gives you a deep dive into the lost archives of Myspace Music
|
||||
* [ooh.directory](https://ooh.directory/)—A collection of 2,358 blogs about every topic
|
||||
* [searchmysite.net](https://searchmysite.net/)—Search real content by real people from their personal websites
|
||||
* [Wiby](https://wiby.me/)—Search engine for the classic web
|
||||
|
||||
## Indieweb: resources
|
||||
## Indieweb / personal web: resources
|
||||
* [90s Cursor Effects](https://tholman.com/cursor-effects/)
|
||||
* [blinkies.cafe](https://blinkies.cafe/)—Blinkie maker
|
||||
* [Blot](https://blot.im/)—A tool that turns a folder into a website (paid)
|
||||
* [GIF Printer 2000](https://melonking.net/frames/pixelsea)
|
||||
* [GifCities](https://gifcities.org/)—The Geocities animated gif search from Internet Archive
|
||||
* [Gify Pet](https://melonking.net/frames/pet)—Who is there to watch over your site when you are gone? GifyPet will!
|
||||
@@ -107,8 +110,12 @@ Here are some links to pages and resources that I believe are worth sharing.
|
||||
* [Guestbooks](https://guestbooks.meadow.cafe/)—A free guestbook service for your website
|
||||
* [Hit counters](https://www.websiteout.net/counter.php)
|
||||
* [Nekoweb](https://nekoweb.org/)—A free static website hosting service
|
||||
* [neocities.org](https://neocities.org/)—Create your own free website.
|
||||
Unlimited creativity, zero ads.
|
||||
* [tamaNOTchi](https://tamanotchi.world/)—cute virtual pets you can customize, grow, and share on your blog or website
|
||||
* [tilde.fun](https://tilde.fun/)—A Linux machine on the internet where you can get a shell account
|
||||
* [tildepages](https://tildepages.org/)—Free & fast web hosting
|
||||
* [Tile-able website backgrounds](https://tiled-bg.blogspot.com/)
|
||||
|
||||
## Literature
|
||||
* [TypeLit.io](https://www.typelit.io/)—Test your typing online by practicing on your favorite literature
|
||||
@@ -231,3 +238,5 @@ Here are some links to pages and resources that I believe are worth sharing.
|
||||
* Microblogging
|
||||
* Search
|
||||
* VPN
|
||||
## Random cool stuff
|
||||
* [intertapes.net](https://intertapes.net/)—An updating collection of found cassette tapes from different locations. The audio fragments include: voice memos, field recordings, mixtapes, bootlegs and more.
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-08_17:47.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Kaden Kobunboku
|
||||
manufacturer: Baieido
|
||||
date: 2026-01-08 17:47:00
|
||||
time: 5:47 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-08_18:43.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Sarasoju
|
||||
manufacturer: Shunkohdo
|
||||
date: 2026-01-08 18:42:00
|
||||
time: 6:42 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-13_09:33.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Pearl
|
||||
manufacturer: Les Encens Du Monde
|
||||
date: 2026-01-13 09:32:00
|
||||
time: 9:32 AM
|
||||
---
|
||||
I'm finding that I quite like charcoal based incense for my small bathroom.
|
||||
7
content/now-burning/Now Burning_2026-01-13_15:31.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Atlas Cedar Experiment
|
||||
manufacturer: Irene of rauchfahne.de
|
||||
date: 2026-01-13 15:30:00
|
||||
time: 3:30 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-16_14:01.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Kobunboku
|
||||
manufacturer: Baieido
|
||||
date: 2026-01-16 14:01:00
|
||||
time: 2:01 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-21_18:56.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Kyara
|
||||
manufacturer: Tennendo
|
||||
date: 2026-01-21 18:56:00
|
||||
time: 6:56 PM
|
||||
---
|
||||
Absolute banger of a daily agarwood stick.
|
||||
7
content/now-burning/Now Burning_2026-01-23_10:08.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Rose Sawayaka
|
||||
manufacturer: Baieido
|
||||
date: 2026-01-23 10:08:00
|
||||
time: 10:08 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-23_11:57.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Suifu
|
||||
manufacturer: Yamadamatsu
|
||||
date: 2026-01-23 11:56:00
|
||||
time: 11:56 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-27_17:37.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Matsu no Tomo - Friend of Pine"
|
||||
manufacturer: Shoyeido
|
||||
date: 2026-01-27 17:37:00
|
||||
time: 5:37 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-01-28_09:22.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Awaji Island Koh-shi Coffee
|
||||
manufacturer: Kunjudo
|
||||
date: 2026-01-28 9:21:00
|
||||
time: 9:21 AM
|
||||
---
|
||||
Coming to dislike this one less as the days go by. Still not sure it smells like coffee though.
|
||||
7
content/now-burning/Now Burning_2026-01-30_09:56.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Moonlit Night
|
||||
manufacturer: Les Encens du Monde
|
||||
date: 2026-01-30 9:57:00
|
||||
time: 9:57 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-04_15:27.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Morikage
|
||||
manufacturer: Hikali Koh
|
||||
date: 2026-02-04 15:27:00
|
||||
time: 3:27 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-05_10:13.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Musk
|
||||
manufacturer: "The Mother’s Fragrances"
|
||||
date: 2026-02-05 10:12:00
|
||||
time: 10:12 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-05_10:38.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Agarwood
|
||||
manufacturer: Ganesha
|
||||
date: 2026-02-05 10:38:00
|
||||
time: 10:38 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-09_13:28.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Agarwood
|
||||
manufacturer: Ganesha
|
||||
date: 2026-02-09 13:20:00
|
||||
time: 1:20 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-09_22:56.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Musk
|
||||
manufacturer: "The Mother’s Fragrances"
|
||||
date: 2026-02-09 22:56:00
|
||||
time: 10:56 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-10_10:06.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Patchouli
|
||||
manufacturer: "The Mother’s Fragrances"
|
||||
date: 2026-02-10 10:06:00
|
||||
time: 10:06 AM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-10_12:42.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Oud
|
||||
manufacturer: Flora Botanical Incense
|
||||
date: 2026-02-10 12:42:00
|
||||
time: 12:42 PM
|
||||
---
|
||||
|
||||
7
content/now-burning/Now Burning_2026-02-11_10:17.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Hawaiian Summer
|
||||
manufacturer: Incense Apprentice
|
||||
date: 2026-02-11 10:16:00
|
||||
time: 10:16 AM
|
||||
---
|
||||
I think a little age has done this stick good. It's got a pleasant coolness.
|
||||
7
content/now-burning/Now Burning_2026-02-13_10:03.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Frankincense & Myrrh with Sacred Sage"
|
||||
manufacturer: Fred Soll
|
||||
date: 2026-02-13 10:02:00
|
||||
time: 10:02 AM
|
||||
---
|
||||
An incredibly strong stick. Ventilation is key!
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Faces in Bloom: Plumeria"
|
||||
manufacturer: Yi-Xin Craft Incense
|
||||
date: 2026-01-05 12:12:00
|
||||
time: 12:12 PM
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Koin
|
||||
manufacturer: Gyokushodo
|
||||
date: 2026-01-05 9:49:00
|
||||
time: 9:49 AM
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Rim Srüng Incense
|
||||
manufacturer: Tara Herbal
|
||||
date: 2026-01-07 11:23:00
|
||||
time: 11:23 AM
|
||||
---
|
||||
|
||||
26
content/prior-thoughts/index.njk
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
title: "Nathan Upchurch | Prior Thoughts"
|
||||
pagination:
|
||||
data: collections.priorThoughts
|
||||
generatePageOnEmptyData: true
|
||||
size: 20
|
||||
permalink: "prior-thoughts/{% if pagination.pageNumber > 0 %}page-{{ pagination.pageNumber + 1 }}/{% endif %}index.html"
|
||||
paginationRootDir: prior-thoughts
|
||||
---
|
||||
<h1>Prior thoughts:</h1>
|
||||
|
||||
{% set postsCount = pagination.items | length %}
|
||||
{% if postsCount > 0 %}
|
||||
{% set postslist = pagination.items %}
|
||||
{% set showPostListHeader = false %}
|
||||
{% include "statusList.njk" %}
|
||||
{% else %}
|
||||
<p>Nothing’s here yet!</p>
|
||||
{% endif %}
|
||||
|
||||
{% include "permalinkButtons.njk" %}
|
||||
|
||||
<a href="/status/">
|
||||
<button type="button">Latest »</button>
|
||||
</a>
|
||||
@@ -24,9 +24,12 @@ structuredData: none
|
||||
* [Now](/now)
|
||||
* [Now Burning](/now-burning)
|
||||
* [Once Burned](/once-burned)
|
||||
* [Prior Thoughts](/prior-thoughts)
|
||||
* [Referrals](/referrals)
|
||||
* [Slashes](/slashes)
|
||||
* Special
|
||||
* [Flying Toasters](/special/flying-toasters)
|
||||
* [Woo-Woo Incense Description Generator](/special/woo-woo-incense-description-generator)
|
||||
* [Status](/status)
|
||||
* [Wishes](/wish)
|
||||
* [Quizzes](/quizzes)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
title: "Woo-Woo Incense Description Generator"
|
||||
structuredData: none
|
||||
forcedWoo: true
|
||||
---
|
||||
<div class="container">
|
||||
<h1>Woo-Woo Incense Description Generator</h1>
|
||||
<p>Ever wonder how incense makers come up with those confusing esoteric descriptions of their incense complete with “medicinal effects?” Want some ideas to spice up your Instagram profile, website, or Etsy listings? Well you’re in luck! Click the button below to generate an incense description so mystical you’ll swear your chakras are aligned! Music by <a href="https://pixabay.com/users/saavane-32312792/">saavane on Pixabay</a>. Woo sound effect by <a href="https://freesound.org/s/30995/">UncleSigmund on freesound</a>. Background effect from <a href="https://codepen.io/tommyho/pen/JjgoZLK">Tommy Ho on Codepen</a>. No <a href="/ai">AI</a> used—I can write slop on my own!</p>
|
||||
|
||||
<script src="/js/incense-description-generator.js"></script>
|
||||
<script>
|
||||
let audioOnClick = new Audio("/audio/30995__unclesigmund__woo-2.mp3");
|
||||
let bgMusic = new Audio("/audio/new-sun-428916.mp3");
|
||||
|
||||
audioOnClick.volume = 0.4;
|
||||
bgMusic.volume = 0.4;
|
||||
bgMusic.loop = true;
|
||||
|
||||
window.addEventListener("pointermove", (e) => {
|
||||
bgMusic.play();
|
||||
});
|
||||
|
||||
window.addEventListener("touchstart", (e) => {
|
||||
bgMusic.play();
|
||||
});
|
||||
|
||||
const generateWoo = (elementId, button) => {
|
||||
const element = document.getElementById(elementId);
|
||||
const wooButton = document.getElementById(button);
|
||||
|
||||
audioOnClick.play();
|
||||
|
||||
element.innerHTML = `
|
||||
<div>
|
||||
${descriptionConstructor(adjective, noun, bodyPart, verb, preposition, ingredient)}
|
||||
</div>
|
||||
`;
|
||||
wooButton.innerHTML = 'Generate Some More Woo!';
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick="generateWoo('wooContainer', 'wooButton')" id="wooButton">Generate Some Woo!</button>
|
||||
<div class="card" id="wooContainer" style="padding: var(--space-s); margin-top: var(--space-l);">Click the button to generate woo…</div>
|
||||
</div>
|
||||
6
content/status/Status_2026-01-07_19:49.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
date: 2026-01-07 19:48:00
|
||||
emoji: 🪱
|
||||
comment: Looks like my new status feature is working as it should!
|
||||
---
|
||||
|
||||
5
content/status/Status_2026-01-09_18:07.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-09 18:05:00
|
||||
emoji: 🦷
|
||||
comment: Tried the viral Nathan and Sons Underbrush gum today. Review to come soon!
|
||||
---
|
||||
5
content/status/Status_2026-01-11_15:07.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-11 14:48:00
|
||||
emoji: 🦶
|
||||
comment: Drying some freshly washed spikenard root and the entire room reeks of it. Sol reckons it smells like feet.
|
||||
---
|
||||
5
content/status/Status_2026-01-12_10:42.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-12 10:42:00
|
||||
emoji: 🎨
|
||||
comment: "If you don't want to worry about pesky things like contrast ratios or accessibility: become an artist, not a designer."
|
||||
---
|
||||
5
content/status/Status_2026-01-13_11:05.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-13 11:04:00
|
||||
emoji: 🍵
|
||||
comment: "[Hard green tea](https://thedieline.com/studio-mpls-brings-70s-cool-to-wild-leaf-hard-green-tea/)? Stop it already."
|
||||
---
|
||||
5
content/status/Status_2026-01-14_14:41.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-14 14:40:00
|
||||
emoji: 💼
|
||||
comment: Nothing quite prepares you to see people your age and younger begin to post drivel on LinkedIn.
|
||||
---
|
||||
5
content/status/Status_2026-01-16_09:51.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-16 9:49:00
|
||||
emoji: 🥛
|
||||
comment: Sol got me a *large* soy-milk maker for Christmas. There's nothing like a hot cup of fresh smilk in the morning, and I'm about to make so much of it!
|
||||
---
|
||||
5
content/status/Status_2026-01-27_17:32.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-27 17:31:00
|
||||
emoji: ✒️
|
||||
comment: "A harsh truth for any new graphic designers out there: Most times, you'll get a logo. It will occasionally be vector. Once in a blue moon, it will also be CMYK / spot. Your client does not know what these things mean. They will never know what these things mean. There is nothing you can do about it."
|
||||
---
|
||||
5
content/status/Status_2026-02-03_11:07.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-02-03 11:04:00
|
||||
emoji: 💸
|
||||
comment: "Can we stop making fun of people who play the lottery please? We know we're not going to win it; we're just paying for that brief, illogical feeling of hope."
|
||||
---
|
||||
42
content/status/index.njk
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
eleventyExcludeFromCollections: true
|
||||
layout: layouts/base.njk
|
||||
title: "Nathan Upchurch | Status: What I'm thinking at the moment."
|
||||
structuredData: none
|
||||
permalink: "/status/index.html"
|
||||
---
|
||||
{% set status = collections.status | last %}
|
||||
|
||||
<h1>Nathan’s status:</h1>
|
||||
<article class="post microblog-post">
|
||||
<div class="microblog-status card">
|
||||
<span class="microblog-emoji">{{ status.data.emoji }}</span>
|
||||
|
||||
<div class="microblog-status-copy">
|
||||
<p>
|
||||
<span class="status-metadata">
|
||||
{% if metadata.author.url %}
|
||||
<a href="{{ metadata.author.url }}">
|
||||
{% endif %}
|
||||
|
||||
{% if metadata.author.name %}
|
||||
{{ metadata.author.name }}
|
||||
{% endif %}
|
||||
|
||||
{% if metadata.author.url %}
|
||||
</a><br />
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{{ status.data.comment | markdownify | safe }}<br />
|
||||
|
||||
<span class="status-metadata">
|
||||
{{ status.date | niceDate }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<a href="/prior-thoughts/">
|
||||
<button type="button">Previous Entries »</button>
|
||||
</a>
|
||||
6
content/status/status.11tydata.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
tags: ["status"],
|
||||
layout: "layouts/base.njk",
|
||||
permalink: false,
|
||||
structuredData: "none",
|
||||
};
|
||||
5
content/status/test.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
date: 2026-01-07 12:23:00
|
||||
emoji: 🤔
|
||||
comment: "Can we not do world war three, please?"
|
||||
---
|
||||
@@ -35,23 +35,40 @@ However if abstention seems unconscionable, I would be delighted if you were to
|
||||
* Vegan haggis - I just miss haggis, and I remember vegan / vegetarian haggis being quite good. I did make a batch of my own once with TVP and it was nice, but it took ages to make.
|
||||
|
||||
### Incense
|
||||
* [Baieido Kaden Kobunboku](https://kikohincense.com/products/baieido-kaden-kobunboku-incense?variant=33394631671896)
|
||||
* [Baieido Tokusen Kobunboku](https://kikohincense.com/collections/baieido-incense/products/tokusen-kobunboku)
|
||||
* [Inca Aromas White Breu](https://tarotarts.com/products/inca-aromas-all-natural-fair-trade-incense-white-breu-for-clarity-and-spirituality?variant=39838738251860)
|
||||
* [Kida Jinseido Ranjatai Incense](https://kikohincense.com/collections/kida-jinseido-incense-since-1937/products/kida-jinseido-ranjatai-incense)
|
||||
* [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)
|
||||
* [Nippon Kodo Kayuragi - Rose](https://kikohincense.com/collections/roses-and-chocolates-incense-collection/products/nippon-kodo-kayuragi-incense-rose)
|
||||
* [Nippon Kodo Hana no Hana - Rose](https://kikohincense.com/collections/roses-and-chocolates-incense-collection/products/nippon-kodo-hana-no-hana-incense-rose)
|
||||
* [Kin Objects Red Soil Aloeswood](https://kinobjects.com/products/red-soil-aloeswood-agarwood-incense-sticks?variant=40432647929879)
|
||||
* [Kokando Kaori no Kioku Chocolate](https://kikohincense.com/collections/roses-and-chocolates-incense-collection/products/kokando-kaori-no-kioku-chocolate-incense)
|
||||
* [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)
|
||||
* [Les Encens du Monde - Moonlit Night | Karin](https://lotuszenincense.com/products/moonlit-night-karin-by-les-encens-du-monde?shpxid=fa2eb8b9-373c-44b6-b675-585881e5540b)
|
||||
* [Mysore](https://www.aliexpress.us/item/3256808362085525.html)
|
||||
* [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)
|
||||
* [Shoyeido Overtones Patchouli](https://shoyeido.com/products/overtones-patchouli-incense)
|
||||
* [Shoyeido Premium Incense Sampler](https://shoyeido.com/products/premium-incense-assortment-sampler)
|
||||
* [Tennendo Hagi Rose Incense](https://kikohincense.com/collections/tennendo-incense-kikoh/products/tennendo-hagi-rose-incense)
|
||||
* [Tennendo Hanano Byakudan](https://www.japanincense.com/tn-0051.html)
|
||||
* [Tennendo Kyara Incense](https://kikohincense.com/products/tennendo-kyara-incense)
|
||||
* [Tennendo Sumire Violet Incense](https://kikohincense.com/products/tennendo-ysumire-violet-incense)
|
||||
* [Yamadamatsu Hyofu](https://kikohincense.com/collections/yamadamatsu-incense-kikoh/products/yamadamatsu-hyofu-incense)
|
||||
* [Yamadamatsu Kumoi](https://kikohincense.com/products/yamadamatsu-kumoyi-incense)
|
||||
* [Yamadamatsu Shoyo](https://www.japanincense.com/ym-0037.html)
|
||||
|
||||
### Tea
|
||||
* A malty black tea from either [Spirit Tea](https://spirittea.co/) or [Yunnan Sourcing](https://yunnansourcing.com/).
|
||||
* A peachy white tea from either [Spirit Tea](https://spirittea.co/) or [Yunnan Sourcing](https://yunnansourcing.com/).
|
||||
* A savory oolong from from either [Spirit Tea](https://spirittea.co/) or [Yunnan Sourcing](https://yunnansourcing.com/).
|
||||
* [Aged Citrus Peel Ripe Pu-erh Instant Tea Resin](https://yunnansourcing.us/products/aged-citrus-peel-ripe-pu-erh-instant-tea-resin?_pos=1&_ss=r)
|
||||
* [Sticky Rice Scent Herb Instant Ripe Pu-erh Tea Resin](https://yunnansourcing.us/products/sticky-rice-scent-herb-instant-ripe-pu-erh-tea-resin?_pos=11&_ss=r)
|
||||
* [Yunnan Rose Buds and Ripe Pu-erh Instant Tea Resin](https://yunnansourcing.us/products/yunnan-rose-buds-and-ripe-pu-erh-instant-tea-resin?_pos=6&_ss=r)
|
||||
|
||||
## Intangible
|
||||
|
||||
|
||||
@@ -17,29 +17,34 @@ const figoptions = {
|
||||
figcaption: true,
|
||||
};
|
||||
|
||||
const timeZone = "America/Chicago";
|
||||
|
||||
export default async function (eleventyConfig) {
|
||||
// Helper Functions
|
||||
eleventyConfig.addDateParsing((dateValue) => {
|
||||
let localDate;
|
||||
if (dateValue instanceof Date) {
|
||||
// and YAML
|
||||
localDate = DateTime.fromJSDate(dateValue, { zone: "utc" }).setZone(
|
||||
timeZone,
|
||||
{ keepLocalTime: true },
|
||||
);
|
||||
} else if (typeof dateValue === "string") {
|
||||
localDate = DateTime.fromISO(dateValue, { zone: timeZone });
|
||||
}
|
||||
if (localDate?.isValid === false) {
|
||||
throw new Error(
|
||||
`Invalid \`date\` value (${dateValue}) is invalid for ${this.page.inputPath}: ${localDate.invalidReason}`,
|
||||
);
|
||||
}
|
||||
return localDate;
|
||||
// Customize Markdown library settings:
|
||||
let markdownItOptions = {
|
||||
html: true,
|
||||
typographer: true,
|
||||
};
|
||||
|
||||
let mdLib = markdownIt(markdownItOptions);
|
||||
|
||||
eleventyConfig.amendLibrary("md", (mdLib) => {
|
||||
mdLib
|
||||
.use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.ariaHidden({
|
||||
placement: "after",
|
||||
class: "header-anchor",
|
||||
symbol: "#",
|
||||
ariaHidden: false,
|
||||
}),
|
||||
level: [1, 2, 3, 4],
|
||||
slugify: eleventyConfig.getFilter("slugify"),
|
||||
})
|
||||
.use(markdownItFootnote)
|
||||
.use(mdfigcaption, figoptions)
|
||||
.use(markdownItContainer, "info");
|
||||
});
|
||||
|
||||
eleventyConfig.setLibrary("md", mdLib);
|
||||
|
||||
// Collections
|
||||
eleventyConfig.addCollection("galleryImages", (collection) => {
|
||||
const galleries = collection.getAll()[0].data.galleries;
|
||||
@@ -76,6 +81,15 @@ export default async function (eleventyConfig) {
|
||||
return arr.slice(1, arr.length);
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("priorThoughts", async (collectionApi) => {
|
||||
const arr = [...collectionApi.getFilteredByTag("status")].sort(
|
||||
function (a, b) {
|
||||
return b.date - a.date;
|
||||
},
|
||||
);
|
||||
return arr.slice(1, arr.length);
|
||||
});
|
||||
|
||||
// Transforms
|
||||
eleventyConfig.addTransform("prettier", function (content, outputPath) {
|
||||
if (outputPath && outputPath.endsWith(".html")) {
|
||||
@@ -127,6 +141,10 @@ export default async function (eleventyConfig) {
|
||||
return `<abbr title="${def}">${abbr}</abbr>`;
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("markdownify", (markdownString) => {
|
||||
return mdLib.renderInline(markdownString);
|
||||
});
|
||||
|
||||
// Shortcodes
|
||||
// Audio player
|
||||
eleventyConfig.addShortcode(
|
||||
@@ -194,38 +212,9 @@ export default async function (eleventyConfig) {
|
||||
);
|
||||
});
|
||||
|
||||
// Customize Markdown library settings:
|
||||
let markdownItOptions = {
|
||||
html: true,
|
||||
typographer: true,
|
||||
};
|
||||
|
||||
let mdLib = markdownIt(markdownItOptions);
|
||||
|
||||
eleventyConfig.amendLibrary("md", (mdLib) => {
|
||||
mdLib
|
||||
.use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.ariaHidden({
|
||||
placement: "after",
|
||||
class: "header-anchor",
|
||||
symbol: "#",
|
||||
ariaHidden: false,
|
||||
}),
|
||||
level: [1, 2, 3, 4],
|
||||
slugify: eleventyConfig.getFilter("slugify"),
|
||||
})
|
||||
.use(markdownItFootnote)
|
||||
.use(mdfigcaption, figoptions)
|
||||
.use(markdownItContainer, "info");
|
||||
});
|
||||
|
||||
eleventyConfig.setLibrary("md", mdLib);
|
||||
|
||||
return {
|
||||
templateFormats: ["md", "njk", "html", "liquid"],
|
||||
|
||||
markdownTemplateEngine: "njk",
|
||||
|
||||
htmlTemplateEngine: "njk",
|
||||
|
||||
dir: {
|
||||
|
||||
BIN
public/audio/30995__unclesigmund__woo-2.mp3
Normal file
BIN
public/audio/new-sun-428916.mp3
Normal file
@@ -156,7 +156,7 @@ html {
|
||||
input.answer {
|
||||
display: inline;
|
||||
}
|
||||
input:not(.answer):not(#weatherToggle),
|
||||
input:not(.answer, .siteSettingsToggle input),
|
||||
textarea {
|
||||
background-color: var(--background-color);
|
||||
border: var(--border-details);
|
||||
@@ -271,7 +271,7 @@ section {
|
||||
grid-column: var(--span-grid);
|
||||
}
|
||||
/* Add fleuron to last <p> in section */
|
||||
> p:not(blockquote > p):last-child:after {
|
||||
> p:not(blockquote > p, p.isso-post-action):last-child:after {
|
||||
content: "\2766";
|
||||
display: inline;
|
||||
font-size: var(--step-1);
|
||||
@@ -372,7 +372,7 @@ main > p > a > img {
|
||||
pre {
|
||||
grid-column: var(--span-grid);
|
||||
}
|
||||
p:not(.quizQuestion) {
|
||||
p:not(.quizQuestion, .isso-input-wrapper) {
|
||||
margin-block: 0 1lh;
|
||||
}
|
||||
p,
|
||||
@@ -526,12 +526,13 @@ table th {
|
||||
"WONK" 1;
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
/* Mastodon */
|
||||
.continue-discussion {
|
||||
grid-column: var(--span-grid);
|
||||
}
|
||||
.continue-discussion button {
|
||||
margin-top: var(--space-xs);
|
||||
margin-top: calc(var(--space-s) * -1);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
|
||||
/* Code Fences */
|
||||
@@ -618,6 +619,11 @@ header .home-link {
|
||||
}
|
||||
}
|
||||
|
||||
.microblog-emoji {
|
||||
font-size: var(--step-6);
|
||||
margin-right: var(--space-s);
|
||||
}
|
||||
|
||||
.microblog-icon {
|
||||
filter: var(--logo-filter);
|
||||
height: var(--space-4xl);
|
||||
@@ -639,6 +645,28 @@ header .home-link {
|
||||
}
|
||||
}
|
||||
|
||||
.microblog-status {
|
||||
&.card {
|
||||
align-items: center;
|
||||
padding: var(--space-s);
|
||||
}
|
||||
.microblog-status-copy {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.status-metadata {
|
||||
font-family: var(--meta-font-family);
|
||||
font-size: var(--meta-font-size);
|
||||
font-style: var(--meta-font-style);
|
||||
font-variation-settings: var(--meta-font-variation-settings);
|
||||
line-height: calc(var(--meta-font-size) * 0.5 + var(--meta-font-size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Info Box */
|
||||
|
||||
.info {
|
||||
@@ -838,6 +866,36 @@ sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* Site Settings */
|
||||
#siteSettingsContainer {
|
||||
& button:not(#settingsDone) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.siteSettingsToggle {
|
||||
color: var(--text-color);
|
||||
font-size: var(--step--2);
|
||||
font-variation-settings: var(--font-variation-ui);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--ui-letter-spacing);
|
||||
font-family: var(--font-family-ui);
|
||||
& label {
|
||||
display: inline;
|
||||
}
|
||||
& input {
|
||||
accent-color: var(--contrast-color);
|
||||
background-color: var(--background-color);
|
||||
border: var(--border-details);
|
||||
border-color: var(--contrast-color);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text-color);
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--contrast-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
a.post-tag {
|
||||
background-color: var(--color-gray-20);
|
||||
|
||||
@@ -9,8 +9,16 @@
|
||||
}
|
||||
|
||||
.socialLinks a button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: var(--space-xl);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin: 0 0 var(--space-s) 0;
|
||||
& > img {
|
||||
max-width: var(--space-s);
|
||||
}
|
||||
}
|
||||
|
||||
h1.socialTitle {
|
||||
@@ -27,7 +35,7 @@ h1.socialTitle {
|
||||
}
|
||||
|
||||
img.profilePic {
|
||||
max-width: var(--space-3xl);
|
||||
max-width: var(--space-6xl);
|
||||
border-radius: 50%;
|
||||
border: solid 2px var(--text-color);
|
||||
}
|
||||
|
||||
42
public/img/icons/breeze/clock-symbolic.svg
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="clock-symbolic.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="41.5"
|
||||
inkscape:cx="11"
|
||||
inkscape:cy="11"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1080"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><defs
|
||||
id="defs3051"><style
|
||||
type="text/css"
|
||||
id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style><style
|
||||
type="text/css"
|
||||
id="current-color-scheme-9">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style></defs><path
|
||||
style="display:inline;fill:#faf5f5;fill-opacity:1;stroke:none;stroke-width:1.33333"
|
||||
d="m 11,2.9999996 c -4.4182786,0 -7.9999999,3.5817218 -7.9999999,8.0000004 0,4.418277 3.5817213,8 7.9999999,8 4.418277,0 8,-3.581723 8,-8 0,-4.4182786 -3.581723,-8.0000004 -8,-8.0000004 z m 0,1.3333339 c 3.681899,0 6.666667,2.9847679 6.666667,6.6666665 0,3.681899 -2.984768,6.666667 -6.666667,6.666667 C 7.3181014,17.666667 4.3333334,14.681899 4.3333334,11 4.3333334,7.3181014 7.3181014,4.3333335 11,4.3333335 Z M 9.6666667,5.6666668 V 11 12.333333 H 16.333333 V 11 H 11 V 5.6666668 Z"
|
||||
class="ColorScheme-Text"
|
||||
id="path1-2" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 62 KiB |
589
public/js/incense-description-generator.js
Normal file
@@ -0,0 +1,589 @@
|
||||
// Woo-woo incense description generator
|
||||
|
||||
const adjective = [
|
||||
"aromatic",
|
||||
"climatic",
|
||||
"cooling",
|
||||
"deep",
|
||||
"dulled",
|
||||
"effervescent",
|
||||
"effluent",
|
||||
"effulgent",
|
||||
"endless",
|
||||
"evocative",
|
||||
"grounding",
|
||||
"healing",
|
||||
"hidden",
|
||||
"indolent",
|
||||
"juicy",
|
||||
"lactescent",
|
||||
"lingering",
|
||||
"ministerial",
|
||||
"nourishing",
|
||||
"open",
|
||||
"precious",
|
||||
"pungent",
|
||||
"resonant",
|
||||
"restless",
|
||||
"sacred",
|
||||
"sensual",
|
||||
"shimmering",
|
||||
"sparkling",
|
||||
"still",
|
||||
"undulating",
|
||||
"unparalleled",
|
||||
"warming",
|
||||
];
|
||||
|
||||
const noun = [
|
||||
["astrology", "astrologies"],
|
||||
["bloom", "blooms"],
|
||||
["breath", "breaths"],
|
||||
["chakra", "chakras"],
|
||||
["clarity", "clarities"],
|
||||
["contribution", "contributions"],
|
||||
["curiosity", "curiosities"],
|
||||
["direction", "directions"],
|
||||
["ember", "embers"],
|
||||
["connection", "connections"],
|
||||
["constriction", "constrictions"],
|
||||
["coolness", "coolnesses"],
|
||||
["expression", "expressions"],
|
||||
["fire", "fires"],
|
||||
["geometry", "geometries"],
|
||||
["glow", "glows"],
|
||||
["harmony", "harmonies"],
|
||||
["meaning", "meanings"],
|
||||
["nourishment", "nourishments"],
|
||||
["onanism", "onanisms"],
|
||||
["relationship", "relationships"],
|
||||
["resonance", "resonances"],
|
||||
["ritual", "rituals"],
|
||||
["smoke", "smokes"],
|
||||
["space", "spaces"],
|
||||
["surface", "surfaces"],
|
||||
["tantra", "tantras"],
|
||||
["tension", "tensions"],
|
||||
["transformation", "transformations"],
|
||||
["vibration", "vibrations"],
|
||||
["wonder", "wonders"],
|
||||
];
|
||||
|
||||
const bodyPart = [
|
||||
"appendix",
|
||||
"armpit",
|
||||
"bile",
|
||||
"central core",
|
||||
"cheeks",
|
||||
"chest",
|
||||
"cuticles",
|
||||
"diaphragm",
|
||||
"fingernails",
|
||||
"flaps",
|
||||
"folds",
|
||||
"frenulum",
|
||||
"goiter",
|
||||
"humors",
|
||||
"limbs",
|
||||
"lingam",
|
||||
"legs",
|
||||
"lungs",
|
||||
"meatus",
|
||||
"mind",
|
||||
"paunch",
|
||||
"perineum",
|
||||
"ribs",
|
||||
"spine",
|
||||
"spleen",
|
||||
"throat",
|
||||
"uvula",
|
||||
"yoni",
|
||||
"wenis",
|
||||
];
|
||||
|
||||
const affliction = [
|
||||
"abasement",
|
||||
"acne",
|
||||
"appropriation",
|
||||
"bodaciousness",
|
||||
"cirrhosis",
|
||||
"concavity",
|
||||
"confiscation",
|
||||
"corpulence",
|
||||
"defenestration",
|
||||
"denunciation",
|
||||
"despondency",
|
||||
"erosion",
|
||||
"exasperation",
|
||||
"fatigue",
|
||||
"fibrillation",
|
||||
"flaking",
|
||||
"hardening",
|
||||
"hypertension",
|
||||
"ill-humor",
|
||||
"imbalance",
|
||||
"impertinence",
|
||||
"impotence",
|
||||
"impropriety",
|
||||
"incontinence",
|
||||
"indolence",
|
||||
"ingrowth",
|
||||
"itching",
|
||||
"leprosy",
|
||||
"malignancy",
|
||||
"mastication",
|
||||
"melancholia",
|
||||
"miasma",
|
||||
"mistreatment",
|
||||
"mortification",
|
||||
"necrosis",
|
||||
"plasticity",
|
||||
"protuberance",
|
||||
"psoriasis",
|
||||
"putrescence",
|
||||
"rumination",
|
||||
"schadenfreude",
|
||||
"scruples",
|
||||
"seizure",
|
||||
"sinkage",
|
||||
"softening",
|
||||
"stoppage",
|
||||
"swelling",
|
||||
"whiffiness",
|
||||
];
|
||||
|
||||
const verb = [
|
||||
["appreciate", "appreciates", "appreciating"],
|
||||
["blend", "blends", "blending"],
|
||||
["bloom", "blooms", "blooming"],
|
||||
["breathe", "breathes", "breathing"],
|
||||
["carry", "carries", "carrying"],
|
||||
["calm", "calms", "calming"],
|
||||
["cleanse", "cleanses", "cleansing"],
|
||||
["clear", "clears", "clearing"],
|
||||
["consolidate", "consolidates", "consolidating"],
|
||||
["cool", "cools", "cooling"],
|
||||
["dance", "dances", "dancing"],
|
||||
["descend", "descends", "descending"],
|
||||
["discern", "discerns", "discerning"],
|
||||
["exacerbate", "exacerbates", "exacerbating"],
|
||||
["fold", "folds", "folding"],
|
||||
["govern", "governs", "governing"],
|
||||
["harmonize", "harmonizes", "harmonizing"],
|
||||
["linger", "lingers", "lingering"],
|
||||
["masticate", "masticates", "masticating"],
|
||||
["nourish", "nourishes", "nourishing"],
|
||||
["retract", "retracts", "retracting"],
|
||||
["scatter", "scatters", "scattering"],
|
||||
["shift", "shifts", "shifting"],
|
||||
["sink", "sinks", "sinking"],
|
||||
["soften", "softens", "softening"],
|
||||
["tingle", "tingles", "tingling"],
|
||||
["titillate", "titillates", "titillating"],
|
||||
["ululate", "ululates", "ululating"],
|
||||
["undulate", "undulates", "undulating"],
|
||||
["vent", "vents", "venting"],
|
||||
["vindicate", "vindicates", "vindicating"],
|
||||
["warm", "warms", "warming"],
|
||||
["waver", "wavers", "wavering"],
|
||||
["weave", "weaves", "weaving"],
|
||||
];
|
||||
|
||||
const healingMethod = [
|
||||
["abate", "abates", "abating"],
|
||||
["alleviate", "alleviates", "alleviating"],
|
||||
["assuage", "assuages", "assuaging"],
|
||||
["balance", "balances", "balancing"],
|
||||
["broaden", "broadens", "broadening"],
|
||||
["cool", "cools", "cooling"],
|
||||
["discipline", "disciplines", "disciplining"],
|
||||
["edify", "edifies", "edifying"],
|
||||
["elevate", "elevates", "elevating"],
|
||||
["enrich", "enriches", "enriching"],
|
||||
["harmonize", "harmonizes", "harmonizing"],
|
||||
["heal", "heals", "healing"],
|
||||
["invigorate", "invigorates", "invigorating"],
|
||||
["lift", "lifts", "lifting"],
|
||||
["lighten", "lightens", "lightening"],
|
||||
["mitigate", "mitigates", "mitigating"],
|
||||
["promote", "promotes", "promoting"],
|
||||
["re-balance", "re-balances", "re-balancing"],
|
||||
["refine", "refines", "refining"],
|
||||
["regenerate", "regenerates", "regenerating"],
|
||||
["relieve", "relieves", "relieving"],
|
||||
["soften", "softens", "softening"],
|
||||
["sooth", "soothes", "soothing"],
|
||||
["uplift", "uplifts", "uplifting"],
|
||||
["warm", "warms", "warming"],
|
||||
];
|
||||
|
||||
const recipe = [
|
||||
"agarbatti",
|
||||
"blend",
|
||||
"composition",
|
||||
"fragrance",
|
||||
"incense",
|
||||
"recipe",
|
||||
"stick",
|
||||
];
|
||||
|
||||
const amount = [
|
||||
"base",
|
||||
"blessing",
|
||||
"boatload",
|
||||
"crumb",
|
||||
"dash",
|
||||
"dollop",
|
||||
"dusting",
|
||||
"glob",
|
||||
"hint",
|
||||
"pinch",
|
||||
"shred",
|
||||
"smattering",
|
||||
"smidgen",
|
||||
"sprinkling",
|
||||
"touch",
|
||||
"trace",
|
||||
];
|
||||
|
||||
const preposition = [
|
||||
"above",
|
||||
"across",
|
||||
"after",
|
||||
"against",
|
||||
"along",
|
||||
"amid",
|
||||
"among",
|
||||
"around",
|
||||
"as",
|
||||
"before",
|
||||
"behind",
|
||||
"below",
|
||||
"beneath",
|
||||
"beside",
|
||||
"between",
|
||||
"beyond",
|
||||
"following",
|
||||
"from",
|
||||
"into",
|
||||
"like",
|
||||
"near",
|
||||
"onto",
|
||||
"opposite",
|
||||
"over",
|
||||
"past",
|
||||
"round",
|
||||
"through",
|
||||
"to",
|
||||
"toward",
|
||||
"under",
|
||||
"underneath",
|
||||
"upon",
|
||||
"versus",
|
||||
"via",
|
||||
"with",
|
||||
"within",
|
||||
];
|
||||
|
||||
const ingredient = [
|
||||
"agarwood",
|
||||
"amber",
|
||||
"ambergris",
|
||||
"ambrette seed",
|
||||
"atlas cedar",
|
||||
"bergamot",
|
||||
"balsam of tolu",
|
||||
"basil",
|
||||
"bay laurel",
|
||||
"benzoin",
|
||||
"borneol camphor",
|
||||
"burgundy pitch",
|
||||
"calamus root",
|
||||
"cardamom",
|
||||
"cassia",
|
||||
"catnip",
|
||||
"chen pi",
|
||||
"chamomile",
|
||||
"cinnamon",
|
||||
"clove",
|
||||
"colophony pine",
|
||||
"copal",
|
||||
"coriander",
|
||||
"dammar",
|
||||
"dragon’s blood",
|
||||
"elemi",
|
||||
"eucalyptus",
|
||||
"frankincense",
|
||||
"galangal root",
|
||||
"galbanum",
|
||||
"ginger root",
|
||||
"guggul",
|
||||
"hibiscus",
|
||||
"hyssop",
|
||||
"juniper berry",
|
||||
"juniper",
|
||||
"labdanum",
|
||||
"laurel leaf",
|
||||
"lavender",
|
||||
"lemon balm",
|
||||
"lemon grass",
|
||||
"marjoram",
|
||||
"mastic",
|
||||
"mugwort",
|
||||
"musk root",
|
||||
"myrrh",
|
||||
"nutmeg",
|
||||
"oakmoss",
|
||||
"onycha",
|
||||
"opoponax",
|
||||
"orris root",
|
||||
"palo santo",
|
||||
"patchouli",
|
||||
"pine needle",
|
||||
"pine resin",
|
||||
"red cedar",
|
||||
"red sandalwood",
|
||||
"rose",
|
||||
"rosemary",
|
||||
"saffron",
|
||||
"sage",
|
||||
"sandalwood",
|
||||
"sandarac",
|
||||
"spikenard",
|
||||
"spruce",
|
||||
"star anise",
|
||||
"storax",
|
||||
"sweetgrass",
|
||||
"thyme",
|
||||
"tolu balsam",
|
||||
"tonka bean",
|
||||
"turkey rhubarb",
|
||||
"turmeric",
|
||||
"valerian",
|
||||
"vanilla",
|
||||
"vetiver",
|
||||
"weeping cypress",
|
||||
];
|
||||
|
||||
const getWord = (wordArr, usedArr, tense, plural) => {
|
||||
if (wordArr == noun) {
|
||||
let word = wordArr[(wordArr.length * Math.random()) | 0][0];
|
||||
let pluralWord = wordArr[(wordArr.length * Math.random()) | 0][1];
|
||||
|
||||
let wordIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
let pluralIsUsed = usedArr.indexOf(pluralWord) >= 0 ? true : false;
|
||||
|
||||
while (wordIsUsed == true || pluralIsUsed == true) {
|
||||
word = wordArr[(wordArr.length * Math.random()) | 0][0];
|
||||
pluralWord = wordArr[(wordArr.length * Math.random()) | 0][1];
|
||||
|
||||
wordIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
pluralIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
}
|
||||
|
||||
return plural ? pluralWord : word;
|
||||
} else if (wordArr == verb || wordArr == healingMethod) {
|
||||
let word = wordArr[(wordArr.length * Math.random()) | 0][0];
|
||||
let present = wordArr[(wordArr.length * Math.random()) | 0][1];
|
||||
let continuous = wordArr[(wordArr.length * Math.random()) | 0][2];
|
||||
|
||||
let wordIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
let presentIsUsed = usedArr.indexOf(present) >= 0 ? true : false;
|
||||
let continuousIsUsed = usedArr.indexOf(continuous) >= 0 ? true : false;
|
||||
|
||||
while (
|
||||
wordIsUsed == true ||
|
||||
presentIsUsed == true ||
|
||||
continuousIsUsed == true
|
||||
) {
|
||||
word = wordArr[(wordArr.length * Math.random()) | 0][0];
|
||||
present = wordArr[(wordArr.length * Math.random()) | 0][1];
|
||||
continuous = wordArr[(wordArr.length * Math.random()) | 0][2];
|
||||
|
||||
wordIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
presentIsUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
continuousIsUsed = usedArr.indexOf(continuous) >= 0 ? true : false;
|
||||
}
|
||||
|
||||
if (tense == "present") {
|
||||
return present;
|
||||
} else if (tense == "continuous") {
|
||||
return continuous;
|
||||
} else {
|
||||
return word;
|
||||
}
|
||||
} else {
|
||||
let word = wordArr[(wordArr.length * Math.random()) | 0];
|
||||
let isUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
|
||||
while (isUsed == true) {
|
||||
word = wordArr[(wordArr.length * Math.random()) | 0];
|
||||
isUsed = usedArr.indexOf(word) >= 0 ? true : false;
|
||||
}
|
||||
|
||||
return word;
|
||||
}
|
||||
};
|
||||
|
||||
const capitalize = (str) => {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
const coinFlip = () => {
|
||||
return Math.random() < 0.5 ? true : false;
|
||||
};
|
||||
|
||||
const randomNumber = (min, max) => {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
};
|
||||
|
||||
const descriptionConstructor = (
|
||||
adjective,
|
||||
noun,
|
||||
bodyPart,
|
||||
verb,
|
||||
preposition,
|
||||
ingredient,
|
||||
) => {
|
||||
let used = [];
|
||||
let description = "<p>";
|
||||
let currentWord = "";
|
||||
|
||||
const addWord = (list, plural, punctuation, tense, capitalizeBool) => {
|
||||
currentWord = getWord(list, used, tense, plural);
|
||||
|
||||
capitalizeBool
|
||||
? (description += capitalize(currentWord))
|
||||
: (description += currentWord);
|
||||
description += punctuation;
|
||||
|
||||
used.push(currentWord);
|
||||
};
|
||||
|
||||
const addAdjective = (punctuation, capitalize) => {
|
||||
addWord(adjective, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addAffliction = (punctuation, capitalize) => {
|
||||
addWord(affliction, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addAmount = (punctuation, capitalize) => {
|
||||
addWord(amount, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addBodyPart = (punctuation, capitalize) => {
|
||||
addWord(bodyPart, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addHealingMethod = (punctuation, tense, capitalize) => {
|
||||
addWord(healingMethod, false, punctuation, tense, capitalize);
|
||||
};
|
||||
const addIngredient = (punctuation, capitalize) => {
|
||||
addWord(ingredient, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addNoun = (punctuation, plural, capitalize) => {
|
||||
addWord(noun, plural, punctuation, null, capitalize);
|
||||
};
|
||||
const addPreposition = (punctuation, capitalize) => {
|
||||
addWord(preposition, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addRecipe = (punctuation, capitalize) => {
|
||||
addWord(recipe, false, punctuation, null, capitalize);
|
||||
};
|
||||
const addVerb = (punctuation, tense, capitalize) => {
|
||||
addWord(verb, false, punctuation, tense, capitalize);
|
||||
};
|
||||
|
||||
const insertCustomString = (str) => {
|
||||
description += str;
|
||||
};
|
||||
|
||||
const removeLastCharacter = () => {
|
||||
description = description.slice(0, description.length - 1);
|
||||
};
|
||||
|
||||
// Construct opening sentence.
|
||||
addAdjective(" ", true);
|
||||
addNoun(" ");
|
||||
addVerb(" ", "present");
|
||||
if (coinFlip()) addPreposition(" ");
|
||||
addVerb(" ", "continuous");
|
||||
coinFlip() ? addNoun(". ", true) : addNoun(". ");
|
||||
insertCustomString("</p>");
|
||||
|
||||
// Construct recipe description
|
||||
insertCustomString("<p>This ");
|
||||
addAdjective(" ");
|
||||
addRecipe(" ");
|
||||
addVerb(" ", "present");
|
||||
insertCustomString("and ");
|
||||
addVerb(" ", "present");
|
||||
addPreposition(" ");
|
||||
addNoun(" ");
|
||||
insertCustomString("and ");
|
||||
addNoun(".</p>");
|
||||
|
||||
// Construct ingredient roles
|
||||
const numOfIngredients = randomNumber(2, 5);
|
||||
|
||||
for (let i = 0; i < numOfIngredients; i++) {
|
||||
i == 0 ? insertCustomString("<p>") : null;
|
||||
if (coinFlip()) {
|
||||
insertCustomString("A ");
|
||||
addAmount(" ");
|
||||
insertCustomString("of ");
|
||||
addAdjective(" ");
|
||||
} else {
|
||||
addAdjective(" ", true);
|
||||
}
|
||||
if (coinFlip()) {
|
||||
if (coinFlip()) {
|
||||
insertCustomString("and ");
|
||||
addAdjective(" ");
|
||||
addIngredient(" ");
|
||||
} else {
|
||||
removeLastCharacter();
|
||||
insertCustomString(", yet ");
|
||||
addAdjective(", ");
|
||||
addIngredient(" ");
|
||||
}
|
||||
} else {
|
||||
addIngredient(" ");
|
||||
}
|
||||
addVerb(" ", "present");
|
||||
if (coinFlip()) {
|
||||
insertCustomString("and ");
|
||||
addVerb(" ", "present");
|
||||
}
|
||||
if (coinFlip()) {
|
||||
if (coinFlip()) {
|
||||
insertCustomString("while ");
|
||||
addHealingMethod(" ", "continuous");
|
||||
addAffliction(" ");
|
||||
insertCustomString("of the ");
|
||||
} else {
|
||||
insertCustomString("and ");
|
||||
addHealingMethod(" ", "present");
|
||||
addAffliction(" ");
|
||||
insertCustomString("of the ");
|
||||
}
|
||||
i == numOfIngredients - 1 ? addBodyPart(".</p>") : addBodyPart(". ");
|
||||
} else {
|
||||
i == numOfIngredients - 1 ? addNoun(".</p>") : addNoun(". ");
|
||||
}
|
||||
}
|
||||
|
||||
// Healing properties
|
||||
insertCustomString("<p>Also good for: ");
|
||||
const numOfHealingProps = randomNumber(1, 5);
|
||||
for (let i = 1; i <= numOfHealingProps; i++) {
|
||||
i == 1 ? addAffliction(" ", true) : addAffliction(" ");
|
||||
insertCustomString("of the ");
|
||||
if (i == numOfHealingProps - 1) {
|
||||
addBodyPart(", and ");
|
||||
} else if (i == numOfHealingProps) {
|
||||
addBodyPart(".</p>");
|
||||
} else {
|
||||
addBodyPart(", ");
|
||||
}
|
||||
}
|
||||
|
||||
return description;
|
||||
};
|
||||
260
public/js/speedlify-score.js
Normal 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;
|
||||
|
||||
// It’s 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;
|
||||
}
|
||||
|
||||
// Hasn’t 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();
|
||||
}
|
||||