Compare commits

...

6 Commits

47 changed files with 2390 additions and 87 deletions

View File

@ -8,5 +8,5 @@ charset = utf-8
indent_style = tab
indent_size = 2
[.{yaml,yml,md,njk}]
[*.{yaml,yml,md,njk}]
indent_style = space

30
_config/filters.js Normal file
View File

@ -0,0 +1,30 @@
import { DateTime } from "luxon";
export default function(eleventyConfig) {
// Return the keys used in an object
eleventyConfig.addFilter("getKeys", target => {
return Object.keys(target);
});
/* For <time> elements */
eleventyConfig.addFilter("htmlDateString", (dateObj) => {
return DateTime.fromJSDate(dateObj, { zone: "utc" }).toFormat('yyyy-LL-dd');
});
/* Human-readable dates */
eleventyConfig.addFilter("readableDate", (dateObj, format, zone) => {
return DateTime.fromJSDate(dateObj, { zone: zone || "utc" })
.toLocaleString(DateTime.DATE_FULL);
});
/* Filter out structural tags */
eleventyConfig.addFilter("removeBasicTags", (tags) => {
return tags.filter(tag => ["all", "posts", "gallery", "reference", "tagPagination"].indexOf(tag) === -1);
});
/* What it says */
eleventyConfig.addFilter("sortAlphabetically", strings =>
(strings || []).sort((b, a) => b.localeCompare(a))
);
};

View File

@ -1,30 +0,0 @@
<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title or metadata.title }}</title>
<meta name="description" content="{{ description or metadata.description }}">
{# <link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}"> #}
<meta property="og:title" content="{{ title or metadata.title }}" />
<meta property="og:type" content="website" />
<meta property="og:description" content="{{ description or metadata.description }}" />
<meta property="og:site_name" content="{{ metadata.title }}" />
<meta name="generator" content="{{ eleventy.generator }}">
{# Styles #}
<link rel="stylesheet" href="/assets/css/main.css">
{# Fonts #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
{# Icons #}
<script src="https://kit.fontawesome.com/884dded219.js" crossorigin="anonymous"></script>
</head>
<body>
{{ content | safe }}
<body>

24
_includes/footer.njk Normal file
View File

@ -0,0 +1,24 @@
<footer>
<ul>
<li>
<a href="/colophon" title="colophon" aria-label="colophon"
{% if page.url == "/colophon/" %}aria-current="page"{% endif %}>
colophon
</a>
</li>
<li>
<a href="/" title="go home"
aria-label="go home | {{ metadata.title }} from {{ metadata.author }} in 2026"
{% if page.url == "/" %}aria-current="page"{% endif %}>
{{ metadata.title }} from {{ metadata.author }} in 2026</a>
</li>
<li>
<a href="https://heckin.technology/inherentlee/leecat.art" title="source code"
aria-label="source code" target="_blank" rel="external">
src
</a>
</li>
</ul>
</footer>

22
_includes/header.njk Normal file
View File

@ -0,0 +1,22 @@
<header>
<a href="#main" id="skip" title="skip to main content" aria-label="skip to main content">
<i class="fa-solid fa-forward" aria-hidden="true"></i> skip
</a>
<nav aria-label="main navigation">
<ul>
{% for entry in collections.all | eleventyNavigation %}
<li>
<a href="{{ entry.url }}" title="{{ entry.data.label }}"
{% if entry.url == page.url %}aria-current="page"{% endif %}
aria-label="{{ entry.data.label }}">
<i class="{{ entry.data.icon }}" aria-hidden="true"></i>
<span class="menu-text">{{ entry.title }}</span>
</a>
</li>
{% endfor %}
</ul>
</nav>
</header>

View File

@ -0,0 +1,46 @@
<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title or metadata.title }}</title>
<meta name="description" content="{{ description or metadata.description }}">
{# <link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}"> #}
<meta property="og:title" content="{{ title or metadata.title }}" />
<meta property="og:type" content="website" />
<meta property="og:description" content="{{ description or metadata.description }}" />
<meta property="og:site_name" content="{{ metadata.title }}" />
<meta name="generator" content="{{ eleventy.generator }}">
{# Fonts #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
{# Icons #}
<script src="https://kit.fontawesome.com/884dded219.js" crossorigin="anonymous"></script>
{# Styles #}
<style>{% include "css/main.css" %}</style>
<style>{% include "css/nav.css" %}</style>
<style>{% include "css/print.css" %}</style>
<style>{% getBundle "css" %}</style>
<script type="module">{% getBundle "js" %}</script>
</head>
<body>
{% include "header.njk" %}
<main id="main">
{{ content | safe }}
<hr>
</main>
{% include "footer.njk" %}
<!-- This page `{{ page.url }}` was built on {% currentBuildDate %} -->
<body>

View File

@ -0,0 +1,8 @@
---
layout: base.njk
---
{% css %}{% include "css/lists.css" %}{% endcss %}
<h1>{{ title }}</h1>
{{ content | safe }}

View File

@ -0,0 +1,10 @@
---
layout: base.njk
---
{% js %}{% include "node_modules/@zachleat/heading-anchors/heading-anchors.js" %}{% endjs %}
<heading-anchors content="<i class='fa-solid fa-anchor'></i>">
<h1>{{ title }}</h1>
{{ content | safe }}
</heading-anchors>

View File

@ -0,0 +1,37 @@
---
layout: base.njk
---
{% css %}{% include "css/post.css" %}{% endcss %}
{% css %}{% include "css/highlighting.css" %}{% endcss %}
{% js %}{% include "node_modules/@zachleat/heading-anchors/heading-anchors.js" %}{% endjs %}
<heading-anchors content="<i class='fa-solid fa-anchor'></i>">
<article>
<h1>{{ title }}</h1>
<div class="post-metadata">
<p>
posted on
<time datetime="{{ page.date | htmlDateString }}">{{ page.date | readableDate }}</time>
by {{ metadata.author }}
</p>
{% set relevantTags = tags | removeBasicTags %}
{% if relevantTags.length %}
<ul class="post-tags">
{% for tag in relevantTags %}
<li>
{% set tagUrl %}/tags/{{ tag | slugify }}/{% endset %}
<a href="{{ tagUrl }}">{{ tag }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if image %}
<img src="/img/{{ image.src }}" alt="{{ image.alt }}">
{% endif %}
{{ content | safe }}
</article>
</heading-anchors>

View File

@ -0,0 +1,6 @@
---
layout: base.njk
---
{% css %}{% include "css/resume.css" %}{% endcss %}
{{ content | safe }}

20
_includes/pagination.njk Normal file
View File

@ -0,0 +1,20 @@
{% if olderHref or newerHref %}
<nav aria-label="pagination">
<ol class="pagination">
{% if olderHref %}
<li class="older">
<a href="{{ olderHref }}">
<i class="fa-solid fa-hand-point-left" aria-hidden="true"></i> older
</a>
</li>
{% endif %}
{% if newerHref %}
<li class="newer">
<a href="{{ newerHref }}">
newer <i class="fa-solid fa-hand-point-right" aria-hidden="true"></i>
</a>
</li>
{% endif %}
</ol>
</nav>
{% endif %}

View File

@ -1,17 +1,17 @@
<ol>
<ol id="postlist">
{% for post in postlist %}
<li>
<a href="{{ post.url }}">
<li class="post">
<a class="postlink" href="{{ post.url }}">
{% if post.data.image %}
<img src="/img/{{ post.data.image.src }}" alt="{{ post.data.image.alt }}">
{% endif %}
<h2>{{ post.data.title }}</h2>
<ul>
{% for tag in post.data.tags %}
<ul class="postlist-tags">
{% for tag in post.data.tags | removeBasicTags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
</a>
</li>
{% endfor %}
<ol>
</ol>

25
_includes/webring.njk Normal file
View File

@ -0,0 +1,25 @@
<nav class="webring" aria-labelledby="{{ id }}">
<h3 id="{{ id }}">{{ name }}</h3>
<p>This site is a member of the <a rel="external" target="_blank" href="{{ url }}">{{ name }}</a>. Navigate through the webring:</p>
<ul>
<li class="prev">
<a rel="external" referrerpolicy="strict-origin" href="{{ prev }}" target="_blank" title="{{ name }} - previous website">
<i class="fa-solid fa-hand-point-left" aria-hidden="true"></i>
prev
</a>
</li>
<li class="rand">
<a rel="external" referrerpolicy="strict-origin" href="{{ rand }}" target="_blank" title="{{ name }} - random website">
<i class="fa-solid fa-dice" aria-hidden="true"></i>
rand
<i class="fa-solid fa-dice" aria-hidden="true"></i>
</a>
</li>
<li class="next">
<a rel="external" referrerpolicy="strict-origin" href="{{ next }}" target="_blank" title="{{ name }} - next website">
next
<i class="fa-solid fa-hand-point-right" aria-hidden="true"></i>
</a>
</li>
</ul>
</nav>

126
css/highlighting.css Normal file
View File

@ -0,0 +1,126 @@
/* Adapted from PrismJS 1.30.0 Tomorrow Night theme
https://prismjs.com/download
*/
code,
pre,
code[class*=language-],
pre[class*=language-] {
font-family: var(--font-family-code);
background-color: var(--color-bg-alt);
font-size: .9rem;
text-shadow: 0 1px var(--color-shadow);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre,
pre[class*=language-] {
margin: 1rem 0;
padding: 1rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
border-radius: .5rem;
overflow: auto;
}
:not(pre)>code,
:not(pre)>code[class*=language-] {
padding: .2rem;
border-radius: .25rem;
white-space: normal;
}
/* Selected text */
code ::-moz-selection,
code::-moz-selection,
pre ::-moz-selection,
pre::-moz-selection,
code[class*=language-] ::-moz-selection,
code[class*=language-]::-moz-selection,
pre[class*=language-] ::-moz-selection,
pre[class*=language-]::-moz-selection,
code ::selection,
code::selection,
pre ::selection,
pre::selection,
code[class*=language-] ::selection,
code[class*=language-]::selection,
pre[class*=language-] ::selection,
pre[class*=language-]::selection {
text-shadow: none;
background-color: var(--color-bg);
}
/* Syntax highlighting */
.token.namespace {
opacity: .7;
}
.token.bold,
.token.important {
font-weight:700
}
.token.italic {
font-style:italic
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation {
color: var(--color-grey);
}
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag {
color: var(--color-red);
}
.token.boolean,
.token.function,
.token.number {
color: var(--color-orange);
}
.token.class-name,
.token.constant,
.token.property,
.token.symbol {
color: var(--color-yellow);
}
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted {
color: var(--color-green);
}
.token.entity,
.token.operator,
.token.url,
.token.function-name {
color:var(--color-blue);
}
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: var(--color-purple);
}

219
css/lists.css Normal file
View File

@ -0,0 +1,219 @@
#postlist,
#taglist {
list-style: none;
}
#postlist, .post,
#taglist, .tag {
margin: 0;
}
/* Odd-numbered posts & tag layout/coloration */
.post:nth-child(odd) .postlink,
.tag:nth-child(odd) .taglink {
grid-template-areas:
'img h2'
'img info'
'img .';
grid-template-columns: 45% auto;;
--color-primary: var(--color-teal);
--color-accent: var(--color-pink);
}
/* Even-numbered posts & tags layout/coloration */
.post:nth-child(even) .postlink,
.tag:nth-child(even) .taglink {
grid-template-areas:
'h2 img'
'info img'
'. img';
grid-template-columns: auto 45%;
--color-primary: var(--color-pink);
--color-accent: var(--color-teal);
}
/* Layout for all posts on mobile */
@media (max-width: 650px) {
.post:nth-child(n) .postlink,
.tag:nth-child(n) .taglink {
grid-template-areas:
'img'
'h2'
'info';
grid-template-columns: auto;
}
}
/* Link */
.postlink,
.taglink {
display: grid;
border: .25rem solid var(--color-primary);
border-radius: 1.25rem;
box-shadow: .35rem .35rem var(--color-shadow);
margin: 2rem 0;
text-decoration: none;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
.postlink:focus-visible,
.taglink:focus-visible {
background-color: var(--color-primary);
outline: none;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
background-color: var(--color-primary);
}
}
/* Forced colors */
@media (forced-colors: active) {
.postlink:focus-visible,
.taglink:focus-visible {
outline-offset: .25rem;
outline: .25rem solid;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
outline-offset: .25rem;
outline: .25rem solid;
}
}
}
/* Click animation */
.postlink:active,
.taglink:active {
box-shadow: none;
top: .2rem;
left: .2rem;
box-shadow: .15rem .15rem var(--color-shadow);
}
/* Post & tag elements */
.post h2, .post img,
.post ul, .post li,
.tag h2, .tag p,
.tag img {
margin: 0;
}
.post h2,
.tag h2 {
grid-area: h2;
padding: .25rem .5rem;
text-transform: uppercase;
font-size: 1.5rem;
color: var(--color-primary);
border-radius: 1rem 1rem 0 0;
border-bottom: .25rem solid var(--color-accent);
}
.post:nth-child(even) h2,
.tag:nth-child(even) h2 {
text-align: right;
}
.postlink:focus-visible h2,
.taglink:focus-visible h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
@media (any-hover: hover) {
.postlink:hover h2,
.taglink:hover h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
}
/* Images */
.post img,
.tag-imgs {
grid-area: img;
}
.tag-imgs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: .15rem;
}
.tag-imgs img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
.missing-image {
width: 100%;
aspect-ratio: 3 / 2;
background-color: var(--color-bg-alt);
border-radius: calc(1rem);
}
.taglink:focus-visible .missing-image {
opacity: .7;
}
@media (any-hover: hover) {
.taglink:hover .missing-image {
opacity: .7;
}
}
/* Post tags */
.postlist-tags {
grid-area: info;
list-style: none;
display: flex;
flex-flow: row wrap;
gap: .5rem;
padding: .5rem;
}
.post:nth-child(odd) .postlist-tags {
justify-content: flex-end;
}
.postlist-tags li,
.tagcount {
background-color: var(--color-primary);
color: var(--color-bg);
padding: 0 .5rem;
border-radius: 1rem;
}
.postlink:focus-visible .postlist-tags li,
.taglink:focus-visible .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
@media (any-hover: hover) {
.postlink:hover .postlist-tags li,
.taglink:hover .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
}
/* Tag count */
.tag p {
grid-area: info;
padding: .5rem;
}
.tag:nth-child(odd) p {
text-align: right;
}

View File

@ -3,10 +3,12 @@
--font-family: 'Atkinson Hyperlegible Next', sans-serif;
--font-family-code: 'Atkinson Hyperlegible Mono', monospace;
--color-dark: #2e303e;
--color-dark-alt: #3c3f52;
--color-light: #ebeeef;
--color-light-alt: #dbe1e3;
--color-teal-dark: #18737b;
--color-teal-light: #25b0bc;
--color-pink-dark: #94195d;
@ -47,8 +49,12 @@
--color-blue: light-dark(var(--color-blue-dark), var(--color-blue-light));
--color-purple: light-dark(var(--color-purple-dark), var(--color-purple-light));
--color-grey: light-dark(var(--color-grey-dark), var(--color-grey-light));
--header-offset: 3.1rem;
}
/* Base */
* {
box-sizing: border-box;
margin: 0;
@ -59,6 +65,205 @@ body {
font-family: var(--font-family);
color: var(--color-text);
background-color: var(--color-bg);
max-width: 60vw;
margin: 0 auto;
}
main {
width: 60vw;
max-width: 1000px;
margin: 0 auto;
scroll-margin-top: var(--header-offset);
}
@media (max-width: 1050px) {
main {
width: 75vw;
}
}
@media (max-width: 650px) {
main {
width: 92vw;
}
}
/* Headers */
h1, h2, h3, h4, h5, h6 {
line-height: 1.25;
color: var(--color-teal);
scroll-margin-top: var(--header-offset);
}
h1 {
margin-top: 3rem;
font-size: 3.5rem;
text-align: center;
}
h2 {
margin-top: 2rem;
font-size: 2.2rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.6rem;
}
h4, h5, h6 {
margin-top: 1rem;
font-size: 1.2rem;
}
/* Images */
img {
display: block;
max-width: 100%;
height: auto;
border-radius: 1rem;
}
/* Paragraphs */
p {
margin: 1.25rem 0;
line-height: 1.4;
}
strong,
b {
font-weight: 900;
}
/* Links */
a {
color: var(--color-text);
border-radius: .25rem;
text-decoration: underline;
text-decoration-style: solid;
text-decoration-thickness: .2em;
text-decoration-color: var(--color-teal);
transition: text-decoration-thickness .5s;
}
a:focus-visible {
text-decoration: none;
outline: .15rem solid var(--color-teal);
}
@media (any-hover: hover) {
a:hover {
text-decoration-thickness: .4em;
}
}
a:active {
text-decoration-thickness: .4em;
}
/* Heading anchors */
a.ha,
span.ha-placeholder {
color: var(--color-pink);
}
span.ha-placeholder {
opacity: .55;
}
/* Lists */
::marker {
color: var(--color-pink);
}
ul, ol, dl, li {
margin-left: 1rem;
}
li {
line-height: 1.2 5;
margin-top: .65rem;
margin-bottom: .65rem;
}
li ul, li ol {
margin: .5rem 0;
}
dt {
font-weight: 900;
margin-top: .5rem;
}
dd {
margin-left: 2rem;
margin-bottom: .75rem;
}
/* Blockquotes */
blockquote {
margin: .5rem 1rem;
padding: 0 1rem;
border-radius: .25rem 1rem 1rem .25rem;
line-height: 1.25;
border-left: .5rem solid var(--color-pink);
}
blockquote,
blockquote p,
blockquote ol,
blockquote ul {
background-color: var(--color-bg-alt);
padding: .5rem;
}
blockquote p {
margin: 0;
}
/* Tables */
table {
width: 100%;
border-spacing: 0; /* border collapse doesn't play nice with radii */
border-radius: .3rem;
border: thin solid var(--color-pink);
}
th {
color: var(--color-bg);
background-color: var(--color-pink);
}
th, td {
padding: .5rem;
text-align: left;
}
tr:nth-child(even) { background-color: var(--color-bg-alt); }
th:not(:first-child) { border-left: thin solid var(--color-bg); }
th:first-child { border-top-left-radius: .25rem; }
th:last-child { border-top-right-radius: .25rem; }
td:not(:first-child) { border-left: thin solid var(--color-pink); }
/* Times */
time {
color: var(--color-grey);
}
/* Horizontal rules */
hr {
color: var(--color-teal);
border: .25rem solid var(--color-teal);
margin: 2rem 0;
}
hr:last-child {
margin-bottom: 0;
}
/* Used on home, reference, gallery pages */
.centered {
text-align: center;
}
/* Currently only used for resume, but it's generalizable */
.upper {
text-transform: uppercase;
}

278
css/nav.css Normal file
View File

@ -0,0 +1,278 @@
/* Header */
header {
position: sticky;
top: 0;
background-color: var(--color-bg);
box-shadow: 0 .25rem .15rem var(--color-shadow);
padding: .75rem 0;
z-index: 10;
}
/* Header links, pagination links */
header a,
.pagination a,
.webring ul a {
border-radius: 1rem;
border: .125rem solid var(--color-pink);
color: var(--color-pink);
text-decoration: none;
padding: 0 .25rem;
box-shadow: .15rem .15rem var(--color-shadow);
font-size: 1.2rem;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
header a,
.pagination .older a,
.webring .prev a,
.webring .rand a {
padding-right: .35rem;
}
.pagination .newer a,
.webring .next a,
.webring .rand a {
padding-left: .35rem;
}
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
outline: none;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
}
}
@media (forced-colors: active) {
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
outline-offset: .125rem;
outline: .125rem solid;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
outline-offset: .125rem;
outline: .125rem solid;
}
}
}
/* Click animation */
header a:active,
.pagination a:active,
.webring ul a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Current page */
header a[aria-current="page"] {
border-color: var(--color-teal);
color: var(--color-teal);
}
header a[aria-current="page"]:focus-visible {
color: var(--color-bg);
border-color: var(--color-teal);
background-color: var(--color-teal);
}
@media (any-hover: hover) {
header a[aria-current="page"]:hover {
color: var(--color-bg);
background-color: var(--color-teal);
border-color: var(--color-teal);
}
}
/* Header link icons, pagination icons */
header i,
.pagination i,
.webring ul i {
color: var(--color-teal);
}
header i,
.pagination .older i,
.webring .prev i,
.webring .rand i:nth-child(1) {
padding-left: .25rem;
}
.pagination .newer i,
.webring .next i,
.webring .rand i:nth-child(2) {
padding-right: .25rem;
}
header a[aria-current="page"] i {
color: var(--color-pink);
}
header a:focus-visible i,
a[aria-current="page"] a:focus-visible i,
.pagination a:focus-visible i,
.webring ul a:focus-visible i {
color: var(--color-bg);
}
@media (any-hover: hover) {
header a:hover i,
header a[aria-current="page"]:hover i,
.pagination a:hover i,
.webring ul a:hover i {
color: var(--color-bg);
}
}
/* Skip link */
#skip {
left: -999px;
position: absolute;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
z-index: -99;
}
#skip:focus-visible {
display: inline-block;
left: auto;
top: auto;
width: auto;
height: auto;
overflow: auto;
margin: 0 10%;
z-index: 999;
}
/* Nav */
header ul {
display: flex;
list-style: none;
gap: 1rem;
justify-content: center;
}
header ul,
header li {
margin: 0;
}
@media (max-width: 650px) {
.menu-text {
display: none; /* Icons only on small screens */
}
header a {
padding: .15rem .5rem;
}
header i {
padding: 0;
}
}
/* Footer */
footer {
padding: 1rem 0;
font-size: .9rem;
}
footer ul {
display: flex;
list-style: none;
gap: .5rem;
justify-content: center;
}
footer li {
margin: 0;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: " ● " / "";
color: var(--color-teal);
}
@media (max-width: 650px) {
footer ul {
flex-flow: column;
text-align: center;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: none;
}
}
footer a {
text-decoration-color: var(--color-pink);
}
footer a:focus-visible {
outline-color: var(--color-pink);
}
/* Pagination */
.pagination {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-areas: "older newer";
list-style: none;
}
.pagination,
.pagination li {
margin: 0;
}
.pagination .older {
grid-area: older;
}
.pagination .newer {
grid-area: newer;
text-align: right;
}
/* webring navigation */
.webring {
margin-bottom: 3rem;
}
.webring ul {
display: flex;
flex-flow: row wrap;
list-style: none;
justify-content: space-around;
gap: .35rem;
}
.webring ul,
.webring li {
margin-left: 0;
}

32
css/palette.css Normal file
View File

@ -0,0 +1,32 @@
.color {
border: .125rem solid var(--color-text);
border-radius: 2rem;
padding: .5rem;
text-align: center;
width: 65%;
margin: 1rem auto;
font-weight: bold;
}
#dark,
#dark-alt,
#teal-dark,
#pink-dark {
color: var(--color-light);
}
#light,
#light-alt,
#teal-light,
#pink-light {
color: var(--color-dark);
}
#dark { background-color: var(--color-dark); }
#dark-alt { background-color: var(--color-dark-alt); }
#light { background-color: var(--color-light); }
#light-alt { background-color: var(--color-light-alt); }
#teal-dark { background-color: var(--color-teal-dark); }
#teal-light { background-color: var(--color-teal-light); }
#pink-dark { background-color: var(--color-pink-dark); }
#pink-light { background-color: var(--color-pink-light); }

75
css/post.css Normal file
View File

@ -0,0 +1,75 @@
.post-metadata {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: baseline;
margin: 1.5rem 0 .5rem;
}
.post-metadata p {
font-size: .9rem;
margin: 0;
}
.post-tags {
display: flex;
flex-flow: row wrap;
gap: .5rem;
list-style: none;
margin: 0;
}
.post-tags li {
margin: 0;
}
.post-tags li a {
text-decoration: none;
color: var(--color-teal);
padding: 0 .5rem;
border-radius: 1rem;
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
line-height: 2;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .1s ease-in, left .1s ease-in;
}
.post-tags li a:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.post-tags li a:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.post-tags li a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}

85
css/print.css Normal file
View File

@ -0,0 +1,85 @@
@media print {
/* Nav elements */
header,
footer,
nav {
display: none !important;
}
/* Base */
body {
background-color: #fff;
color: #000;
}
main {
width: 95vw;
}
h1,h2,h3,h4,h5,h6 {
color: #000;
}
/* Links */
/* Hover is not really necessary, but it's annoying when testing otherwise */
a,
a:hover {
text-decoration-style: dotted;
text-decoration-thickness: .1rem;
text-decoration-color: #000;
}
a::after{
content: " (" attr(href) ")";
}
/* Code */
code,
pre,
code[class*=language-],
pre[class*=language-] {
text-shadow: none;
background-color: var(--color-light);
color: #000 !important;
}
.token.namespace {
opacity: 1;
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation,
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag,
.token.boolean,
.token.function,
.token.number,
.token.class-name,
.token.constant,
.token.property,
.token.symbol,
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted,
.token.entity,
.token.operator,
.token.url,
.token.function-name,
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: #000;
}
}

41
css/resume.css Normal file
View File

@ -0,0 +1,41 @@
.resume h1 {
margin-top: 2rem;
font-size: 2.8rem;
}
.resume h2 {
margin: 1.7rem 0 1rem;
font-size: 1.9rem;
}
.resume h3 {
margin: 0;
font-size: 1.2rem;
}
.two-col {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
.col2 {
text-align: right;
}
.spacer {
margin: 1rem 0;
border-top: solid var(--color-teal);
}
.job-details p {
margin: 0;
}
.job-details a {
font-size: 1rem;
}
.job p {
margin-top: 0;
}

View File

@ -1,11 +1,61 @@
import { feedPlugin } from "@11ty/eleventy-plugin-rss";
import { IdAttributePlugin } from "@11ty/eleventy";
import { eleventyImageTransformPlugin } from "@11ty/eleventy-img";
import eleventyNavigationPlugin from "@11ty/eleventy-navigation";
import { feedPlugin } from "@11ty/eleventy-plugin-rss";
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import { attrs } from "@mdit/plugin-attrs";
import { chunk } from "lodash-es";
import pluginFilters from "./_config/filters.js";
export default async function(eleventyConfig) {
/* Passthroughs */
eleventyConfig.addPassthroughCopy({"css": "assets/css"});
/* Markdown HTML attribute parsing */
eleventyConfig.amendLibrary("md", (mdLib) => mdLib.use(attrs));
/* Bundles */
/* CSS */
eleventyConfig.addBundle("css", {
toFileDirectory: "dist",
bundleHtmlContentFromSelector: "style",
});
/* Javascript */
eleventyConfig.addBundle("js", {
toFileDirectory: "dist",
bundleHtmlContentFromSelector: "script",
});
/* Collections */
/* Tag pagination */
eleventyConfig.addCollection("tagPagination", function(collection) {
let tagSet = new Set(collection.getAllSorted().flatMap((post) => post.data.tags || []));
tagSet = tagSet.difference(new Set(["posts", "gallery", "reference"]));
let paginationSize = 13;
let tagMap = [];
let tagArray = [...tagSet];
for( let tagName of tagArray) {
let tagItems = collection.getFilteredByTag(tagName);
let pagedItems = chunk(tagItems.reverse(), paginationSize); // console.log( tagName, tagItems.length, pagedItems.length );
for( let pageNumber = 0, max = pagedItems.length; pageNumber < max; pageNumber++) {
tagMap.push({
tagName: tagName,
pageNumber: pageNumber,
pageSize: pagedItems.length,
pageData: pagedItems[pageNumber]
});
}
}
return tagMap;
});
/* Plugins */
/* All filters from _config/filters.js */
eleventyConfig.addPlugin(pluginFilters);
/* RSS */
eleventyConfig.addPlugin(feedPlugin, {
type: "atom", // or "rss", "json"
@ -32,7 +82,7 @@ export default async function(eleventyConfig) {
formats: ["auto"],
// output image widths
widths: [800],
widths: [1000],
// optional, attributes assigned on <img> nodes override these values
htmlOptions: {
@ -43,6 +93,20 @@ export default async function(eleventyConfig) {
},
});
/* Navigation */
eleventyConfig.addPlugin(eleventyNavigationPlugin);
/* `id` attributes */
eleventyConfig.addPlugin(IdAttributePlugin);
/* Syntax highlighting */
eleventyConfig.addPlugin(syntaxHighlight);
/* Shortcodes */
eleventyConfig.addShortcode("currentBuildDate", () => {
return (new Date()).toISOString();
});
/* Watch when serving */
eleventyConfig.addWatchTarget("css");
};
@ -51,6 +115,7 @@ export const config = {
dir: {
input: "src",
includes: "../_includes",
layouts: "../_includes/layouts",
data: "../_data"
},
markdownTemplateEngine: "njk",

129
package-lock.json generated
View File

@ -11,7 +11,14 @@
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
"@11ty/eleventy-img": "^6.0.4",
"@11ty/eleventy-plugin-rss": "^2.0.4"
"@11ty/eleventy-navigation": "^1.0.5",
"@11ty/eleventy-plugin-bundle": "^3.0.7",
"@11ty/eleventy-plugin-rss": "^2.0.4",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"@mdit/plugin-attrs": "^0.24.2",
"@zachleat/heading-anchors": "https://github.com/lee0c/heading-anchors/tarball/main",
"lodash-es": "^4.17.23",
"luxon": "^3.7.2"
}
},
"node_modules/@11ty/dependency-tree": {
@ -165,6 +172,20 @@
"url": "https://opencollective.com/11ty"
}
},
"node_modules/@11ty/eleventy-navigation": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-navigation/-/eleventy-navigation-1.0.5.tgz",
"integrity": "sha512-zb6xe29cM9viSdYtZywKIkJw2HIROyBINdBcFWC9uD0c/jYOTAex5nwy3HNEuh5t6/Ld/S9V4gEizfmeYuYpCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"dependency-graph": "^1.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/11ty"
}
},
"node_modules/@11ty/eleventy-plugin-bundle": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz",
@ -201,6 +222,20 @@
"url": "https://opencollective.com/11ty"
}
},
"node_modules/@11ty/eleventy-plugin-syntaxhighlight": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.2.tgz",
"integrity": "sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"prismjs": "^1.30.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/11ty"
}
},
"node_modules/@11ty/eleventy-utils": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz",
@ -652,6 +687,49 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@mdit/helper": {
"version": "0.22.2",
"resolved": "https://registry.npmjs.org/@mdit/helper/-/helper-0.22.2.tgz",
"integrity": "sha512-i0mmN0S/BwR7zAKs9TnT9knmMVq3WGDJ3wO9PiETs0vUAwtcXIq5J0k8GAtGgKKTb7WTQuc19yt8uVQGVYfr2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/markdown-it": "^14.1.2"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"markdown-it": "^14.1.0"
},
"peerDependenciesMeta": {
"markdown-it": {
"optional": true
}
}
},
"node_modules/@mdit/plugin-attrs": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@mdit/plugin-attrs/-/plugin-attrs-0.24.2.tgz",
"integrity": "sha512-2JJDMr8jILBa5c+GRbsyVzRsL7yxuYb60KpUHhR/BjbuM5s9ahPyXPO1Z/C4YXaXLeyHkEddqekXHBCr52rLMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdit/helper": "0.22.2",
"@types/markdown-it": "^14.1.2"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"markdown-it": "^14.1.0"
},
"peerDependenciesMeta": {
"markdown-it": {
"optional": true
}
}
},
"node_modules/@rgrove/parse-xml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.0.tgz",
@ -695,6 +773,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@zachleat/heading-anchors": {
"version": "1.0.5",
"resolved": "https://github.com/lee0c/heading-anchors/tarball/main",
"integrity": "sha512-DQz11XHUNEXzwq8GBedqBVBYGjtdSjWDgcXQrg4bcbS9N3qwJZFhi6Sj4UnzQD7eQOEejCvxTzldS9iRi+rQzg==",
"dev": true,
"license": "MIT"
},
"node_modules/a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
@ -1695,6 +1805,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"dev": true,
"license": "MIT"
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@ -2059,6 +2176,16 @@
"node": ">=12"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View File

@ -4,7 +4,8 @@
"description": "Lee's personal website, take 2, built with 11ty",
"main": "index.js",
"scripts": {
"serve": "rm -rf _site && npx @11ty/eleventy --serve"
"build": "rm -rf _site && npx @11ty/eleventy",
"start": "rm -rf _site && npx @11ty/eleventy --serve --quiet"
},
"keywords": [],
"author": "Lee Cattarin",
@ -13,6 +14,13 @@
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
"@11ty/eleventy-img": "^6.0.4",
"@11ty/eleventy-plugin-rss": "^2.0.4"
"@11ty/eleventy-navigation": "^1.0.5",
"@11ty/eleventy-plugin-bundle": "^3.0.7",
"@11ty/eleventy-plugin-rss": "^2.0.4",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"@mdit/plugin-attrs": "^0.24.2",
"@zachleat/heading-anchors": "https://github.com/lee0c/heading-anchors/tarball/main",
"lodash-es": "^4.17.23",
"luxon": "^3.7.2"
}
}

View File

@ -1,26 +0,0 @@
---
permalink: feed.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ metadata.language or page.lang }}">
<title>{{ metadata.title }}</title>
<subtitle>{{ metadata.description }}</subtitle>
<link href="{{ permalink | htmlBaseUrl(metadata.base) }}" rel="self" />
<link href="{{ metadata.base | addPathPrefixToFullUrl }}" />
<updated>{{ collections.posts | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
<id>{{ metadata.base | addPathPrefixToFullUrl }}</id>
<author>
<name>{{ metadata.author.name }}</name>
</author>
{%- for post in collections.posts | reverse %}
{%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
<entry>
<title>{{ post.data.title }}</title>
<link href="{{ absolutePostUrl }}" />
<updated>{{ post.date | dateToRfc3339 }}</updated>
<id>{{ absolutePostUrl }}</id>
<content type="html">{{ post.content | renderTransforms(post.data.page, metadata.base) }}</content>
</entry>
{%- endfor %}
</feed>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

BIN
src/img/about/duckies.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
src/img/about/kestrel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/img/about/koi-pond.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -1,12 +1,22 @@
---
layout: base.njk
eleventyExcludeFromCollections: true
layout: page-lists.njk
pagination:
data: collections.posts
size: 13
reverse: true
alias: postlist
eleventyNavigation:
key: home
order: 3
icon: fa fa-solid fa-crow
title: home
permalink: index.html
---
<h1>Home</h1>
<p class="centered">(or <a href="/tags/">browse by tags</a>)</p>
{% include "postlist.njk" %}
{# idk why these are backwards either #}
{% set newerHref = pagination.href.previous %}
{% set olderHref = pagination.href.next %}
{% include "pagination.njk" %}

9
src/meta/404.md Normal file
View File

@ -0,0 +1,9 @@
---
layout: page.njk
permalink: 404.html
eleventyExcludeFromCollections: true
---
# Sorry, can't find that
Head [home](/).

25
src/meta/gallery.njk Normal file
View File

@ -0,0 +1,25 @@
---
layout: page-lists.njk
permalink: gallery/index.html
pagination:
data: collections["gallery"]
size: 13
reverse: true
alias: postlist
eleventyNavigation:
key: gallery
order: 2
title: gallery
icon: fa-regular fa-images
label: view the gallery
---
<p class="centered">the gallery page is for finished art</p>
<p class="centered">(or <a href="/tags/">browse by tags</a>)</p>
{% include "postlist.njk" %}
{# idk why these are backwards either #}
{% set newerHref = pagination.href.previous %}
{% set olderHref = pagination.href.next %}
{% include "pagination.njk" %}

25
src/meta/reference.njk Normal file
View File

@ -0,0 +1,25 @@
---
layout: page-lists.njk
permalink: reference/index.html
pagination:
data: collections["reference"]
size: 13
reverse: true
alias: postlist
eleventyNavigation:
key: reference
order: 1
title: reference
icon: fa-regular fa-folder-open
label: read reference posts
---
<p class="centered">the reference page is for informational posts</p>
<p class="centered">(or <a href="/tags/">browse by tags</a>)</p>
{% include "postlist.njk" %}
{# idk why these are backwards either #}
{% set newerHref = pagination.href.previous %}
{% set olderHref = pagination.href.next %}
{% include "pagination.njk" %}

16
src/meta/sitemap.xml.njk Normal file
View File

@ -0,0 +1,16 @@
---
permalink: sitemap.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
{% for page in collections.all %}
{% if page.data.permalink != false %}
{% set absoluteUrl %}{{ page.url | htmlBaseUrl(metadata.url) }}{% endset %}
<url>
<loc>{{ absoluteUrl }}</loc>
<lastmod>{{ page.date | htmlDateString }}</lastmod>
</url>
{% endif %}
{% endfor %}
</urlset>

22
src/meta/tag-pages.njk Normal file
View File

@ -0,0 +1,22 @@
---
layout: page-lists.njk
pagination:
data: collections.tagPagination
size: 1
alias: tag
eleventyComputed:
permalink: /tags/{{ tag.tagName | slugify }}/{% if tag.pageNumber %}{{ tag.pageNumber + 1 }}/{% endif %}
title: "tag: {{ tag.tagName }}"
---
{% set postlist = tag.pageData %}
{% include "postlist.njk" %}
{# idk why these are backwards either #}
{% if tag.pageNumber > 0 %}
{% set newerHref = pagination.href.previous %}
{% endif %}
{% if tag.pageNumber < tag.pageSize - 1 %}
{% set olderHref = pagination.href.next %}
{% endif %}
{% include "pagination.njk" %}

27
src/meta/tags.njk Normal file
View File

@ -0,0 +1,27 @@
---
layout: page-lists.njk
permalink: tags/index.html
title: all tags
---
<ul id="taglist">
{% for tag in collections | getKeys | removeBasicTags | sortAlphabetically %}
{% set tagUrl %}/tags/{{ tag | slugify }}/{% endset %}
<li class="tag">
<a href="{{ tagUrl }}" class="taglink">
<div class="tag-imgs">
{% for i in range(0, 4) %}
{% if collections[tag][i].data.image %}
<img src="/img/{{ collections[tag][i].data.image.src }}" alt="{{ collections[tag][i].data.image.alt }}">
{% else %}
<div class="missing-image"></div>
{% endif %}
{% endfor %}
</div>
<h2>{{ tag }}</h2>
{% set numPosts = collections[tag].length %}
<p><span class="tagcount">({{ numPosts }} post{% if numPosts > 1 %}s{% endif %})</span></p>
</a>
</li>
{% endfor %}
</ul>

52
src/pages/about.njk Normal file
View File

@ -0,0 +1,52 @@
---
eleventyNavigation:
key: about
order: 4
title: about
icon: fa-regular fa-user
label: about Lee
---
<p>My name is Lee Cattarin. I use he or ze pronouns.</p>
<p>I'm a programmer (<a href="/resume/">are you looking for a resume?</a>), artist&crafter - knitting, spinning yarn, stamp carving/printmaking, cardmaking & papercrafts, bookbinding, leatherworking - bread baker, guitar player, and probably a lot of other things.</p>
<img src="/img/about/koi-pond.jpg" alt="A koi pond in fall afternoon light. A slender white person in a knitted dark teal sweater crouches in front of the pond and extends hir hand towards the surface of the water. Several curious koi are arriving to see what the matter is, and one white koi has stuck its face a bit out of the water to reach toward hir hand.">
<p>As of November 2023, my wife Brooke Osment and I have a art store: Riverside Refuge Studio. You can find various store links, or other ways to connect, on my <a href="/contact">contact page</a>. We're based out of Vashon, WA, USA, and ship internationally.</p>
<h2>pets</h2>
<p>We have a dog...</p>
<img src="/img/about/kestrel.png" alt="4 collaged pictures of Kestrel the Malinois mutt, a big tan dog with a thick ruff and half floppy, half pointy ears. In the pictures, he rolls on his back in the grass, looks snobbily at the camera with one ear flopping, looks off to one side, and sleeps in a big leather chair.">
<p>...and six ducks.</p>
<img src="/img/about/duckies.png" alt="2 pictures that show all six ducks, each duck labeled with their name. In the first image, Sparrow (Golden 300, a light brown breed that looks similar to female mallards) and Puffin (Swedish Black, black with a white bib and beautiful iridescence) come up to eat from my hand. In the second image, Chickadee (Magpie, black and white splotched) is up front looking to one side, and behind her are Dodo (Silver Runner, white and grey), Emu (Fawn & White Runner, white and pale tan), and Kiwi (Chocolate Runner, dark brown all over), with Sparrow and Puffin partially visible behind.">
<h2>webrings</h2>
{% set id = "css-joy-webring" %}
{% set name = "CSS Joy webring" %}
{% set url = "https://cs.sjoy.lol/" %}
{% set prev = "https://webri.ng/webring/cssjoy/previous?via=https://leecat.art" %}
{% set next = "https://webri.ng/webring/cssjoy/next?via=https://leecat.art" %}
{% set rand = "https://webri.ng/webring/cssjoy/random?via=https://leecat.art" %}
{% include "webring.njk" %}
{% set id = "a11y-webring-club" %}
{% set name = "a11y-webring.club" %}
{% set url = "https://a11y-webring.club/" %}
{% set prev = "https://a11y-webring.club/prev" %}
{% set next = "https://a11y-webring.club/next" %}
{% set rand = "https://a11y-webring.club/random" %}
{% include "webring.njk" %}
{% set id = "no-ai-webring" %}
{% set name = "No AI webring" %}
{% set url = "https://baccyflap.com/noai/" %}
{% set prev = "https://baccyflap.com/noai/?prv&s=lee" %}
{% set next = "https://baccyflap.com/noai/?nxt&s=lee" %}
{% set rand = "https://baccyflap.com/noai/?rnd" %}
{% include "webring.njk" %}

18
src/pages/colophon.md Normal file
View File

@ -0,0 +1,18 @@
---
title: colophon
---
This is v2 of my personal website, build with [{{ eleventy.generator }}](https://www.11ty.dev/){target="_blank" rel="external"}. It's been hand-coded from the ground up.
v1 of this site began in 2022 and was based on [Millennial](https://lenpaul.github.io/Millennial/){target="_blank" rel="external"}, a minimalist Jekyll theme for running a blog or publication by Paul Le.
The fonts are [Atkinson Hyperlegible Next and Atkinson Hyperlegible Mono](https://brailleinstitute.org/freefont){target="_blank" rel="external"} for standard text and monospace respectively, specifically designed for low-vision readers to improve character recognition. Also they look neat :)
Thank you to some lovely friends for their feedback and help with the site! You should hire them. Yes, you.
- [Lenny](http://www.wondra.codes/){target="_blank" rel="external"}, especially for their HTML/CSS and accessibility expertise
- [Shir](https://shirgoldberg.com/){target="_blank" rel="external"}
You can [find the accessibility statement here](/accessibility). You can also [explore the sitemap](/sitemap.xml). If you'd like, you can view the [site's palette](/palette) or the [style overview](/style).
This site is created [without the use of generative AI](https://declare-ai.org/1.0.0/none.html){target="_blank" rel="external"}.

119
src/pages/contact.md Normal file
View File

@ -0,0 +1,119 @@
---
eleventyNavigation:
key: contact
order: 5
title: contact
icon: fa-solid fa-envelope-open-text
label: contact Lee
---
## contact me
<dl>
<dt><i aria-hidden="true" class="fa-brands fa-signal-messenger"></i> signal</dt>
<dd><a href="https://signal.me/#eu/PcJ86iNQeAbC-SFbxqI00gFQb6zL4Y6QDCsFDU2mokM4K40djsdNYa3hJAee79ll">inherentlee.13</a></dd>
<dt><i aria-hidden="true" class="fa-solid fa-envelope"></i> email</dt>
<dd><a href="mailto:lee.cattarin@gmail.com" target="_blank">lee dot cattarin at gmail dot com</a></dd>
<dt><i aria-hidden="true" class="fa-solid fa-mobile-retro"></i> text (no phone calls!)</dt>
<dd>seven seven four, two four nine, zero five eight six</dd>
<dt><i aria-hidden="true" class="fa-brands fa-discord"></i> discord</dt>
<dd>inherentlee</dd>
<dt><i aria-hidden="true" class="fa-solid fa-envelopes-bulk"></i> snail mail</dt>
<dd>message me for address!</dd>
<dt><i aria-hidden="true" class="fa-solid fa-signature"></i> guestbook?</dt>
<dd><a href="/guestbook">guestbook.</a></dd>
</dl>
## socials
<dl>
<dt><i aria-hidden="true" class="fa-brands fa-mastodon"></i> the fediverse/mastodon</dt>
<dd><a rel="me" href="https://flipping.rocks/@inherentlee" target="_blank">@inherentlee@flipping.rocks</a> and <a rel="me" href="https://weirder.earth/@inherentlee" target="_blank">@inherentlee@weirder.earth</a> (weirder.earth largely unused)</dd>
<dt><i aria-hidden="true" class="fa-solid fa-rss"></i> rss</dt>
<dd><a href="/feed.xml">feed</a></dd>
<dt><i aria-hidden="true" class="fa-solid fa-icicles"></i> codeberg</dt>
<dd><a href="https://codeberg.org/inherentlee" target="_blank">inherentlee</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-github"></i> github</dt>
<dd><a href="https://github.com/lee0c" target="_blank">lee0c</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-ravelry"></i> ravelry</dt>
<dd><a href="https://ravelry.com/people/inherentlee" target="_blank">inherentlee</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-twitch"></i> twitch</dt>
<dd><a href="https://twitch.tv/inherentlee" target="_blank">inherentlee</a></dd>
</dl>
---
## shops
if pricing is an issue for you, reach out and we can work out sliding scale options - or an art trade!
<dl>
<dt><i aria-hidden="true" class="fa-regular fa-square"></i> square</dt>
<dd><a href="https://riverside-refuge.square.site/" target="_blank">Riverside Refuge Studio</a></dd>
<dt><i aria-hidden="true" class="fa-solid fa-store"></i> faire (wholesale)</dt>
<dd><a href="https://faire.com/direct/riversiderefugestudio" target="_blank">Riverside Refuge Studio</a></dd>
<dt><i aria-hidden="true" class="fa-solid fa-mug-saucer"></i> kofi (now the home of <a href="https://fedizinefest.fyi">FediZineFest 2025</a> and <a href="https://rescue-trans-rescue.quest" target="_blank">Rescue Trans Rescue</a>)</dt>
<dd><a href="https://ko-fi.com/inherentlee" target="_blank">inherentlee</a></dd>
</dl>
### payment methods
<dl>
<dt><i aria-hidden="true" class="fa-solid fa-comment-dollar"></i> venmo</dt>
<dd><a href="https://www.venmo.com/u/lee-cattarin" target="_blank">lee-cattarin</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-paypal"></i> paypal</dt>
<dd><a href="https://paypal.me/leecattarin?country.x=US&locale.x=en_US" target="_blank">leecattarin</a></dd>
</dl>
---
## in physical stores!
### Colorado
#### Denver
- [(dis)obedience](https://www.disobediencedenver.com){:target="_blank"}: stickers, pins, cards
### Oregon
#### Beaverton
- [Forager Vintage](https://foragervintage.com/){:target="_blank"}: stickers, pins, cards
### Washington
#### Port Orchard
- [Aphrodisia Boutique](https://www.aphrodisia.boutique/){:target="_blank"}: stickers, pins, cards, prints, leather goods (impact toys, cuffs, potentially harnesses)
#### Seattle
- [Push/Pull](https://www.pushpullseattle.com/){:target="_blank"}: stickers, pins, cards
- [Standard Goods Ballard](https://thestandardgoods.com/pages/ballard){:target="_blank"}: stickers
- [Standard Goods Capitol Hill](https://thestandardgoods.com/pages/capitol-hill){:target="_blank"}: stickers
#### Tacoma
- Anna's Anomalies (Sanford and Sons Antiques, 743 Broadway, Tacoma, WA, 98402): stickers, pins, cards, prints
#### Vashon
- [Vashon Made (formerly Starving Artist Works) (facebook)](https://www.facebook.com/SawStarvingArtistWorks/){:target="_blank"}: stickers, cards, prints, wallets, handbound books, yarn
- [Standard Goods Vashon](https://thestandardgoods.com/pages/vashon){:target="_blank"}: stickers
- [Vashon Pharmacy](https://vashonpharmacy.com/){:target="_blank"}: cards
---
## not online or online unreliably
<dl>
<dt><i aria-hidden="true" class="fa-brands fa-linkedin"></i> linkedin</dt>
<dd><a href="https://www.linkedin.com/in/lee-cattarin/" target="_blank">Lee Cattarin</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-instagram"></i> instagram</dt>
<dd><a href="https://instagram.com/inherentlee" target="_blank">inherentlee</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-facebook"></i> facebook</dt>
<dd><a href="https://www.facebook.com/lee.cattarin.50/" target="_blank">Lee Cattarin</a></dd>
<dt><i aria-hidden="true" class="fa-brands fa-twitter"></i> twitter</dt>
<dd><a href="https://twitter.com/inherentlee" target="_blank">inherentlee</a></dd>
</dl>

67
src/pages/guestbook.md Normal file
View File

@ -0,0 +1,67 @@
---
title: guestbook
---
guestbook entries are manually added and moderated :)
yes, you may sign again even if you've been here before - just no spamming please!
## write
if the embed below is not working for you, you can [open the guestbook form in a new tab](https://airtable.com/app1YuM4uE4sRTYgh/pag4JzOylrLoLy4AS/form){:target="_blank"} or just [contact me any way you want](/contact) with your desired message
<iframe class="airtable-embed" src="https://airtable.com/embed/app1YuM4uE4sRTYgh/pag4JzOylrLoLy4AS/form" frameborder="0" onmousewheel="" width="100%" height="533" style="background: transparent;"></iframe>
## read
J'myle Koretz on <time datetime="2025-05-21">21 May 2025</time>:
> It was delightful to meet you at the Vashon art walk a couple weekends back. Kestrel was awesome, and your [Brooke's] jackets were so cool!
Jack on <time datetime="2024-12-01">1 December 2024</time>:
> All the things you make and do are so cool and I feel so lucky every time I get to see them (including the ones that now live in my home!)
hello on <time datetime="2024-12-01">1 December 2024</time>:
> thanks for being gay and weird pal
Fén (Spirits) on <time datetime="2024-09-06">6 September 2024</time>:
> Hi you make really good art and crafts and have been awesome to get to know ^^ Keep on keeping on~
Olive on <time datetime="2024-09-06">6 September 2024</time>:
> Hi! You're cool :))
Fern (Rainbow) on <time datetime="2024-09-06">6 September 2024</time>:
> I think you're awesome and this is really impressive 🥺
handmade ghost on <time datetime="2024-09-06">6 September 2024</time>:
> Everything Lee makes is a gift to the world--just knowing ze made something new brings me joy and sends me scrambling to this site!
[jay](jaygrant.us){:target="_blank"} on <time datetime="2024-09-06">6 September 2024</time>:
> EGG
pqqq on <time datetime="2024-09-06">6 September 2024</time>:
> Hello from Fedi! Your site is absolutely wonderful, and I love that you're adding a guestbook, too!!
Morgan on <time datetime="2024-09-06">6 September 2024</time>:
> You make good art and good takes on gender 💜
[Lisa](@mycrowgirl@mastodon.social){:target="_blank"} on <time datetime="2024-09-06">6 September 2024</time>:
> Ask not for whom the snoot is booped. It is booped for weevils.
nathanlovestrees on <time datetime="2024-09-06">6 September 2024</time>:
> honk!
[✨pencilears✨](https://pencilears.eternalaugust.net/comic/){:target="_blank"} on <time datetime="2024-09-06">6 September 2024</time>:
> Hello! I'm glad we're still doing guest books on websites.
[brhfl](https://brhfl.com){:target="_blank"} on <time datetime="2024-09-06">6 September 2024</time>:
> hey hi! just giving you some test data and also basking in the nostalgia of a guestbook :)
Eedlipherus C. Bigribs on <time datetime="2024-09-06">6 September 2024</time>:
> Signing the guestbook so you have data to format on the page lyao.
>
> And this is a second paragraph, because, honestly, this is more interesting than the QA I get paid to do, so I'll just make this bit long enough to wrap for a few lines, yep, maybe a little bit more, uh huh, that's right, eat your heart out, Lorem Ipsum.
jade on <time datetime="2024-09-06">6 September 2024</time>:
> Hi Lee!
alex tax1a on <time datetime="2024-09-06">6 September 2024</time>:
> Hi!! signing the guestbook, so you have at least one datum.

View File

@ -0,0 +1,6 @@
export default {
permalink: function ({ title }) {
return `/${this.slugify(title)}/index.html`;
},
layout: "page.njk"
};

13
src/pages/palette.njk Normal file
View File

@ -0,0 +1,13 @@
---
title: palette
---
{% css %}{% include "css/palette.css" %}{% endcss %}
<p class="color" id="dark">#2e303e</p>
<p class="color" id="dark-alt">#3c3f52</p>
<p class="color" id="light">#ebeeef</p>
<p class="color" id="light-alt">#dbe1e3</p>
<p class="color" id="teal-dark">#18737b</p>
<p class="color" id="teal-light">#25b0bc</p>
<p class="color" id="pink-dark">#94195d</p>
<p class="color" id="pink-light">#ee9fcb</p>

201
src/pages/resume.html Normal file
View File

@ -0,0 +1,201 @@
---
title: resume
layout: resume.njk
---
<div class="resume">
<h1 class="centered upper">Lee Cattarin</h1>
<p class="centered">
he/him or ze/hir • Vashon, WA 98070
<br/>
<a href="mailto:lee.cattarin@gmail.com?subject=Resume%20inquiry">lee.cattarin@gmail.com</a><a href="/">this very website</a><a href="https://linkedin.com/in/lee-cattarin" target="_blank">linkedin.com/in/lee-cattarin</a>
</p>
<h2 class="centered upper">Platforms Engineer</h2>
<p>
Design-oriented platforms/infrastructure engineer with a well-rounded background in algorithms, UI/UX, observabilty, databases, and accessibility. Consistent customer focus with strong communication skills and a passion for sharing knowledge.
</p>
<hr>
<h2 class="centered upper" id="toolkit">Toolkit</h2>
<ul>
<li>Git, bash, and the terminal</li>
<li>Containers and Kubernetes</li>
<li>Terraform, Bicep, and infrastructure deployment</li>
<li>GitHub Actions and other pipelines</li>
<li>HTML/CSS, Javascript, and UI/UX design</li>
<li>Languages like Go, C, and Python</li>
<li>Accessiblity knowledge and thoughtful design</li>
</ul>
<hr>
<h2 class="centered upper" id="experience">Experience</h2>
<div class="job">
<div class="two-col job-details">
<p><b>Microsoft,</b> Redmond, WA</p>
<p class="col2"><time datetime="2018-08">August 2018</time> <time datetime="2025-07">July 2025</time></p>
</div>
<h3>Software Development Engineer I & II</h3>
<p>
Worked on green- and brown-field projects with customers, solving emerging problems in infrastructure, devOps, and LLM/human interaction. Wrote reports on product feedback to relay to product teams and improved documentation.
</p>
<ul>
<li>Contributed UI/UX, algorithm, and database design to a new human-in-the-loop approach to LLM form automation, ensuring both that content was effectively reviewed and that auditing was smooth.</li>
<li>Deployed and configured Kubernetes for many scenarios, handling monitoring and observability, scaling, traffic routing, security and policy, Windows container support, and more.</li>
<li>Maintained team working agreements to ensure team unity and consistency. Shaped team processes and documentation to improve clarity and speed up onboarding.</li>
<li>Improved Azure and open source documentation with new, updated, and corrected information.</li>
<li>Coached multiple learning-oriented hackathons to ensure broader community understanding and adoption of Azure services.</li>
<li>Presented to groups of 10100 on backend accessibility and queer/trans education.</li>
</ul>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<p><b>Riverside Refuge Studio,</b> Vashon, WA</p>
<p class="col2"><time datetime="2023-10">October 2023</time> Present</p>
</div>
<h3>Co-owner and artist</h3>
<p>
Maintains website, storefront, and inventory for a diverse set of artistic goods. Communicates with a range of customers, both digitally and in-person.
</p>
<ul>
<li>Designs and creates art/crafts in a variety of mediums ranging from visual to functional.</li>
<li>Presents work in art shows in and around Seattle.</li>
<li>Runs booths or studio space at art fairs and markets.</li>
</ul>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<p><b>Rensselaer Polytechnic Institute,</b> Troy, NY</p>
<p class="col2"><time datetime="2015-09">September 2015</time> <time datetime="2017-12">December 2017</time></p>
</div>
<h3>Undergraduate Programming Mentor</h3>
<p>
Helped students debug and develop in Python and learn core programming concepts. Created daily quiz material and graded exams.
</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<p><b>Microsoft,</b> Redmond, WA</p>
<p class="col2"><time datetime="2017-06">June 2017</time> <time datetime="2017-08">August 2017</time></p>
</div>
<h3>Software Development Engineering Intern</h3>
<p>
Developed a chatbot add-on for the Azure Android application.
</p>
</div>
<hr>
<h2 class="centered upper" id="projects">Projects</h2>
<div class="job">
<div class="two-col job-details">
<h3>11ty Lessons (<a href="https://inherentlee.codeberg.page/lessons" target="_blank">inherentlee.codeberg.page/lessons</a>)</h3>
<p class="col2"><time datetime="2026-02">February 2026</time> Present</p>
</div>
<p>Designs, develops, and maintains Eleventy-based website that details lessons learned while building with Eleventy.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>Spoonfairies (<a href="https://inherentlee.codeberg.page/spoonfairies" target="_blank">inherentlee.codeberg.page/spoonfairies</a>)</h3>
<p class="col2"><time datetime="2026-02">February 2026</time> Present</p>
</div>
<p>Designs, develops, and maintains Eleventy-based website for a just-launching project to build community and help people find support.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>Siblinghood of the Traveling Greeting Card (<a href="https://siblinghood.quest" target="_blank">siblinghood.quest</a>)</h3>
<p class="col2"><time datetime="2025-11">November 2025</time> Present</p>
</div>
<p>Designs, develops, and maintains website. Manages communication and logistics for a ~40 person global community building project.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>Beall Greenhouses (<a href="https://beall-greenhouses-market.pages.dev" target="_blank">beall-greenhouses-market.pages.dev</a>)</h3>
<p class="col2"><time datetime="2025-10">October 2025</time> Present</p>
</div>
<p>Created logo. Designs, develops, and maintains website. Manages artist information for the Beall Greenhouses artist studios.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>leecat.art (you are here!) (<a href="/">leecat.art</a>)</h3>
<p class="col2"><time datetime="2022-10">October 2022</time> Present</p>
</div>
<p>Designs, maintains, and creates all content.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>FediZineFest (<a href="https://fedizinefest.fyi" target="_blank">fedizinefest.fyi</a>)</h3>
<p class="col2"><time datetime="2023-12">December 2023</time> <time datetime="2025-07">July 2025</time></p>
</div>
<p>Created and ran a global event for zine artists on the fediverse (Mastodon) for two years. Coordinated website, physical material shipping and logistics, payment, and marketing for a 4050 person project.</p>
</div>
<div class="spacer"></div>
<div class="job">
<div class="two-col job-details">
<h3>Rescue Trans Rescue (<a href="https://rescue-trans-rescue.quest" target="_blank">rescue-trans-rescue.quest</a>)</h3>
<p class="col2"><time datetime="2024">2024</time></p>
</div>
<p>Created a digital art exhibit and sale to raise money for charity. Coordinated ~30 artists to contribute physical and digital work as well as a collaborative sticker sheet. Raised ~2.3k for Trans Rescue.</p>
</div>
<hr>
<h2 class="centered upper">Education</h2>
<p class="centered">
<b>Bachelor of Science — Computer Science</b>
<br/>
Rensselaer Polytechnic Institute, Troy, NY
</p>
</div>

131
src/pages/style.md Normal file
View File

@ -0,0 +1,131 @@
---
title: style
---
Adaped from an introduction to Markdown in order to test and display styling of basic components of the site.
## Heading level 2
Since your title (defined in the front matter) is your heading level 1, you should never use another heading level 1 in your body.
### Heading level 3
The number of pound signs determines the heading level.
#### Heading level 4
It's also important not to skip heading levels. Don't jump from a 2 to a 4 or similar.
##### Heading level 5
You can use up to level 6!
###### Here's level 6
It's just unnecessary.
## Paragraphs
You'll notice that I am putting blank lines between headings and plain text. This is necessary, or they won't render correctly.
It's also important to put a blank line in between each paragraph. See what happens without it:
This is supposed to be a new paragraph, but it isn't.
### Inline styles
We can, of course, create **bold** and *italicized* text, or `inline monospace text`.
We can also create links, like this [link to the home page](/).
## Horizontal lines
Sometimes you want to insert a visual break in your text that isn't just a new paragraph. You can use three dashes to create a horizontal line:
---
This text will be below the line.
## Lists
### Unordered lists
Unordered lists can be created with dashes or asterisks. With dashes:
- this is an item
- this is another item
With asterisks:
* this is an item
* this is another item
### Ordered lists
Ordered (numbered) lists can be created with (surprise!) numbers. You can write numbers as you would normally, *or* you can just write the number 1 over and over, like so:
1. this is item 1
1. despite being written with a 1, this is item 2
This allows you to insert more information into lists in the future without having to renumber every following item.
### Nested lists
Both unordered and ordered lists can be nested. Just tab the nested section inwards:
- this is an item
- this is nested below it
- this is also nested
- this is another item
You can mix unordered and ordered lists when you nest.
## Quotes
You can always just use quotation marks, of course, but if you are quoting a larger chunk of text it can be nice to use a blockquote.
You format a blockquote by starting the line with a caret:
> This is a quote, and it will render differently than a paragraph.
If you want a quote to have multiple separate paragraphs, and still contiguously display as one quote, make sure to put a caret on the empty line between the paragraphs.
> This is a multi-paragraph quote.
>
> Here's the second paragraph.
>
> - Blockquotes can also have lists
> - They still have the caret at the front
## Monospace
You can write single words `in monospace`, or create code blocks:
```
3 backticks surround code blocks
```
Code blocks can have syntax highlighting:
```html
<h1>Hello, world</h1>
```
## Tables
Tables in Markdown are kind of annoying to format. You use the pipe (`|`) character as well as dashes.
```
| Header 1 | Header 2 |
|---|---|
| data 1a | data 1b |
| data 2a | data 2b |
| data 3a | data 3b |
```
When I remove the monospace block, you can see how this formats:
| Header 1 | Header 2 |
|---|---|
| data 1a | data 1b |
| data 2a | data 2b |
| data 3a | data 3b |

View File

@ -0,0 +1,140 @@
---
title: moving images
image:
src: 2026/cormorant.jpg
alt: "Image unrelated to post. A cormorant, a type of black waterfowl, poses with wings spread on a buoy in Puget Sound. Off to the left, another bird floats."
tags:
- reference
- software
- some
- more
- tags
---
## problem statement
today I decided to finally clean up the `assets/img` directory for this site. Since 2022, when I started this project, I've just been adding images directly to that directory with no further segmentation - messy of me, I know! It's gotten unwieldy and I'm starting to get worried about generic names leading to duplicates at some point, particularly for the non-gallery images where I have a tendency to [use](/stationery-exchange) [lots](/favorite-git-flag) [of](/trans-networks) [mushroom](/no-politics) [images](/domain-and-site-setup).
so it's time to move them into year-based folders. Let's talk about how I did that. `bash` away!
(want to [skip right to the completed script?](#result))
## find
let's start with the basics: a list of posts. `find` gets us everything under a specific directory - in this case, the `_posts` directory. We can filter out the directories a few different ways, but I piped the `find` output through a basic `grep` looking for `.md` in the filename.
```sh
for FILE in $(find _posts | grep .md)
do
# TBD
done
```
## grep
`grep` can also help us get image names with the regex `"name:.+jpg|png"`. I add `name:` to the regex because there are *very occasionally* images that aren't the featured image for the post, and those don't fit the pattern of `name: <img>`. Since there's so few of those, I ended up handling them manually.
to make `grep` work with regex, it needs the `-E` flag.
```sh
# gives us
# name: <img>
# note the 4 spaces at the beginning of the line
IMAGE_LINE=$(cat $FILE | grep -E "name:.+jpg|png$")
```
## cut
that output gets us the full line of text that includes the image filename. Let's trim out what we actually want.
below, `-d` sets a delimiter, and `-f` chooses what field we want to return. Because there's 4 spaces before `name`, our field index is actually pretty high - `cut` is creating 4 empty strings.
```sh
IMAGE=$(echo $IMAGE_LINE | cut -d ' ' -f 6 -)
```
or, for brevity:
```sh
IMAGE=$(cat $FILE | grep -E "name:.+jpg|png$" | cut -d ' ' -f 6 -)
```
with `cut`, we can also get the year of the post:
```sh
YEAR=$(echo $FILE | cut -d '/' -f 2 -)
```
## sed
there's two major things we need to do with the information we've gathered:
1. replace the image filename in-place in the post's markdown file
1. move the image file from its original location into a new directory
we can do replacement with `sed`, where our pattern should be something like this: `s/$IMAGE/$YEAR\/&\` (the `&` subs in the found string - in this case `$IMAGE`). We could also use comma separators if we don't want to escape the slash, like `s,$IMAGE,$YEAR/&,` - I did this for ease of reading.
by default, `sed` prints to standard output, so we'll tell it to edit in-place instead with `-i`. Here's our full `sed` command:
```sh
sed "s,$IMAGE,$YEAR/&," -i $FILE
```
## mving and shaking
(my mom thinks I'm funny.)
now we'll handle moving the image file from its original location into a new directory. let's create our image paths, source and destination:
```sh
IMG_DIR=assets/img
NEW_IMAGE=$IMG_DIR/$YEAR/$IMAGE
IMAGE=$IMG_DIR/$IMAGE
```
trying to `mv` the images will immediately cause problems, because the year directories don't exist yet. A simple check gets us past that:
```sh
if [ ! -d $IMG_DIR/$YEAR ]
then
mkdir $IMG_DIR/$YEAR
fi
```
finally, we can `mv` the image:
```sh
mv $IMAGE $NEW_IMAGE
```
## result
here's our final script:
```sh
for FILE in $(find _posts | grep .md)
do
# parse image and year info
IMAGE=$(cat $FILE | grep -E "name:.+jpg|png$" | cut -d ' ' -f 6 -)
YEAR=$(echo $FILE | cut -d '/' -f 2 -)
# replace in-place in file
sed "s,$IMAGE,$YEAR/&," -i $FILE
# path creation
IMG_DIR=assets/img
NEW_IMAGE=$IMG_DIR/$YEAR/$IMAGE
IMAGE=$IMG_DIR/$IMAGE
# create dir for year if it doesn't exist
if [ ! -d $IMG_DIR/$YEAR ]
then
mkdir $IMG_DIR/$YEAR
fi
# move image
mv $IMAGE $NEW_IMAGE
done
```
questions? errors? [ping me!](/contact)

View File

@ -1,11 +0,0 @@
---
title: Sample
date: 2026-02-17
tags:
- test
image:
src: 2026/sample-0.jpg
alt: filler
---

View File

@ -5,5 +5,5 @@ export default {
tags: [
"posts"
],
layout: "base.njk"
layout: "post.njk"
};