Compare commits

...

6 Commits

Author SHA1 Message Date
b9124f18ab Add friendica to /me 2025-02-06 10:14:55 -06:00
bd05208aee Deprecate cowsay of the day 2025-02-06 10:14:38 -06:00
a25d8ef163 Update changelog 2025-02-04 18:43:36 -06:00
2de19c8882 Fix typo 2025-02-04 18:43:27 -06:00
793b6482b2 Re-implement open graph etc 2025-02-04 18:42:57 -06:00
9e62744415 Add article 2025-02-04 18:42:36 -06:00
10 changed files with 239 additions and 50 deletions

View File

@ -23,18 +23,11 @@ My blog, originally based on the very helpful eleventy-base-blog v8, although it
* Blogroll generated from _data/blogroll.js, with an automatically updated .opml so that visitors can import every blog in the list * Blogroll generated from _data/blogroll.js, with an automatically updated .opml so that visitors can import every blog in the list
* Image galleries * Image galleries
### Technical ### Fun
* Reusable web components: * Image galleries
* Card * Quizzes
* Mastodon comment
* Profile picture
* Embedded toot
* Embed audio
### Quality of Life ### Quality of Life
* Copyright notice, default post image, alt text, and author details defined in `metadata.js`. * Copyright notice, default post image, alt text, and author details defined in `metadata.js`.
* "Read Next" highlighting the previous blog post at the bottom of every post * "Read Next" highlighting the previous blog post at the bottom of every post
* robots.txt tells AI scrapers to GTFO * robots.txt tells AI scrapers to GTFO
### Weird and Wonderful
* [Accessible ~~cowsay~~ cowthink output embedding](https://upchur.ch/gitea/n_u/nathanupchurch.com/wiki#add-a-cowsay-to-a-post)

View File

@ -1,24 +0,0 @@
export default {
onScience: `
_________________________________________
( Once, when the secrets of science were )
( the jealously guarded property of a )
( small priesthood, the common man had no )
( hope of mastering their arcane )
( complexities. Years of study in musty )
( classrooms were prerequisite to )
( obtaining even a dim, incoherent )
( knowledge of science. )
( )
( Today all that has changed: a dim, )
( incoherent knowledge of science is )
( available to anyone. )
( )
( -- Tom Weller, "Science Made Stupid" )
-----------------------------------------
o ^__^
o (oo)\\_______
(__)\\ )\\/\\
||----w |
|| ||`
}

View File

@ -27,6 +27,12 @@ export default {
linkDisplay: "My Blog", linkDisplay: "My Blog",
iconURL: "/img/logo.svg", iconURL: "/img/logo.svg",
}, },
{
title: "Friendica",
linkURL: "https://friendica.world/profile/nathan",
linkDisplay: "Friendica",
iconURL: "/img/friendica.svg",
},
{ {
title: "Mastodon", title: "Mastodon",
linkURL: "https://lounge.town/@nathanu", linkURL: "https://lounge.town/@nathanu",

View File

@ -4,7 +4,6 @@
<link rel="icon" type="image/x-icon" href="/img/logo_favicon.svg"> <link rel="icon" type="image/x-icon" href="/img/logo_favicon.svg">
<link rel="blogroll" type="text/xml" href="{{ metadata.blogrollUrl }}"> <link rel="blogroll" type="text/xml" href="{{ metadata.blogrollUrl }}">
<meta name="description" content="{{ description or metadata.description }}"> <meta name="description" content="{{ description or metadata.description }}">
<meta name="image" content="{{ metadata.url }}{{ imageURL or metadata.author.profilePic }}">
<meta name="robots" content="noai, noimageai"> <meta name="robots" content="noai, noimageai">
<meta name="generator" content="{{ eleventy.generator }}"> <meta name="generator" content="{{ eleventy.generator }}">
<link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}"> <link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}">

View File

@ -22,4 +22,15 @@
"url": "{{ page.url | htmlBaseUrl(metadata.url) }}", "url": "{{ page.url | htmlBaseUrl(metadata.url) }}",
} }
</script> </script>
<!-- Open Graph -->
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ title }}" />
<meta property="og:description" content="{{ synopsis}}" />
<meta property="og:url" content="{{ page.url | htmlBaseUrl(metadata.url) }}" />
<meta property="og:image" content="{% if imageURL %}{{ imageURL | htmlBaseUrl(metadata.url) }}{% else %}{{ metadata.defaultPostImageURL | htmlBaseUrl(metadata.url) }}{% endif %}" />
<!-- Twitter card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ title }}" />
<meta name="twitter:description" content="{{ synopsis}}" />
<meta name="twitter:image" content="{% if imageURL %}{{ imageURL | htmlBaseUrl(metadata.url) }}{% else %}{{ metadata.defaultPostImageURL | htmlBaseUrl(metadata.url) }}{% endif %}" />
{% endif %} {% endif %}

