Redesign site and optimize page performance
All checks were successful
Deploy Website / build-and-deploy (push) Successful in 23s

This commit is contained in:
Akshay Kolli
2026-07-18 19:57:44 -04:00
parent 2f2794f5d5
commit 9dda076b56
53 changed files with 747 additions and 277 deletions

View File

@@ -4,4 +4,5 @@
.hugo_build.lock .hugo_build.lock
.DS_Store .DS_Store
public public
dist
resources/_gen resources/_gen

View File

@@ -29,3 +29,17 @@ jobs:
--restart unless-stopped \ --restart unless-stopped \
-p 8083:80 \ -p 8083:80 \
akkolli-website:latest akkolli-website:latest
- name: Purge Cloudflare cache
env:
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
if [ -n "$CLOUDFLARE_ZONE_ID" ] && [ -n "$CLOUDFLARE_API_TOKEN" ]; then
curl --fail --silent --show-error \
--request POST \
--url "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{"purge_everything":true}'
fi

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.hugo_build.lock .hugo_build.lock
resources/_gen/ resources/_gen/
public/ public/
dist/
.DS_Store .DS_Store

View File

@@ -7,4 +7,5 @@ RUN hugo --minify --cleanDestinationDir
FROM nginx:1.27-alpine FROM nginx:1.27-alpine
COPY --from=build /src/public /usr/share/nginx/html COPY --from=build /src/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80

View File

@@ -3,7 +3,7 @@
src: src:
url("/fonts/VT323-Regular.woff") format("woff"), url("/fonts/VT323-Regular.woff") format("woff"),
url("/fonts/VT323-Regular.ttf") format("truetype"); url("/fonts/VT323-Regular.ttf") format("truetype");
font-display: block; font-display: swap;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
unicode-range: U+0020-007E, U+00A0, U+2013-2014, U+2018-2019, U+201C-201D, U+2026; unicode-range: U+0020-007E, U+00A0, U+2013-2014, U+2018-2019, U+201C-201D, U+2026;
@@ -29,7 +29,7 @@
--margin-gap: 1.5rem; --margin-gap: 1.5rem;
--panel-padding: 0.85rem; --panel-padding: 0.85rem;
--section-gap: 1.1rem; --section-gap: 1.1rem;
--bottom-scroll-room: 38vh; --bottom-scroll-room: 15vh;
--row-padding: 0.2rem; --row-padding: 0.2rem;
--rule: 2px; --rule: 2px;
--control-size: 0.66rem; --control-size: 0.66rem;
@@ -42,6 +42,7 @@
--vignette-alpha: 0.07; --vignette-alpha: 0.07;
--body-font: Monaco, Menlo, "SF Mono", ui-monospace, Consolas, "Courier New", monospace; --body-font: Monaco, Menlo, "SF Mono", ui-monospace, Consolas, "Courier New", monospace;
--reading-font: var(--body-font);
--display-font: "VT323 Local", "Print Char 21", "Web437 Apple II", "Apple II Screen Typeface", var(--body-font); --display-font: "VT323 Local", "Print Char 21", "Web437 Apple II", "Apple II Screen Typeface", var(--body-font);
--signature-font: "Snell Roundhand", "Apple Chancery", "Brush Script MT", "Segoe Script", cursive; --signature-font: "Snell Roundhand", "Apple Chancery", "Brush Script MT", "Segoe Script", cursive;
} }
@@ -78,9 +79,9 @@ html {
font-size: 18px; font-size: 18px;
line-height: 1.45; line-height: 1.45;
scroll-behavior: smooth; scroll-behavior: smooth;
-webkit-font-smoothing: none; -webkit-font-smoothing: antialiased;
font-smooth: never; -moz-osx-font-smoothing: grayscale;
image-rendering: pixelated; image-rendering: auto;
} }
body { body {
@@ -99,7 +100,7 @@ body::before {
content: ""; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999; z-index: 0;
pointer-events: none; pointer-events: none;
background: background:
repeating-linear-gradient( repeating-linear-gradient(
@@ -112,7 +113,7 @@ body::before {
), ),
radial-gradient(circle at center, transparent 62%, rgb(0 0 0 / var(--vignette-alpha))); radial-gradient(circle at center, transparent 62%, rgb(0 0 0 / var(--vignette-alpha)));
background-size: 100% auto, 100% 100%; background-size: 100% auto, 100% 100%;
mix-blend-mode: multiply; mix-blend-mode: normal;
} }
html[data-display="clean"] body::before, html[data-display="clean"] body::before,
@@ -125,6 +126,25 @@ html[data-display="plain"] body::before {
color: var(--ink); color: var(--ink);
} }
.skip-link {
position: fixed;
top: 0.5rem;
left: 0.5rem;
z-index: 10000;
padding: 0.4rem 0.65rem;
border: var(--rule) solid var(--line);
background: var(--paper);
color: var(--ink);
box-shadow: 2px 2px 0 var(--line);
font-size: 0.78rem;
font-weight: 700;
transform: translateY(calc(-100% - 1rem));
}
.skip-link:focus {
transform: translateY(0);
}
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -141,7 +161,8 @@ a:active {
} }
a:focus-visible, a:focus-visible,
button:focus-visible { button:focus-visible,
summary:focus-visible {
outline: var(--rule) solid var(--ink); outline: var(--rule) solid var(--ink);
outline-offset: 2px; outline-offset: 2px;
background: var(--ink); background: var(--ink);
@@ -169,6 +190,7 @@ summary,
.home { .home {
position: relative; position: relative;
z-index: 1;
width: var(--article-width); width: var(--article-width);
max-width: calc(100vw - 2.5rem); max-width: calc(100vw - 2.5rem);
margin: 1.5rem auto; margin: 1.5rem auto;
@@ -210,8 +232,8 @@ summary,
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1.5rem; width: 2rem;
height: 1.5rem; height: 2rem;
border: var(--rule) solid var(--line); border: var(--rule) solid var(--line);
background: var(--paper); background: var(--paper);
box-shadow: 1px 1px 0 var(--line); box-shadow: 1px 1px 0 var(--line);
@@ -254,9 +276,12 @@ summary,
} }
.file-tab { .file-tab {
display: block; display: inline-flex;
min-height: 2.15rem;
align-items: center;
justify-content: center;
margin-bottom: calc(-1 * var(--rule)); margin-bottom: calc(-1 * var(--rule));
padding: 0.08rem 0.55rem 0.1rem; padding: 0.2rem 0.65rem 0.24rem;
border: var(--rule) solid var(--line); border: var(--rule) solid var(--line);
background: var(--soft); background: var(--soft);
box-shadow: none; box-shadow: none;
@@ -297,6 +322,10 @@ summary,
border-top: 0; border-top: 0;
} }
.file-panel[hidden] {
display: none;
}
.sr-only { .sr-only {
position: absolute; position: absolute;
width: 1px; width: 1px;
@@ -310,7 +339,7 @@ summary,
.item-list { .item-list {
display: grid; display: grid;
gap: 0.22rem; gap: 0.38rem;
} }
.item-row { .item-row {
@@ -319,14 +348,16 @@ summary,
gap: 0.28rem; gap: 0.28rem;
align-items: start; align-items: start;
min-width: 0; min-width: 0;
padding: 0.16rem 0.32rem 0.2rem; padding: 0.42rem 0.48rem 0.46rem;
border: var(--rule) solid var(--line); border: 1px solid var(--line);
background: var(--paper); background: var(--paper);
box-shadow: 2px 2px 0 var(--line); box-shadow: none;
} }
.item-row[data-row-href] { .item-row[data-row-href] {
cursor: pointer; cursor: pointer;
border-width: var(--rule);
box-shadow: 2px 2px 0 var(--line);
} }
.item-row[data-row-href]:hover, .item-row[data-row-href]:hover,
@@ -347,10 +378,6 @@ summary,
color: var(--paper); color: var(--paper);
} }
.item-row + .item-row {
border-top: var(--rule) solid var(--line);
}
.post-list { .post-list {
border-bottom: 0; border-bottom: 0;
} }
@@ -372,8 +399,11 @@ summary,
} }
.tag-filter-status a { .tag-filter-status a {
align-self: baseline; display: inline-flex;
padding: 0 0.22rem; min-height: 1.8rem;
align-items: center;
align-self: center;
padding: 0.1rem 0.4rem;
border: var(--rule) solid var(--line); border: var(--rule) solid var(--line);
background: var(--paper); background: var(--paper);
color: var(--ink); color: var(--ink);
@@ -395,7 +425,7 @@ summary,
grid-template-columns: 5.85rem minmax(0, 1fr); grid-template-columns: 5.85rem minmax(0, 1fr);
gap: 0.65rem; gap: 0.65rem;
align-items: baseline; align-items: baseline;
padding: var(--row-padding) 0; padding: max(var(--row-padding), 0.3rem) 0;
border-top: 0; border-top: 0;
} }
@@ -522,17 +552,18 @@ summary,
} }
.row-summary { .row-summary {
display: -webkit-box;
grid-column: 1 / -1; grid-column: 1 / -1;
min-width: 4rem; min-width: 0;
max-width: 100%; max-width: 100%;
margin: 0; margin: 0.12rem 0 0;
padding-left: 1rem; padding-left: 0;
overflow: hidden; overflow: hidden;
color: var(--muted); color: var(--muted);
font-size: 0.78rem; font-size: 0.78rem;
line-height: 1.1; line-height: 1.28;
text-overflow: ellipsis; -webkit-box-orient: vertical;
white-space: nowrap; -webkit-line-clamp: 2;
} }
.item-status { .item-status {
@@ -572,9 +603,12 @@ summary,
} }
.item-links a { .item-links a {
display: inline-block; display: inline-flex;
min-height: 1.9rem;
align-items: center;
justify-content: center;
max-width: 100%; max-width: 100%;
padding: 0.06rem 0.36rem 0.08rem; padding: 0.12rem 0.42rem;
border: 1px solid currentColor; border: 1px solid currentColor;
background: var(--paper); background: var(--paper);
color: var(--ink); color: var(--ink);
@@ -606,6 +640,40 @@ summary,
font-weight: 600; font-weight: 600;
} }
.tag-empty {
margin: 0.45rem 0 0;
padding: 0.55rem;
border: 1px solid var(--line);
color: var(--muted);
font-size: 0.82rem;
font-weight: 600;
}
.tag-empty[hidden] {
display: none;
}
.publication-row .item-title {
overflow: visible;
text-overflow: clip;
white-space: normal;
}
.publication-venue {
grid-column: 1 / -1;
margin: 0.12rem 0 0;
color: var(--muted);
font-size: 0.75rem;
font-weight: 700;
line-height: 1.25;
}
.publication-links {
grid-column: 1 / -1;
justify-self: start;
margin-top: 0.25rem;
}
.contact-section { .contact-section {
margin-top: var(--section-gap); margin-top: var(--section-gap);
padding: 0.34rem 0.42rem 0.42rem; padding: 0.34rem 0.42rem 0.42rem;
@@ -636,6 +704,9 @@ summary,
.contact-section a { .contact-section a {
color: var(--ink); color: var(--ink);
font-weight: 600; font-weight: 600;
text-decoration: underline;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.16em;
} }
.contact-section a:hover, .contact-section a:hover,
@@ -644,6 +715,33 @@ summary,
color: var(--paper); color: var(--paper);
} }
.error-panel {
margin-top: var(--section-gap);
padding: 1rem;
border: var(--rule) solid var(--line);
text-align: center;
}
.error-code {
margin: 0;
font-family: var(--display-font);
font-size: 4.5rem;
line-height: 0.8;
}
.error-panel h2 {
margin: 0.7rem 0 0.25rem;
font-family: var(--display-font);
font-size: 1.6rem;
font-weight: 400;
}
.error-panel > p:not(.error-code) {
margin: 0 0 0.8rem;
color: var(--muted);
font-size: 0.82rem;
}
.post { .post {
margin: 1.5rem auto; margin: 1.5rem auto;
padding: 0 1.25rem var(--bottom-scroll-room); padding: 0 1.25rem var(--bottom-scroll-room);
@@ -651,6 +749,7 @@ summary,
.post-layout { .post-layout {
position: relative; position: relative;
z-index: 1;
width: var(--article-width); width: var(--article-width);
max-width: calc(100vw - 2.5rem); max-width: calc(100vw - 2.5rem);
margin: 0 auto; margin: 0 auto;
@@ -662,9 +761,12 @@ summary,
.back-link, .back-link,
.post-button { .post-button {
display: inline-block; display: inline-flex;
min-width: 3.2rem; min-width: 3.2rem;
padding: 0.08rem 0.45rem; min-height: 2.15rem;
align-items: center;
justify-content: center;
padding: 0.18rem 0.55rem;
border: var(--rule) solid var(--line); border: var(--rule) solid var(--line);
background: var(--paper); background: var(--paper);
box-shadow: inset 0 0 0 1px var(--paper); box-shadow: inset 0 0 0 1px var(--paper);
@@ -710,15 +812,20 @@ summary,
.post-content { .post-content {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
font-size: 0.95rem; font-family: var(--reading-font);
line-height: 1.55; font-size: 0.94rem;
font-weight: 400;
line-height: 1.72;
} }
.post-footer { .post-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-top: 1.25rem; margin-top: 1.25rem;
padding-top: 0.65rem; padding-top: 0.65rem;
border-top: var(--rule) solid var(--line); border-top: var(--rule) solid var(--line);
text-align: right;
} }
.post-content > :first-child { .post-content > :first-child {
@@ -732,7 +839,7 @@ summary,
line-height: 1.18; line-height: 1.18;
font-weight: 400; font-weight: 400;
letter-spacing: 0; letter-spacing: 0;
margin-top: 1.75em; margin-top: 1.9em;
margin-bottom: 0.45em; margin-bottom: 0.45em;
} }
@@ -756,15 +863,26 @@ summary,
.post-content table, .post-content table,
.post-content figure, .post-content figure,
.post-content .highlight { .post-content .highlight {
margin-top: 0.9rem; margin-top: 1rem;
margin-bottom: 0.7rem; margin-bottom: 0.82rem;
} }
.post-content strong, .post-content strong,
.post-content b { .post-content b {
color: var(--ink); color: var(--ink);
font-weight: 900; font-weight: 750;
text-shadow: 0.035em 0 0 var(--ink); text-shadow: none;
}
.post-content p a,
.post-content li a,
.post-content blockquote a,
.post-content figcaption a,
.post-content td a,
.post-content .footnotes a {
text-decoration: underline;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.16em;
} }
.post-content blockquote { .post-content blockquote {
@@ -776,6 +894,7 @@ summary,
} }
.post-content code { .post-content code {
font-family: var(--body-font);
font-size: 0.9em; font-size: 0.9em;
background: background:
repeating-linear-gradient(45deg, rgb(0 0 0 / 0.08) 0 1px, transparent 1px 4px), repeating-linear-gradient(45deg, rgb(0 0 0 / 0.08) 0 1px, transparent 1px 4px),
@@ -871,8 +990,19 @@ summary,
} }
.post-content img { .post-content img {
display: block;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
image-rendering: auto;
}
.figure-media {
display: block;
width: 100%;
}
.figure-media img {
margin-inline: auto;
} }
.post-content table { .post-content table {
@@ -926,6 +1056,7 @@ summary,
} }
.post-margin { .post-margin {
display: none;
width: 100%; width: 100%;
margin-left: 0; margin-left: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -955,34 +1086,42 @@ summary,
text-align: center; text-align: center;
} }
#TableOfContents ul { #TableOfContents ul,
#MobileTableOfContents ul {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
margin: 0; margin: 0;
} }
#TableOfContents > ul { #TableOfContents > ul,
#MobileTableOfContents > ul {
padding: 0.28rem; padding: 0.28rem;
} }
#TableOfContents ul ul { #TableOfContents ul ul,
#MobileTableOfContents ul ul {
padding-left: 0.55rem; padding-left: 0.55rem;
margin-top: 0.08rem; margin-top: 0.08rem;
} }
#TableOfContents li { #TableOfContents li,
#MobileTableOfContents li {
margin: 0.06rem 0; margin: 0.06rem 0;
} }
#TableOfContents a { #TableOfContents a,
display: block; #MobileTableOfContents a {
padding: 0.04rem 0.12rem; display: flex;
min-height: 1.55rem;
align-items: center;
padding: 0.16rem 0.2rem;
text-decoration: none; text-decoration: none;
line-height: 1.18; line-height: 1.18;
white-space: normal; white-space: normal;
} }
#TableOfContents ul ul a { #TableOfContents ul ul a,
#MobileTableOfContents ul ul a {
font-size: 0.68rem; font-size: 0.68rem;
font-weight: 600; font-weight: 600;
} }
@@ -993,6 +1132,44 @@ summary,
font-weight: 600; font-weight: 600;
} }
.post-toc-inline {
margin-top: 1rem;
border: var(--rule) solid var(--line);
background: var(--paper);
font-family: var(--body-font);
font-size: 0.78rem;
font-weight: 600;
}
.post-toc-inline summary {
min-height: 2.25rem;
padding: 0.38rem 0.55rem;
background: var(--soft);
font-family: var(--display-font);
font-size: 1.08rem;
font-weight: 400;
line-height: 1.15;
}
.post-toc-inline[open] summary {
border-bottom: var(--rule) solid var(--line);
}
.post-toc-inline summary:focus-visible {
background: var(--ink);
color: var(--paper);
}
.post-toc-inline-body {
padding: 0.4rem;
}
#MobileTableOfContents a {
display: flex;
min-height: 1.9rem;
padding: 0.3rem 0.35rem;
}
.sidenote-number { .sidenote-number {
counter-increment: sidenote; counter-increment: sidenote;
} }
@@ -1046,6 +1223,7 @@ summary,
} }
.post-layout.has-toc .post-margin { .post-layout.has-toc .post-margin {
display: block;
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
width: var(--margin-width); width: var(--margin-width);
@@ -1056,15 +1234,13 @@ summary,
left: calc(50% - var(--article-half-width) - var(--margin-gap) - var(--margin-width)); left: calc(50% - var(--article-half-width) - var(--margin-gap) - var(--margin-width));
align-self: start; align-self: start;
} }
.post-toc-inline {
display: none;
}
} }
@media (max-width: 1280px) { @media (max-width: 1280px) {
.post-margin {
max-height: none;
margin-bottom: 2rem;
padding-top: 1rem;
}
.sidenote { .sidenote {
display: none; display: none;
float: none; float: none;
@@ -1084,7 +1260,7 @@ summary,
:root { :root {
--panel-padding: 0.72rem; --panel-padding: 0.72rem;
--section-gap: 0.8rem; --section-gap: 0.8rem;
--bottom-scroll-room: 34vh; --bottom-scroll-room: 10vh;
--dither-alpha: 0.055; --dither-alpha: 0.055;
--scanline-alpha: 0.045; --scanline-alpha: 0.045;
--scanline-soft-alpha: 0.014; --scanline-soft-alpha: 0.014;
@@ -1156,17 +1332,13 @@ summary,
.item-row { .item-row {
grid-template-columns: 4.35rem minmax(0, 1fr); grid-template-columns: 4.35rem minmax(0, 1fr);
gap: 0.2rem; gap: 0.2rem;
padding: 0.28rem 0.32rem; padding: 0.4rem 0.42rem 0.44rem;
} }
.post-row + .post-row { .post-row + .post-row {
border-top: var(--rule) solid var(--line); border-top: var(--rule) solid var(--line);
} }
.item-row + .item-row {
border-top: 1px solid var(--line);
}
.item-body { .item-body {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 0.08rem 0.36rem; gap: 0.08rem 0.36rem;
@@ -1188,14 +1360,13 @@ summary,
min-width: 0; min-width: 0;
padding-left: 0; padding-left: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; white-space: normal;
white-space: nowrap;
} }
.item-links { .item-links {
width: auto; width: auto;
gap: 0.24rem; gap: 0.24rem;
flex-wrap: nowrap; flex-wrap: wrap;
justify-self: end; justify-self: end;
} }
@@ -1230,8 +1401,8 @@ summary,
} }
.post-content { .post-content {
font-size: 0.92rem; font-size: 0.95rem;
line-height: 1.5; line-height: 1.66;
} }
.post-content h2 { .post-content h2 {
@@ -1263,21 +1434,6 @@ summary,
font-size: 0.72rem; font-size: 0.72rem;
} }
.post-margin {
max-height: 12rem;
margin-bottom: 1rem;
padding-top: 0;
overflow: auto;
font-size: 0.72rem;
}
.toc-title {
font-size: 0.95rem;
}
#TableOfContents > ul {
padding: 0.22rem;
}
} }
@media (max-width: 420px) { @media (max-width: 420px) {
@@ -1290,8 +1446,8 @@ summary,
} }
.social-link { .social-link {
width: 1.42rem; width: 2rem;
height: 1.42rem; height: 2rem;
} }
.social-link img { .social-link img {
@@ -1299,12 +1455,45 @@ summary,
height: 0.88rem; height: 0.88rem;
} }
.file-tabs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.25rem;
overflow: visible;
padding-left: 0;
border-bottom: 0;
}
.file-tab { .file-tab {
padding-right: 0.34rem; width: 100%;
padding-left: 0.34rem; margin-bottom: 0;
padding-right: 0.42rem;
padding-left: 0.42rem;
font-size: 0.95rem; font-size: 0.95rem;
} }
.file-tab.active,
.file-tab.active:hover,
.file-tab.active:focus-visible {
border-bottom-color: var(--line);
background: var(--ink);
color: var(--paper);
}
.file-panel {
margin-top: 0.4rem;
border-top: var(--rule) solid var(--line);
}
.item-row {
grid-template-columns: 1fr;
gap: 0.16rem;
}
.item-links {
justify-self: start;
}
.post-title, .post-title,
.item-title { .item-title {
font-size: 1.12rem; font-size: 1.12rem;
@@ -1315,6 +1504,12 @@ summary,
} }
} }
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) { @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
:root { :root {
--dither-alpha: 0; --dither-alpha: 0;

240
assets/js/site.js Normal file
View File

@@ -0,0 +1,240 @@
(() => {
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
function isPlainLeftClick(event) {
return (
event.button === 0 &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey
);
}
function setupFileTabs() {
const index = $("[data-file-index]");
if (!index) return;
const tabs = $$("[data-file-tab]", index);
const panels = $$("[data-file-panel]", index);
const announcer = $("[data-file-announcer]", index);
const canonical = $('link[rel="canonical"]');
const routes = new Map(
tabs.map((tab) => [new URL(tab.href, window.location.href).pathname, tab.dataset.fileTab])
);
let activeId = null;
function idForLocation() {
return routes.get(window.location.pathname) || "posts";
}
function activate(id, { announce = false } = {}) {
const activeTab = tabs.find((tab) => tab.dataset.fileTab === id) || tabs[0];
if (!activeTab) return;
const changed = activeId !== activeTab.dataset.fileTab;
activeId = activeTab.dataset.fileTab;
for (const tab of tabs) {
const active = tab === activeTab;
tab.classList.toggle("active", active);
if (active) tab.setAttribute("aria-current", "page");
else tab.removeAttribute("aria-current");
}
for (const panel of panels) {
panel.hidden = panel.dataset.filePanel !== activeTab.dataset.fileTab;
}
document.title = activeTab.dataset.pageTitle || document.title;
if (canonical) canonical.href = new URL(activeTab.href, window.location.href).href;
if (announce && changed && announcer) {
announcer.textContent = `Showing ${activeTab.textContent.trim()}`;
}
}
for (const tab of tabs) {
tab.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
const url = new URL(tab.href, window.location.href);
const nextLocation = `${url.pathname}${url.search}${url.hash}`;
const currentLocation = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (nextLocation !== currentLocation) {
window.history.pushState({ fileTab: tab.dataset.fileTab }, "", nextLocation);
}
activate(tab.dataset.fileTab, { announce: true });
window.dispatchEvent(new CustomEvent("filetabchange"));
});
}
window.addEventListener("popstate", () => activate(idForLocation(), { announce: true }));
activate(idForLocation());
}
function setupToc() {
const headings = $$(".post-content h2[id], .post-content h3[id]");
const links = $$("#TableOfContents a");
if (!headings.length || !links.length) return;
let queued = false;
function update() {
const y = window.scrollY + window.innerHeight * 0.35;
let activeId = headings[0].id;
for (const heading of headings) {
const headingY = heading.getBoundingClientRect().top + window.scrollY;
if (headingY > y) break;
activeId = heading.id;
}
for (const link of links) {
link.classList.toggle(
"active",
decodeURIComponent(link.hash.slice(1)) === activeId
);
}
queued = false;
}
function queue() {
if (queued) return;
queued = true;
window.requestAnimationFrame(update);
}
window.addEventListener("scroll", queue, { passive: true });
window.addEventListener("resize", queue);
update();
}
function setupPostTags() {
const list = $("[data-post-list]");
if (!list) return;
const rows = $$(".post-row", list);
const links = $$("[data-tag]");
const status = $("[data-tag-filter-status]");
const clear = status?.querySelector("a");
const empty = $("[data-tag-empty]");
function setUrl(tag) {
const url = new URL(window.location.href);
if (tag) url.searchParams.set("tag", tag);
else url.searchParams.delete("tag");
window.history.pushState({ tag }, "", `${url.pathname}${url.search}`);
}
function applyTag(tag) {
for (const link of links) {
link.classList.toggle("active", link.dataset.tag === tag);
}
let visible = 0;
for (const row of rows) {
const tags = JSON.parse(row.dataset.tags || "[]");
const matched = !tag || tags.includes(tag);
row.hidden = !matched;
if (matched) visible += 1;
}
if (status) {
const name = $("[data-tag-filter-name]", status);
const count = $("[data-tag-filter-count]", status);
if (name) name.textContent = tag ? `[${tag}]` : "";
if (count) count.textContent = `${visible} ${visible === 1 ? "post" : "posts"}`;
status.hidden = !tag;
status.classList.toggle("is-empty", visible === 0);
}
if (empty) empty.hidden = !tag || visible > 0;
}
function applyFromLocation() {
applyTag(new URLSearchParams(window.location.search).get("tag"));
}
for (const link of links) {
link.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
const nextTag = link.classList.contains("active") ? null : link.dataset.tag;
setUrl(nextTag);
applyTag(nextTag);
});
}
if (clear) {
clear.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
setUrl(null);
applyTag(null);
});
}
window.addEventListener("popstate", applyFromLocation);
window.addEventListener("filetabchange", applyFromLocation);
applyFromLocation();
}
function setupClickableRows() {
const rows = $$("[data-row-href]");
for (const row of rows) {
row.addEventListener("click", (event) => {
if (event.target.closest("a")) return;
const href = row.dataset.rowHref;
if (href) window.location.assign(href);
});
row.addEventListener("pointerdown", (event) => {
if (event.target.closest("a")) return;
row.classList.add("is-pressed");
});
for (const eventName of ["pointerup", "pointercancel", "pointerleave"]) {
row.addEventListener(eventName, () => row.classList.remove("is-pressed"));
}
}
}
function setupPrefetch() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection?.saveData || /(^|-)2g$/.test(connection?.effectiveType || "")) return;
const prefetched = new Set();
function prefetch(href) {
const url = new URL(href, window.location.href);
if (url.origin !== window.location.origin || url.hash || url.pathname === window.location.pathname) return;
if (prefetched.has(url.href)) return;
prefetched.add(url.href);
const hint = document.createElement("link");
hint.rel = "prefetch";
hint.as = "document";
hint.href = url.href;
document.head.append(hint);
}
for (const link of $$(".item-title[href], .post-title[href], .back-link[href], .post-button[href]")) {
link.addEventListener("pointerenter", () => prefetch(link.href), { once: true });
link.addEventListener("focus", () => prefetch(link.href), { once: true });
}
for (const row of $$("[data-row-href]")) {
row.addEventListener("pointerenter", () => prefetch(row.dataset.rowHref), { once: true });
}
}
setupFileTabs();
setupToc();
setupPostTags();
setupClickableRows();
setupPrefetch();
})();

View File

@@ -68,7 +68,7 @@ So the practical distinction is not "does this GPU support FP4 at all?" It does.
I still wanted to measure the card instead of just reading target tables. So I pulled CUTLASS and ran its NVFP4 matrix multiplication example on the RTX 5090. I still wanted to measure the card instead of just reading target tables. So I pulled CUTLASS and ran its NVFP4 matrix multiplication example on the RTX 5090.
{{< figure src="/images/1_blackwell_dc_vs_gf/5090_65536_cropped.png" alt="A screenshot of a CUTLASS NVFP4 matrix multiplication benchmark on an RTX 5090" />}} {{< figure src="/images/1_blackwell_dc_vs_gf/5090_65536_cropped.png" width="236" height="77" alt="A screenshot of a CUTLASS NVFP4 matrix multiplication benchmark on an RTX 5090" />}}
That is over a petaflop of NVFP4 compute. That is over a petaflop of NVFP4 compute.
@@ -76,7 +76,7 @@ A petaflop means one quadrillion floating-point operations per second. For a des
But the next question is whether the Tensor Cores are being fed efficiently. Nsight Compute gives the more interesting picture. But the next question is whether the Tensor Cores are being fed efficiently. Nsight Compute gives the more interesting picture.
{{< figure src="/images/1_blackwell_dc_vs_gf/geforce_ncu.png" alt="Nsight Compute showing register pressure and memory bottlenecks on a GeForce GPU" />}} {{< figure src="/images/1_blackwell_dc_vs_gf/geforce_ncu.png" width="1974" height="807" alt="Nsight Compute showing register pressure and memory bottlenecks on a GeForce GPU" />}}
The short read is: memory is the bottleneck. The Tensor Cores can chew through math faster than the rest of the kernel can keep them supplied. Shared memory pressure shows up immediately. The short read is: memory is the bottleneck. The Tensor Cores can chew through math faster than the rest of the kernel can keep them supplied. Shared memory pressure shows up immediately.
@@ -86,9 +86,9 @@ That is exactly the problem the datacenter path is designed to address.
To compare against datacenter Blackwell, I rented a B200 instance on Vast.ai and ran the same kind of matrix multiplication with CUTLASS kernels targeting `sm_100a`. To compare against datacenter Blackwell, I rented a B200 instance on Vast.ai and ran the same kind of matrix multiplication with CUTLASS kernels targeting `sm_100a`.
{{< figure src="/images/1_blackwell_dc_vs_gf/nvtop_b200.png" alt="nvtop showing B200 GPU memory capacity" />}} {{< figure src="/images/1_blackwell_dc_vs_gf/nvtop_b200.png" width="647" height="106" alt="nvtop showing B200 GPU memory capacity" />}}
{{< figure src="/images/1_blackwell_dc_vs_gf/b200_65536_cropped.png" alt="A CUTLASS benchmark result from a B200 GPU" />}} {{< figure src="/images/1_blackwell_dc_vs_gf/b200_65536_cropped.png" width="300" height="108" alt="A CUTLASS benchmark result from a B200 GPU" />}}
That run gets past 2 petaflops, and I suspect better kernels can push it further. The point is not just the number. The point is that the B200 has access to the datacenter Blackwell path that the 5090 does not. That run gets past 2 petaflops, and I suspect better kernels can push it further. The point is not just the number. The point is that the B200 has access to the datacenter Blackwell path that the 5090 does not.

View File

@@ -31,7 +31,7 @@ That is awkward for a large GPU. A 5090 has a lot of compute, but a single decod
So raw GPU size does not always turn into efficient local inference. So raw GPU size does not always turn into efficient local inference.
{{< figure src="/images/2_qwen36_tokens_per_kwh/fig5_autoregressive_decode.svg" alt="Autoregressive decoding diagram showing one model pass producing one accepted token at a time" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/fig5_autoregressive_decode.svg" width="1200" height="520" alt="Autoregressive decoding diagram showing one model pass producing one accepted token at a time" />}}
Speculative decoding tries to fix this shape. Speculative decoding tries to fix this shape.
@@ -49,7 +49,7 @@ That is why the Qwen3.6 MTP model caught my attention. `Qwen3.6-27B` is a dense
Large enough to be useful. Small enough to run locally. Large enough to be useful. Small enough to run locally.
{{< figure src="/images/2_qwen36_tokens_per_kwh/fig6_mtp_decode.svg" alt="MTP speculative decoding diagram showing a lookahead head proposing several draft tokens and the target model verifying them" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/fig6_mtp_decode.svg" width="1200" height="560" alt="MTP speculative decoding diagram showing a lookahead head proposing several draft tokens and the target model verifying them" />}}
## The 5090 settings ## The 5090 settings
@@ -84,7 +84,7 @@ The same power settings without MTP gave me **32.1 tok/s**.
So MTP was not a small change. It moved the setup from usable to comfortable. So MTP was not a small change. It moved the setup from usable to comfortable.
{{< figure src="/images/2_qwen36_tokens_per_kwh/author_nvidia_mtp_x6_215w_report.png" alt="Author measurement screenshot showing Qwen3.6-27B MTP throughput and a 215 watt GPU power reading" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/author_nvidia_mtp_x6_215w_report.png" width="1656" height="807" alt="Author measurement screenshot showing Qwen3.6-27B MTP throughput and a 215 watt GPU power reading" />}}
## The math ## The math
@@ -170,7 +170,7 @@ That is still fine for me. I would leave that running.
Could I push the GPU lower? Yes. I can get the 5090 closer to **150 W**. I need a proper sweep before I say where the best point is. For now, 215 W is the point I like. It is fast enough, and it does not feel wasteful. Could I push the GPU lower? Yes. I can get the 5090 closer to **150 W**. I need a proper sweep before I say where the best point is. For now, 215 W is the point I like. It is fast enough, and it does not feel wasteful.
{{< figure src="/images/2_qwen36_tokens_per_kwh/fig1_tokens_per_day.png" alt="Bar chart comparing generated tokens per day across the measured NVIDIA run and M3 Ultra benchmark rows" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/fig1_tokens_per_day.png" width="1800" height="990" alt="Bar chart comparing generated tokens per day across the measured NVIDIA run and M3 Ultra benchmark rows" />}}
## The Mac comparison ## The Mac comparison
@@ -190,7 +190,7 @@ Those are good numbers for agent work. The missing number is power.
Against my GPU only 215 W point, the M3 Ultra 80 core oQ4 MTP rows need to run around **95 to 104 W at the wall** to match the 5090 on generated tokens per kWh. Against my GPU only 215 W point, the M3 Ultra 80 core oQ4 MTP rows need to run around **95 to 104 W at the wall** to match the 5090 on generated tokens per kWh.
{{< figure src="/images/2_qwen36_tokens_per_kwh/fig3_m3_break_even_power.png" alt="Bar chart showing the M3 Ultra wall-power levels needed to match the measured NVIDIA GPU-side tokens per kWh" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/fig3_m3_break_even_power.png" width="1800" height="990" alt="Bar chart showing the M3 Ultra wall-power levels needed to match the measured NVIDIA GPU-side tokens per kWh" />}}
Against my rough 275 W whole box estimate, the break even numbers move up. Against my rough 275 W whole box estimate, the break even numbers move up.
@@ -211,7 +211,7 @@ That is why I do not want to turn this into a clean win for either side. The Mac
The real comparison needs outlet power during the same workload. The real comparison needs outlet power during the same workload.
{{< figure src="/images/2_qwen36_tokens_per_kwh/fig4_m3_power_sensitivity.png" alt="Line chart showing M3 Ultra tokens per kWh across different assumed wall-power levels" />}} {{< figure src="/images/2_qwen36_tokens_per_kwh/fig4_m3_power_sensitivity.png" width="1800" height="990" alt="Line chart showing M3 Ultra tokens per kWh across different assumed wall-power levels" />}}
## What changed for me ## What changed for me

View File

@@ -19,7 +19,7 @@ math: false
ClipBored is a small native macOS clipboard manager. It captures local clipboard history and opens a keyboard-first responsive bottom panel for search, sorting, copy, paste, pinning, deletion, and organization. ClipBored is a small native macOS clipboard manager. It captures local clipboard history and opens a keyboard-first responsive bottom panel for search, sorting, copy, paste, pinning, deletion, and organization.
{{< figure src="/images/projects/clipbored/panel.png" alt="ClipBored clipboard panel snapshot." />}} {{< figure src="/images/projects/clipbored/panel.png" width="3024" height="860" alt="ClipBored clipboard panel snapshot." />}}
## What It Does ## What It Does

View File

@@ -15,7 +15,7 @@ math: false
FeedMe is a small native SwiftUI feed reader for macOS and iOS. It is local-first, account-free, and built around a shared Swift package for feed parsing, OPML import/export, refresh orchestration, and SQLite persistence. FeedMe is a small native SwiftUI feed reader for macOS and iOS. It is local-first, account-free, and built around a shared Swift package for feed parsing, OPML import/export, refresh orchestration, and SQLite persistence.
{{< figure src="/images/projects/feedme/demo.png" alt="FeedMe running with a local demo library on iOS." />}} {{< figure src="/images/projects/feedme/demo.png" width="644" height="1400" alt="FeedMe running with a local demo library on iOS." />}}
## What It Does ## What It Does

View File

@@ -21,7 +21,7 @@ math: false
I Hate PDFs is a small native macOS PDF reader for local reading, highlighting, commenting, and review. It uses SwiftUI, AppKit, and PDFKit, keeps documents on your Mac, and avoids accounts, tracking, and cloud upload. I Hate PDFs is a small native macOS PDF reader for local reading, highlighting, commenting, and review. It uses SwiftUI, AppKit, and PDFKit, keeps documents on your Mac, and avoids accounts, tracking, and cloud upload.
{{< figure src="/images/projects/ihatepdfs/default-reading.png" alt="I Hate PDFs default reading mode." />}} {{< figure src="/images/projects/ihatepdfs/default-reading.png" width="2024" height="1568" alt="I Hate PDFs default reading mode." />}}
## What It Does ## What It Does

View File

@@ -17,7 +17,7 @@ math: false
Inspire Me is a small browser app that displays a changing stream of words over a soft animated atmosphere. It is built as a static web app with JavaScript, Canvas, CSS typography controls, and a local word corpus. Inspire Me is a small browser app that displays a changing stream of words over a soft animated atmosphere. It is built as a static web app with JavaScript, Canvas, CSS typography controls, and a local word corpus.
{{< figure src="/images/projects/inspire-me/preview.png" alt="Inspire Me showing a large word over a soft animated paper-like background." />}} {{< figure src="/images/projects/inspire-me/preview.png" width="1440" height="900" alt="Inspire Me showing a large word over a soft animated paper-like background." />}}
## What It Does ## What It Does

View File

@@ -0,0 +1,12 @@
# Production cache setup
The container now sends long-lived cache headers for fingerprinted CSS and JavaScript, 30-day headers for fonts and images, and a one-hour shared-cache lifetime for HTML.
Cloudflare does not cache HTML by default. Add a Cache Rule for `akkolli.net` that marks successful `GET` and `HEAD` HTML responses as eligible for cache, respects the origin `Cache-Control` header, and uses the origin-provided Edge TTL.
Add these Gitea Actions secrets so a deploy purges stale HTML after the new container starts:
- `CLOUDFLARE_ZONE_ID`
- `CLOUDFLARE_API_TOKEN` with `Cache Purge` permission for the zone
The deployment remains functional when those secrets are absent; it simply skips the purge.

11
layouts/404.html Normal file
View File

@@ -0,0 +1,11 @@
{{ define "main" }}
<main class="home error-page" id="main-content">
{{ partial "home-intro.html" . }}
<section class="error-panel" aria-labelledby="not-found-heading">
<p class="error-code" aria-hidden="true">404</p>
<h2 id="not-found-heading">File not found</h2>
<p>The page may have moved, or the address may be incomplete.</p>
<a class="post-button" href="{{ "/" | relURL }}">&lt; Home</a>
</section>
</main>
{{ end }}

View File

@@ -1,5 +1,5 @@
{{ define "main" }} {{ define "main" }}
<main class="home"> <main class="home" id="main-content">
{{ partial "home-intro.html" . }} {{ partial "home-intro.html" . }}
{{ partial "file-index.html" (dict "active" .Section) }} {{ partial "file-index.html" (dict "active" .Section) }}
</main> </main>

View File

@@ -1,6 +1,7 @@
{{ define "main" }} {{ define "main" }}
<article class="post" id="top"> <main class="post" id="main-content">
{{ $hasToc := and (ne .Params.toc false) (gt (len (findRE "<li>" .TableOfContents)) 0) }} {{ $hasToc := and (ne .Params.toc false) (gt (len (findRE "<li>" .TableOfContents)) 0) }}
{{ $mobileToc := replace .TableOfContents `id="TableOfContents"` `id="MobileTableOfContents"` }}
{{ $backHref := "/" | relURL }} {{ $backHref := "/" | relURL }}
{{ $backLabel := "Home" }} {{ $backLabel := "Home" }}
{{ if in (slice "projects" "websites") .Section }} {{ if in (slice "projects" "websites") .Section }}
@@ -10,7 +11,7 @@
{{ $backHref = "/publications/" | relURL }} {{ $backHref = "/publications/" | relURL }}
{{ $backLabel = "Publications" }} {{ $backLabel = "Publications" }}
{{ end }} {{ end }}
<div class="post-layout{{ if $hasToc }} has-toc{{ end }}"> <article class="post-layout{{ if $hasToc }} has-toc{{ end }}">
{{ if $hasToc }} {{ if $hasToc }}
<aside class="post-margin" aria-label="Table of contents"> <aside class="post-margin" aria-label="Table of contents">
<div class="toc"> <div class="toc">
@@ -48,16 +49,26 @@
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}
{{ if $hasToc }}
<details class="post-toc-inline">
<summary>On this page</summary>
<div class="post-toc-inline-body">
{{ $mobileToc | safeHTML }}
</div>
</details>
{{ end }}
</header> </header>
<main class="post-content"> <div class="post-content">
{{ .Content }} {{ .Content }}
</main> </div>
<footer class="post-footer"> <footer class="post-footer">
<a class="post-button" href="#top" aria-label="Scroll to top" title="Scroll to top">^ Top</a> <a class="post-button" href="{{ $backHref }}" aria-label="Back to {{ $backLabel }}" title="Back to {{ $backLabel }}">&lt; {{ $backLabel }}</a>
<a class="post-button" href="#main-content" aria-label="Scroll to top" title="Scroll to top">^ Top</a>
</footer> </footer>
</div> </div>
</div>
</article> </article>
</main>
{{ end }} {{ end }}

View File

@@ -2,13 +2,43 @@
{{ $src := .Get "src" }} {{ $src := .Get "src" }}
{{ $caption := .Get "caption" }} {{ $caption := .Get "caption" }}
{{ $alt := .Get "alt" | default $caption }} {{ $alt := .Get "alt" | default $caption }}
{{ $width := .Get "width" }}
{{ $height := .Get "height" }}
{{ $loading := .Get "loading" | default "lazy" }}
{{ $fetchpriority := .Get "fetchpriority" }}
{{ $figures := .Page.Scratch.Get "figures" | default dict }} {{ $figures := .Page.Scratch.Get "figures" | default dict }}
{{ $number := add (len $figures) 1 }} {{ $number := add (len $figures) 1 }}
{{ .Page.Scratch.SetInMap "figures" $id $number }} {{ .Page.Scratch.SetInMap "figures" $id $number }}
<figure id="{{ $id }}" class="figure"> <figure id="{{ $id }}" class="figure">
{{ with $src }} {{ with $src }}
<img src="{{ . | relURL }}" alt="{{ $alt }}"> {{ $ext := path.Ext . }}
{{ $base := strings.TrimSuffix $ext . }}
{{ $srcset := slice }}
{{ range slice 480 644 768 960 1344 }}
{{ $candidate := printf "%s-%d.webp" $base . }}
{{ if fileExists (printf "static%s" $candidate) }}
{{ $srcset = $srcset | append (printf "%s %dw" ($candidate | relURL) .) }}
{{ end }}
{{ end }}
<picture class="figure-media">
{{ with $srcset }}
<source
type="image/webp"
srcset="{{ delimit . ", " }}"
sizes="(max-width: 860px) calc(100vw - 2rem), 42rem"
>
{{ end }}
<img
src="{{ . | relURL }}"
alt="{{ $alt }}"
{{ with $width }}width="{{ . }}"{{ end }}
{{ with $height }}height="{{ . }}"{{ end }}
loading="{{ $loading }}"
decoding="async"
{{ with $fetchpriority }}fetchpriority="{{ . }}"{{ end }}
>
</picture>
{{ end }} {{ end }}
{{ if or $caption .Inner }} {{ if or $caption .Inner }}

View File

@@ -7,19 +7,37 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title> {{ $pageTitle := site.Title }}
{{- if .IsHome -}} {{ if not .IsHome }}{{ $pageTitle = printf "%s · %s" .Title site.Title }}{{ end }}
{{ site.Title }} · {{ site.Params.author }} {{ $description := .Description | default site.Params.description }}
{{- else -}} {{ $socialImage := "og.jpg" | absURL }}
{{ .Title }} · {{ site.Title }}
{{- end -}} <title>{{ $pageTitle }}</title>
</title> <meta name="description" content="{{ $description }}">
<meta name="description" content="{{ .Description | default site.Params.description }}"> <meta name="theme-color" content="#c8c8c8">
<link rel="icon" type="image/png" sizes="50x50" href="{{ "favicon.png" | relURL }}"> <link rel="canonical" href="{{ .Permalink }}">
<link rel="icon" type="image/png" href="{{ "favicon.png" | relURL }}">
<link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ "index.xml" | absURL }}"> <link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ "index.xml" | absURL }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:site_name" content="{{ site.Title }}">
<meta property="og:title" content="{{ $pageTitle }}">
<meta property="og:description" content="{{ $description }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:image" content="{{ $socialImage }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{{ site.Title }} — research notes and small software">
{{ if .IsPage }}<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">{{ end }}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $pageTitle }}">
<meta name="twitter:description" content="{{ $description }}">
<meta name="twitter:image" content="{{ $socialImage }}">
<meta name="twitter:image:alt" content="{{ site.Title }} — research notes and small software">
<link rel="preload" href="{{ "fonts/VT323-Regular.woff" | relURL }}" as="font" type="font/woff" crossorigin> <link rel="preload" href="{{ "fonts/VT323-Regular.woff" | relURL }}" as="font" type="font/woff" crossorigin>
<style>{{ readFile "static/css/main.css" | safeCSS }}</style> {{ $styles := resources.Get "css/main.css" | minify | fingerprint "sha384" }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity }}" crossorigin="anonymous">
{{ if .Param "math" }} {{ if .Param "math" }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
@@ -31,10 +49,12 @@
></script> ></script>
{{ end }} {{ end }}
<script defer src="{{ "js/site.js" | relURL }}"></script> {{ $scripts := resources.Get "js/site.js" | minify | fingerprint "sha384" }}
<script defer src="{{ $scripts.RelPermalink }}" integrity="{{ $scripts.Data.Integrity }}" crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<a class="skip-link" href="#main-content">Skip to content</a>
{{ block "main" . }}{{ end }} {{ block "main" . }}{{ end }}
</body> </body>

View File

@@ -1,5 +1,5 @@
{{ define "main" }} {{ define "main" }}
<main class="home"> <main class="home" id="main-content">
{{ partial "home-intro.html" . }} {{ partial "home-intro.html" . }}
{{ partial "file-index.html" (dict "active" "posts") }} {{ partial "file-index.html" (dict "active" "posts") }}
</main> </main>

View File

@@ -1,3 +1,3 @@
<section class="contact-section" aria-labelledby="contact-email-heading"> <section class="contact-section" aria-label="Contact by email">
<p>Email me at <a href="mailto:akshaykolli@hotmail.com">akshaykolli@hotmail.com</a></p> <p>Email me at <a href="mailto:akshaykolli@hotmail.com">akshaykolli@hotmail.com</a></p>
</section> </section>

View File

@@ -1,17 +1,19 @@
{{ $active := .active | default "posts" }} {{ $active := .active | default "posts" }}
{{ $tabs := slice {{ $tabs := slice
(dict "id" "posts" "label" "Posts" "href" ("/" | relURL)) (dict "id" "posts" "label" "Posts" "href" ("/" | relURL) "pageTitle" site.Title)
(dict "id" "projects" "label" "Projects" "href" ("/projects/" | relURL)) (dict "id" "projects" "label" "Projects" "href" ("/projects/" | relURL) "pageTitle" (printf "Projects · %s" site.Title))
(dict "id" "publications" "label" "Publications" "href" ("/publications/" | relURL)) (dict "id" "publications" "label" "Publications" "href" ("/publications/" | relURL) "pageTitle" (printf "Publications · %s" site.Title))
(dict "id" "contact" "label" "Contact" "href" ("/contact/" | relURL)) (dict "id" "contact" "label" "Contact" "href" ("/contact/" | relURL) "pageTitle" (printf "Contact · %s" site.Title))
}} }}
<div class="file-index"> <div class="file-index" data-file-index>
<nav class="file-tabs" aria-label="Index views"> <nav class="file-tabs" aria-label="Index views">
{{ range $tabs }} {{ range $tabs }}
<a <a
class="file-tab{{ if eq $active .id }} active{{ end }}" class="file-tab{{ if eq $active .id }} active{{ end }}"
href="{{ .href }}" href="{{ .href }}"
data-file-tab="{{ .id }}"
data-page-title="{{ .pageTitle }}"
aria-label="Show {{ .label }}" aria-label="Show {{ .label }}"
title="Show {{ .label }}" title="Show {{ .label }}"
{{ if eq $active .id }}aria-current="page"{{ end }} {{ if eq $active .id }}aria-current="page"{{ end }}
@@ -19,17 +21,26 @@
{{ end }} {{ end }}
</nav> </nav>
<section class="file-panel" aria-labelledby="{{ $active }}-heading"> <p class="sr-only" data-file-announcer aria-live="polite"></p>
<h2 class="sr-only" id="{{ $active }}-heading">{{ title $active }}</h2> <div class="file-panel-host" data-file-panel-host>
<section class="file-panel" data-file-panel="posts" aria-labelledby="posts-heading"{{ if ne $active "posts" }} hidden{{ end }}>
{{ if eq $active "publications" }} <h2 class="sr-only" id="posts-heading">Posts</h2>
{{ partial "publication-list.html" hugo.Data.publications }}
{{ else if eq $active "projects" }}
{{ partial "page-list.html" (where site.RegularPages "Section" "projects") }}
{{ else if eq $active "contact" }}
{{ partial "contact-section.html" . }}
{{ else }}
{{ partial "post-list.html" (where site.RegularPages "Section" "posts") }} {{ partial "post-list.html" (where site.RegularPages "Section" "posts") }}
{{ end }} </section>
<section class="file-panel" data-file-panel="projects" aria-labelledby="projects-heading"{{ if ne $active "projects" }} hidden{{ end }}>
<h2 class="sr-only" id="projects-heading">Projects</h2>
{{ partial "page-list.html" (where site.RegularPages "Section" "projects") }}
</section>
<section class="file-panel" data-file-panel="publications" aria-labelledby="publications-heading"{{ if ne $active "publications" }} hidden{{ end }}>
<h2 class="sr-only" id="publications-heading">Publications</h2>
{{ partial "publication-list.html" hugo.Data.publications }}
</section>
<section class="file-panel" data-file-panel="contact" aria-labelledby="contact-heading"{{ if ne $active "contact" }} hidden{{ end }}>
<h2 class="sr-only" id="contact-heading">Contact</h2>
{{ partial "contact-section.html" . }}
</section> </section>
</div> </div>
</div>

View File

@@ -2,7 +2,7 @@
<div class="item-list"> <div class="item-list">
{{ range .ByWeight }} {{ range .ByWeight }}
{{ $pageTitle := .Title }} {{ $pageTitle := .Title }}
<article class="item-row" data-row-href="{{ .RelPermalink }}" role="link" tabindex="0" aria-label="Open {{ $pageTitle }}"> <article class="item-row" data-row-href="{{ .RelPermalink }}">
<div class="item-year">{{ with .Date }}{{ .Format "Jan 2006" }}{{ end }}</div> <div class="item-year">{{ with .Date }}{{ .Format "Jan 2006" }}{{ end }}</div>
<div class="item-body"> <div class="item-body">
<div class="item-head"> <div class="item-head">

View File

@@ -1,9 +1,11 @@
{{ with . }} {{ with . }}
<div class="tag-filter-status" data-tag-filter-status hidden> <div class="tag-filter-status" data-tag-filter-status aria-live="polite" hidden>
<span>Showing <span data-tag-filter-name></span></span> <span>Showing <span data-tag-filter-name></span> · <span data-tag-filter-count></span></span>
<a href="{{ "/" | relURL }}" aria-label="Clear tag filter" title="Clear tag filter">clear</a> <a href="{{ "/" | relURL }}" aria-label="Clear tag filter" title="Clear tag filter">clear</a>
</div> </div>
<p class="tag-empty" data-tag-empty hidden>No posts match this tag.</p>
<div class="post-list" data-post-list> <div class="post-list" data-post-list>
{{ range .ByDate.Reverse }} {{ range .ByDate.Reverse }}
{{ $tags := .Params.tags | default (slice) }} {{ $tags := .Params.tags | default (slice) }}

View File

@@ -3,7 +3,7 @@
{{ range sort . "date" "desc" }} {{ range sort . "date" "desc" }}
{{ $pubTitle := .title }} {{ $pubTitle := .title }}
{{ $authors := .authors }} {{ $authors := .authors }}
<article class="item-row"> <article class="item-row publication-row">
<div class="item-year"> <div class="item-year">
{{ with .date }} {{ with .date }}
{{ time.Format "Jan 2006" . }} {{ time.Format "Jan 2006" . }}
@@ -15,7 +15,13 @@
<div class="item-head"> <div class="item-head">
<div class="item-title">{{ $pubTitle }}</div> <div class="item-title">{{ $pubTitle }}</div>
</div> </div>
{{ with .venue }}<p class="publication-venue">{{ . }}</p>{{ end }}
{{ with $authors }}<p class="row-summary">{{ . }}</p>{{ end }} {{ with $authors }}<p class="row-summary">{{ . }}</p>{{ end }}
{{ with .links }}
<span class="item-links publication-links">
{{ range . }}<a href="{{ .url }}" aria-label="{{ .label }} for {{ $pubTitle }}" title="{{ .label }} for {{ $pubTitle }}">{{ .label }}</a>{{ end }}
</span>
{{ end }}
</div> </div>
</article> </article>
{{ end }} {{ end }}

50
nginx.conf Normal file
View File

@@ -0,0 +1,50 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
server_tokens off;
etag on;
gzip on;
gzip_vary on;
gzip_min_length 512;
gzip_comp_level 6;
gzip_types
application/javascript
application/json
application/xml
image/svg+xml
text/css
text/plain
text/xml;
location ~* \.[0-9a-f]{32,}\.(?:css|js)$ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* \.(?:avif|gif|ico|jpe?g|png|svg|webp|woff2?|ttf)$ {
try_files $uri =404;
add_header Cache-Control "public, max-age=2592000, stale-while-revalidate=86400" always;
}
location ~* \.(?:xml|txt)$ {
try_files $uri =404;
add_header Cache-Control "public, max-age=3600, stale-while-revalidate=86400" always;
}
location / {
try_files $uri $uri/ =404;
add_header Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400" always;
}
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-cache" always;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -1,135 +0,0 @@
(() => {
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
function setupToc() {
const headings = $$(".post-content h2[id], .post-content h3[id]");
const links = $$("#TableOfContents a");
if (!headings.length || !links.length) return;
let queued = false;
function update() {
const y = window.scrollY + window.innerHeight * 0.35;
let activeId = headings[0].id;
for (const heading of headings) {
if (heading.offsetTop > y) break;
activeId = heading.id;
}
for (const link of links) {
link.classList.toggle(
"active",
decodeURIComponent(link.hash.slice(1)) === activeId
);
}
queued = false;
}
function queue() {
if (queued) return;
queued = true;
window.requestAnimationFrame(update);
}
window.addEventListener("scroll", queue, { passive: true });
window.addEventListener("resize", queue);
update();
}
function setupPostTags() {
const list = document.querySelector("[data-post-list]");
if (!list) return;
const rows = $$(".post-row", list);
const links = $$("[data-tag]");
const status = document.querySelector("[data-tag-filter-status]");
const clear = status?.querySelector("a");
function setUrl(tag) {
const url = new URL(window.location.href);
if (tag) {
url.searchParams.set("tag", tag);
} else {
url.searchParams.delete("tag");
}
window.history.pushState(null, "", `${url.pathname}${url.search}`);
}
function applyTag(tag) {
for (const link of links) {
link.classList.toggle("active", link.dataset.tag === tag);
}
let visible = 0;
for (const row of rows) {
const tags = JSON.parse(row.dataset.tags || "[]");
const matched = !tag || tags.includes(tag);
row.hidden = !matched;
if (matched) visible += 1;
}
if (status) {
const name = status.querySelector("[data-tag-filter-name]");
if (name) name.textContent = tag ? `[${tag}]` : "";
status.hidden = !tag;
status.classList.toggle("is-empty", visible === 0);
}
}
for (const link of links) {
link.addEventListener("click", (event) => {
event.preventDefault();
const nextTag = link.classList.contains("active") ? null : link.dataset.tag;
applyTag(nextTag);
setUrl(nextTag);
});
}
if (clear) {
clear.addEventListener("click", (event) => {
event.preventDefault();
applyTag(null);
setUrl(null);
});
}
applyTag(new URLSearchParams(window.location.search).get("tag"));
}
function setupClickableRows() {
const rows = $$("[data-row-href]");
function openRow(row) {
const href = row.dataset.rowHref;
if (href) window.location.href = href;
}
for (const row of rows) {
row.addEventListener("click", (event) => {
if (event.target.closest("a")) return;
openRow(row);
});
row.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
openRow(row);
});
row.addEventListener("pointerdown", (event) => {
if (event.target.closest(".item-links a")) return;
row.classList.add("is-pressed");
});
for (const eventName of ["pointerup", "pointercancel", "pointerleave", "blur"]) {
row.addEventListener(eventName, () => row.classList.remove("is-pressed"));
}
}
}
setupToc();
setupPostTags();
setupClickableRows();
})();

BIN
static/og.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB