Last update: 2024-06-23
Motivation
I have had trouble with coming up and keeping with projects without external accountability. I have gotten incrementally better at it in recent years, and I hope a site like this would serve both as a portfolio for job seeking and an outlet to further keep me accountable, motivate me on my projects, and work on my writing.
I wanted to work on a minimalistic site, where I could concentrate solely on frontend design with HTML, CSS, and non-essential JavaScript. Journal posts could use a process on the backend to produce them from more universal files, like Org files.
Design
The site should have a flow from more sparse and general information, to more dense and specific information. This is facilitated by having a minimal page about me personally, a projects overview page, and finally a journals page. In-text links go only from towards more specific parts of the site and never backwards, so the flow is unidirectionally Me -> Projects -> Journal, and only navigation bar can be used to go back upstream.
Introduction site only has a collage image visible to set up the mood, which will be on the background in other sections. About page shortly describes my interests and vibes; the content on this site. About page has links to relevant projects. This projects overview has project-specific descriptions about motivations, progress, aims, and results respectively. These descriptions then have links to relevant journal posts, which go into technical detail.
All pages share the same basic layout. The layout is a 3-partition navigation bar - sidebar/ToC - content layout. The sidebar should be collapsible on narrow screens to get closer to 80 characters per line optimum.
These sketches visualize it in landscape and portrait:
Visual design around the prose should avoid harsh contrasts and have a warm, old manuscript-like look. Dark mode is not required.
Implementation
The site is primarily driven by its CSS file. HTML merely contains the text and links with minimal formatting without customizations. The index.html
introductory page contains the essential HTML structure shared by all pages:
<!doctype html> <html lang="en-US"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Tatu Heikkilä</title> <link rel="stylesheet" href="portfolio.css" /> <script src="portfolio.js"></script> </head> <body> <div id="page"> <header> <a href="index.html" class="currentNav">Tatu</a> <a href="projects.html" class="navButton">Projects</a> <a href="journal.html" class="navButton">Journal</a> </header> <main> <toc> <p>Email: <a href="mailto:public@tazca.com">public@tazca.com</a></p> </toc> <content> <p><i>Technology is easy, communication is hard: don't reinvent the wheel.</i></p> <p>Hey, I'm Tatu Heikkilä. I have a professional interest in <a href="projects.html#MSc work">software engineering</a>, <a href="projects.html#Systems administration">IT administration</a>, and <a href="projects.html#MSc work">associated communications</a>. Curiosity and research comes natural to me, and my academic fascination has revolved both around foundational SWE knowledge and around effectively communicating knowledge and decisionmaking.</p> <p>On my free time, I <a href="projects.html#3D design and printing">model and print functional and sculptural 3D prints</a>, paint watercolors and make watercolor paint. <!-- <a href="projects.html#Paintmaking">make watercolor paint</a>. --> I volunteer at a <a href="https://kameraseuravastavalo.fi/en/people/">local photography club</a>, where I instruct guided darkroom workshops, maintain the darkroom, take part in organizing other events, and organize meetings as head of the board.</p> <p>My academic theses were written in Finnish, but include an English abstract. An English examination into my MSc thesis can be found <a href="projects.html#MSc work">among my projects</a>. <br>BSc: <a href="https://urn.fi/URN:NBN:fi:tuni-202104273752">Working environment serving distributed agile software engineering</a><br> MSc: <a href="https://urn.fi/URN:NBN:fi:tuni-202404305017">Comprehensible programming</a></p> </content> </main> </div> </body> </html>
The site uses a Bembo-like font for non-code text, and Fantasque Sans Mono for code. Fantasque Sans is a fairly opinionated font, but I like its looped k's very much.
@font-face { font-family: "ET Book"; src: url("assets/etbookot-roman-webfont.woff2") format('woff2'); } @font-face { font-family: "ET Book"; src: url("assets/etbookot-italic-webfont.woff2") format('woff2'); font-style: italic; } @font-face { font-family: "ET Book"; src: url("assets/etbookot-bold-webfont.woff2") format('woff2'); font-weight: bold; } @font-face { font-family: 'Fantasque Sans Mono'; src: url('assets/FantasqueSansMono-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; }
Lab coordinate system is way easiest to reason and intuit, and are also technically the best choice, so LCh is used. Site's essential color scheme form around the tan (80°) background and its opposite color blue (260°). The slight departures from this are the button borders with turquoise (207°, PG50), buttons at yellow-green 110°, and button texts with dark blood-red (30°, PR179).
:root { --bg-color: lch(97% 20 80 / 0.96); --button-color: linear-gradient(150deg, lch(80% 45 110 / 0.15), lch(65% 45 110 / 0.15)); --button-hover-color: linear-gradient(150deg, lch(80% 45 110 / 0.25), lch(65% 45 110 / 0.25)); --code-bg-color: lch(98% 35 85); --in-page-link-color: lch(20% 90 260); --mono-in-text-color: lch(20% 20 260); --text-on-button-color: lch(25% 30 30); --text-on-bg-color: lch(3% 10 260); --toc-bg-color: radial-gradient(lch(91% 25 80 / 0.2), lch(98% 25 80 / 0.96)); } *{ scroll-behavior: smooth !important; }
CSS defining the actual structure of the page doesn't do any immense or hacky tricks. CSS Grid L2 partitions the page, first in two rows.
body { height: 100%; background-color: var(--bg-color); font-family: "ET Book"; font-size: 1.15em; line-height: 150%; } #page { display: grid; width: 100%; height: 98vh; grid-template-rows: min-content auto; grid-template-areas: "header" "main"; justify-content: center; } #page > header { grid-area: header; margin-bottom: 0.5em; }
The main
then partitions it further into two columns, and sets up a separate background image handling for the intro
class for index.html
.
#page > main { background-image: linear-gradient(var(--bg-color), var(--bg-color)), url("work.webp"); background-repeat: no-repeat; background-size: contain; } #page > main.intro { background-image: url("work.webp"); }
#page > main > toc { grid-area: toc; background: var(--toc-bg-color); border-radius: 0.5em; }
The rest of the structure is straightforward, dealing with how the structure flows on narrower and wider displays. On wider displays, the sidebar needs to stay with the reader as the header
and content
scroll up. This is done with align-self: start
, position: sticky
and top: 0
. On narrow displays, ToC is only shown at top.
#page > main > content { grid-area: content; padding-left: 1em; padding-right: 1em; } #page > main > content svg, #page > main > content img { width: 95%; } @media (min-width: 72em) { #page > main > toc { width: 15em; } #page > main > content { width: 40em; padding-left: 1em; padding-right: 1em; } #page > main { grid-area: main; display: grid; grid-template-columns: auto 1fr; grid-template-areas: "toc content"; } #page > main > toc { grid-area: toc; background: var(--toc-bg-color); border-radius: 0.5em; padding-right: 1em; align-self: start; position: sticky; top: 0; } } @media (max-width: 72em) { #page > main > content { width: auto; max-width: 35em; padding-left: 0.5em; padding-right: 0.5em; } #page > header { width: auto; max-width: 35em; } #page > main { grid-area: main; display: grid; grid-template-rows: auto 1fr; grid-template-areas: "toc" "content"; } #page > main > toc { grid-area: toc; background: var(--toc-bg-color); border-radius: 0.5em; } } #page > main > content p { text-align: justify; color: var(--text-on-bg-color); } #page > main > content ul { } #page > main > toc ul { padding-inline-start: 1em; list-style-type: none; }
A separate a.active
class is needed for highlighting current section in ToC with a bit of JavaScript.
#page > main > toc a { text-decoration-line: none; color: var(--in-page-link-color); &:visited { color: var(--in-page-link-color); } } #page > main > toc a.active { color: var(--text-on-button-color); &:visited { color: var(--text-on-button-color); } }
window.addEventListener('DOMContentLoaded', () => { const anchors = document.querySelectorAll('h1, h2, h3, h4'); const links = document.querySelectorAll('toc a'); window.addEventListener('scroll', (event) => { if (typeof(anchors) != 'undefined' && anchors != null && typeof(links) != 'undefined' && links != null) { let scrollTop = window.scrollY; // highlight the last scrolled-to: set everything inactive first links.forEach((link, index) => { link.classList.remove("active"); }); // then iterate backwards, on the first match highlight it and break for (var i = anchors.length-1; i >= 0; i--) { if (scrollTop > anchors[i].offsetTop - 75) { links[i].classList.add('active'); break; } } } }); });
The buttons have a transition for a hover effect.
.navButton { background: var(--button-color); border: solid 0.075em; border-left: solid 0.05em; border-top: solid 0.05em; border-color: lch(80% 20 207 / 0.5); border-radius: 0.5em; color: var(--text-on-button-color); display: inline-block; font-size: 1.5em; min-width: 5rem; padding: 0.4em 0.8em; text-align: center; text-decoration: none; transition: background-color .3s ease; &:focus, &:hover { background: var(--button-hover-color); } } .currentNav { background: transparent; border-radius: 0.5em; color: var(--text-on-button-color); display: inline-block; font-size: 1.5em; min-width: 5rem; padding: 0.4em 0.8em; text-align: center; text-decoration: none; transition: background-color .3s ease; }
Special formatting for links that point inside the site, that is relative links, is done with a special selector :not
. All absolute links will have e.g. git://
.
a:not([href*='//']){ text-decoration-line: none; }
All in all, nothing too complicated, just a nice, not very fragile, extensible at small scale, site. However, I want to write journal posts in Org to transclude relevant code blocks, easy linking, and drawing, and that requires some setup.
Org exports
The default org-html-export-as-html
has structure that is not compatible with the layout this site uses. So, we need to derive our own export function.
;; custom org HTML template (require 'ox) (org-export-define-derived-backend 'journal-html 'html :menu-entry '(?j "to journal HTML" my-org-html-export-to-html) :translate-alist '((inner-template . journal-html-inner-template) (template . journal-html-template)) ) (defun journal-html-template (contents info) (concat "<!DOCTYPE html>\n" (format "<html lang=\"%s\">\n" (plist-get info :language)) "<head>\n" (format "<meta charset=\"%s\">\n" (coding-system-get org-html-coding-system 'mime-charset)) (format "<title>%s</title>\n" (org-export-data (or (plist-get info :title) "") info)) (format "<meta name=\"author\" content=\"%s\">\n" (org-export-data (plist-get info :author) info)) "<link href=\"../portfolio.css\" rel=\"stylesheet\" style=\"text/css\" />\n" "<script src=\"../portfolio.js\"></script>\n" "</head>\n" "<body>\n" "<div id=\"page\">\n" "<header>\n" " <a href=\"../index.html\" class=\"navButton\">Tatu</a> <a href=\"../projects.html\" class=\"navButton\">Projects</a> <a href=\"../journal.html\" class=\"navButton\">Journal</a>\n" (format "<div class=\"currentNav\">%s</div>\n" (org-export-data (or (plist-get info :title) "") info)) "</header>\n" contents "</div>" "</body>\n" "</html>\n")) (defun journal-html-inner-template (contents info) "Return body of document string after HTML conversion. CONTENTS is the transcoded contents string. INFO is a plist holding export options." (concat "<main>\n" ;; Table of contents. (let ((depth (plist-get info :with-toc))) (when depth (my-org-html-toc depth info))) ;; Document contents. "<content>\n" contents "</content>\n" "</main>\n") ) (defun my-org-html-export-to-html (&optional async subtreep visible-only body-only ext-plist) "Export current buffer to a HTML file. If narrowing is active in the current buffer, only export its narrowed part. Return output file's name." (interactive) (let* ((extension (concat "." (or (plist-get ext-plist :html-extension) org-html-extension "html"))) (file (org-export-output-file-name extension subtreep)) (org-export-coding-system org-html-coding-system)) (org-export-to-file 'journal-html file async subtreep visible-only body-only ext-plist))) (defun my-org-html-toc (depth info &optional scope) "Build a table of contents. DEPTH is an integer specifying the depth of the table. INFO is a plist used as a communication channel. Optional argument SCOPE is an element defining the scope of the table. Return the table of contents as a string, or nil if it is empty." (let ((toc-entries (mapcar (lambda (headline) (cons (org-html--format-toc-headline headline info) (org-export-get-relative-level headline info))) (org-export-collect-headlines info depth scope)))) (when toc-entries (let ((toc (concat "<toc>\n" (format "<p>Last update: %s</p>\n" (org-export-data (plist-get info :date) info)) (org-html--toc-text toc-entries) "</toc>\n"))) (if scope toc toc))))) (add-to-list 'load-path "~/.emacs.d/nonmanaged/ob-yaml/") (require 'ob-yaml) ;; Load Babel and tangle on save ;; Allow in-place language execution for Latex, Python, and shell languages. (org-babel-do-load-languages 'org-babel-load-languages '((latex . t) (python . t) (shell . t) (yaml . t)))
Finally, Org exports use code
and pre
, which require some overflow, color and font formatting.
code { /* monospace-emphasized words; mainly in Org exports */ color: var(--mono-in-text-color); font-family: "Fantasque Sans Mono", monospace; font-size: 0.9em; } .org-src-container { overflow-x: auto; margin-top: 0.75em; margin-bottom: 0.75em; background-color: var(--code-bg-color); } pre { /* Specific to Org exported journals: */ background-color: var(--code-bg-color); font-family: "Fantasque Sans Mono", monospace; font-size: 0.8em; line-height: 125%; padding: 1em; }