View File

@ -1,13 +0,0 @@
---
title: Cowsay of the Day Science
description: An ASCII cow postulates on the state of science education in the modern world.
date: 2024-01-02
tags:
- Cowsay of the Day
synopsis: An ASCII cow postulates on the state of science education in the modern world.
imageURL: /img/cowsayOfTheDay.avif
imageAlt: An ASCII cow with a thought bubble containing the word wut
mastodon_id: "111688829907363670"
---
As a big-old nerd, I spend a lot of time in the terminal on my computer. When you spend a lot of time somewhere, you want it to be comfortable. As a part of making my terminal more homey, I've set it up to give me a random quote each time I start a new session, delivered, of course, by a cow. Here's today's cowsay of the day:
{{ cowList.onScience | cowsay | safe }}

View File

@ -0,0 +1,173 @@
---
title: "Building a Quiz System With Eleventy"
description: "Remember when internet quizzes were a thing? I wanted to bring them to my website."
date: 2025-02-04
tags:
- Site Updates
- Eleventy
synopsis: "Remember when internet quizzes were a thing? I wanted to bring them to my website."
imageURL: ""
imageAlt: ""
mastodon_id: "113948404881440370"
---
You might seen my [recent toot](https://lounge.town/@nathanu/113936929893588739) about the [fancy new "How Much of a Linux Nerd are You?" quiz](/quizzes/how-much-of-a-linux-nerd-are-you/) on my website. Some time ago, I realized that I missed taking fun internet quizzes and decided to implement a quiz system on my own site that would allow me to easily make fun quizzes to share. Here's how I built it with [Eleventy](https://www.11ty.dev/).
## The plan
First, I had to decide what sort of quizzes I wanted to be able to make. Some quizzes are designed to score the quiz-taker in order to place them into a category at the end, like those fun Buzzfeed quizzes that used to be so popular. Other quizzes are designed to test the quiz-takers knowledge of a subject, with each question having a definite right answer. I wanted to be able to do both, and I wanted my quizzes to be fairly flexible.
I decided to arrange things so that the quiz-author can enter any number of questions, answers, and consequences. While any number of answers can be entered for a given question, only one answer can be selected at a time, and every question must be answered. Each answer is assigned a number of points by the quiz author: positive, negative, or zero, and consequences each have a certain points threshold after which they are eligible to appear.
A consequence is a result that appears in a modal when the quiz-taker clicks the "submit" button at the end. It shows text defined by the quiz author, an image if the author chooses to include one, and it contains a "Score Details" dropdown that shows the number of points scored on each question.
I decided that I didn't want to use a global data file, not only because it isn't terribly ergonomic, but also because it's much simpler to take advantage of Eleventy's tag/collection system when possible, and frankly, I hoped to avoid some of the faffing about I had to do when [implementing image galleries](/blog/galleries/).
## Setting up the content directory
As I was going to use markdown files to build my quizzes, I needed to set up a content directory, `/content/quizzes/`, and set some defaults in `/content/quizzes/quizzes.11tydata.js` to make sure that everything I put inside of it was automatically tagged as a quiz, and would use the correct layout.
```javascript
export default {
tags: ["quiz"],
layout: "layouts/quizzes.njk",
};
```
By tagging these files as quizzes, a new [collection](https://www.11ty.dev/docs/collections/) containing all of my quizzes will be created, and I can add this collection to the `filterTagList` filter in my config file that allows me to easily omit everything that isn't a blog post from post-lists on my site, but that's out of scope for this article.
## Quiz Structure
YAML (or in fact any markup or programming language that respects whitespace) is no fun, but at least I won't wind up with a gargantuan JavaScript data file like I have [for my galleries](https://upchur.ch/gitea/n_u/nathanupchurch.com/src/branch/main/_data/galleries.js). Here's what `/content/quizzes/my-quiz.md` might look like:
``` yaml
---
title: ""
description: ""
date: 2025-02-04
imageURL: ""
imageAlt: ""
consequences:
- title: ""
points: 0
spiel: ""
image: ""
imageAlt: ""
questions:
- title: ""
image: ""
imageAlt: ""
imageCaption: ""
answers:
- name: ""
points: 0
---
This is a great quiz that I'm sure you'll have fun taking.
```
This results in a nice JavaScript object we can iterate through. In the body of the markdown document, beneath the front matter, is the text that can be injected via `{% raw %}{{ content }}{% endraw %}`. You'll see in a bit that this will go at the top of the quiz, beneath the title, which is injected with my post layout. This is so that it's easy to use markdown to style this part of the content, include images, et cetera, without worrying about trying to get that working while including it in the YAML.
## The quiz layout
Alright! Now that we have the quiz structure nailed down, we can write `/includes/layouts/quizzes.njk` which will iterate through the data and spit out an HTML form for us. I'm using the loop index number as the question number, which I can also use to set the `name` attribute for each of the answer `<input>` elements related to a given question. By doing this, the browser knows that the answers beneath a question are all related and will only allow the quiz-taker to select one of them.
I'm going to add a link to our yet-to-be-written script here and set the form to call `handleQuizSubmit()` on submit (`return false` prevents the page from refreshing when the submit button is clicked). Don't ask me why I put the script there precisely; as it isn't called until the submit button is clicked, I suppose it could go just about anywhere.
The points threshold for each consequence is stored in the [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) `data-points-threshold` so that we can use these numbers in our JavaScript.
The answers are assigned an ID that looks like this: `q[questionNumber]a[answerNumber]`. Beyond using this to also populate the `for` property of their respective labels, you could use this to link to individual answers too.
```html
---
layout: layouts/post.njk
structuredData: none
---{% raw %}
{{ content | safe }}
<section class="quiz">
<form onsubmit="handleQuizSubmit(); return false">
{% for question in questions %}
{% set q = loop.index %}
<div class="questionBox">
<p class="quizQuestion">{{ q }}. {{ question.title }}</p>
{% if question.image %}
<figure>
<a href="{{ question.image }}">
<img src="{{ question.image }}" alt="{{ question.imageAlt }}">
</a>
{% if question.imageCaption %}
<figcaption>{{ question.imageCaption }}</figcaption>
{% endif %}
</figure>
{% endif %}
<div class="answersBox">
{% for answer in question.answers %}
<div class="answerBox">
<input class="answer" type="radio" value="{{ answer.points }}" id="q{{ q }}a{{ loop.index }}" name="{{ q }}" required>
<label for="q{{ q }}a{{ loop.index }}">{{ answer.name }}</label>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<script src="/js/quiz.js"></script>
<button>Submit</button>
</form>
</section>
{% for consequence in consequences %}
<dialog class="consequence" data-points-threshold="{{ consequence.points }}">
<h2>{{ consequence.title }}</h2>
<p>{{ consequence.spiel }}</p>
{% if consequence.image %}
<img src="{{ consequence.image }}" alt="{{ consequence.imageAlt }}">
{% endif %}
<details>
<summary>Score Details</summary>
<p class="scoreDetails"></p>
</details>
<form method="dialog">
<button>Thanks</button>
</form>
</dialog>
{% endfor %}{% endraw %}
```
All of the consequences are rendered as `<dialog>` elements that we can open as a modal later with our script. And look, I know people have opinions about JavaScript, but I really didn't fancy the extra build time, bandwidth, and effort it would have taken to avoid fourty lines of simple JavaScript, and to be honest, I *like* JavaScript. I think it's useful and fun to write, so there.
## The quiz script
As far as logic goes, in `/js/quiz.js` we first want to calculate the score, and get the data to populate the `<details>` elements in our consequence modals. This is handled by `score()`, which will return an object containing the total number of points scored and an array containing the points scored on each question. When we have that, we'll go ahead and `populateDetails()` and finally use `dishOutConsequences()` to launch the freshly updated `<dialog>` as a modal via `showModal()`.
```javascript
const score = (answers) => {
let total = 0;
let scores = [];
for (let i = 0; i < answers.length; i++) {
const questionNumber = answers[i].name;
if (answers[i].checked) {
total += Number(answers[i].value);
scores.push({
questionNumber: questionNumber,
points: answers[i].value,
});
}
}
return { totalPoints: total, scores: scores };
};
const dishOutConsequences = (consequences, points) => {
for (let i = consequences.length - 1; i >= 0; i--) {
if (points >= Number(consequences[i].dataset.pointsThreshold)) {
consequences[i].showModal();
return;
}
}
};
const populateDetails = (detailsElement, scores, total) => {
detailsElement.innerHTML = `Total Score: ${total} points<br />`;
for (let i = 0; i < scores.length; i++) {
detailsElement.innerHTML += `<br />Question ${scores[i].questionNumber >= 10 ? scores[i].questionNumber : "0" + scores[i].questionNumber}: ${scores[i].points} points`;
}
};
const handleQuizSubmit = () => {
const answers = document.getElementsByClassName("answer");
const consequences = document.getElementsByClassName("consequence");
const details = document.getElementsByClassName("scoreDetails");
const totalPoints = score(answers).totalPoints;
const scoreDetails = score(answers).scores;
for (let i = 0; i < details.length; i++) {
populateDetails(details[i], scoreDetails, totalPoints);
}
dishOutConsequences(consequences, totalPoints);
};
```
And with that, our quiz ought to be operational! After this, I went ahead and listed my latest quiz on my index page, but that's beyond the scope of this article. It took me some time to get around to finishing this, but as you can see, it wasn't terribly difficult at all. I hope you enjoyed reading about how I built my quiz system. Please let me know if you decide to implement something similar!

View File

@ -4,7 +4,12 @@ title: Nathan Upchurch | Changelog
structuredData: none structuredData: none
--- ---
# Changelog # Changelog
* 2025-02-01 * 2025-02-06
* Add [Friendica profile](https://friendica.world/profile/nathan) to [/me](/me).
* Deprecate cowsay of the day.
* 2025-02-04
* Re-implement support for Open Graph and Twitter Card metadata because [I'm an idiot](https://github.com/mastodon/mastodon/issues/33812#issuecomment-2635441141) and didn't realize that you can't use the `<meta>` tag for images and there appears to be no officially supported way to do this except for appropriating the mechanism reserved for app icons and favicons.
* 2025-02-02
* Implement [quiz features](/quizzes/) and add [first quiz](/quizzes/how-much-of-a-linux-nerd-are-you/). * Implement [quiz features](/quizzes/) and add [first quiz](/quizzes/how-much-of-a-linux-nerd-are-you/).
* 2025-02-01 * 2025-02-01
* Remove support for Open Graph and Twitter Card metadata because A. bloat, and B. screw Musk and Zuck. * Remove support for Open Graph and Twitter Card metadata because A. bloat, and B. screw Musk and Zuck.

View File

@ -299,4 +299,4 @@ questions:
--- ---
Are you *k*onfident in your KDE knowledge? Have you joined us now, shared the software, and spread the good *gn*ews about your newfound freedom? Do you find it *awk*ward when people use proprietary software? No more *Stall*ing; it's time to *find* out whether you're a true GNU/Linux nerd. Are you *k*onfident in your KDE knowledge? Have you joined us now, shared the software, and spread the good *gn*ews about your newfound freedom? Do you find it *awk*ward when people use proprietary software? No more *Stall*ing; it's time to *find* out whether you're a true GNU/Linux nerd.
*Note: please not type any of commands on this page into your terminal without first looking them up.* *Note: Please do not type any of the commands on this page into your terminal without first looking them up. And even then, probably still don't.*

39
public/img/friendica.svg Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="192"
height="192"
viewBox="0 0 1920 1920"
version="1.1"
id="svg1"
sodipodi:docname="friendica.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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">
<defs
id="defs1" />
<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="1.1888021"
inkscape:cx="-82.856517"
inkscape:cy="76.127054"
inkscape:window-width="2048"
inkscape:window-height="1080"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill="#febf19"
d="M40 371q0-136 98-234 98-97 234-97h1178q136 0 233 97 97 98 97 234v1178q0 136-97 234-97 97-233 97H372q-137 0-234-97-97-98-98-234Zm1510-258h-296v442H666v373l587-4 1 441H666v442h884q107 0 182-75 75-74 74-183V371q0-108-74-182-74-75-182-76z"
id="path1"
style="fill:#ffffff;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB