Website Dev Notes
Table of Contents
- Running the website
- Deployment (GitHub Actions)
- VS Code
- Embedding markdown content on a page
- SEO and social cards (per-page front matter)
- Accessibility and content QA
- Code highlighting
- Styling tables
- Make a Note (Call Out Box)
- How to add custom CSS to markdown
- LaTeX
- Disqus
- Troubleshooting video playback locally
- Tools
Running the website
Assuming you have the prerequisite libraries and software infrastructure (e.g., Jekyll)—see our website development setup guide here—you can open terminal in VSCode and type:
> bundle exec jekyll serve
Deployment (GitHub Actions)
The live site at https://makeabilitylab.github.io/physcomp/ is built and published by the GitHub Actions workflow in .github/workflows/jekyll.yml. On every push to main (and on manual runs from the Actions tab), the workflow runs bundle exec jekyll build on a clean Ubuntu runner and deploys the resulting _site/ to GitHub Pages with actions/deploy-pages.
This replaced the older “Deploy from a branch” GitHub Pages build, which only ran whitelisted plugins and no custom build steps. Building in Actions lets us run custom Jekyll plugins, inline source code at build time, and add content lint/test gates. See issue #98.
For the Actions deploy to publish, the repo’s Settings → Pages → Build and deployment → Source must be set to GitHub Actions (not “Deploy from a branch”). The workflow still installs the same github-pages gem from the Gemfile, so the built output matches the previous branch-based build.
VS Code
I’ve been using VS Code with some popular markdown extensions to develop the website.
Extensions
I have the following extensions installed for VS Code:
- Code Spell Check 1.8.0 (1.1m downloads)
- Markdown All in One 2.7.0 (1.2m downloads)
- markdownlint 0.34.0 (1.5, downloads)
- Paste Image 1.0.4 (45K): Allows user to paste images in clipboard using
alt-cmd-v(Mac) andctrl-alt-v(Windows)
Embedding markdown content on a page
Including other markdown pages: https://stackoverflow.com/a/41966993/388117.
SEO and social cards (per-page front matter)
The site uses jekyll-seo-tag (pulled in via the github-pages gem) to emit <meta> description, Open Graph, and Twitter-card tags. Every lesson page should set two front-matter keys so search results and link previews (Slack, iMessage, Discord, X, LinkedIn, Facebook) are page-specific instead of falling back to the generic site description and card.
---
layout: default
title: L4: Fading an LED
description: "Smoothly fade an LED on and off with Arduino's analogWrite() and pulse-width modulation (PWM), controlling output voltage at fine gradations beyond just HIGH/LOW."
image: /arduino/assets/movies/Arduino_LEDFade_Pin3.gif
nav_order: 4
parent: Output
---
description: — a 1–2 sentence summary, ideally ≤ 160 characters (search engines truncate the visible snippet around there). Write it for a human skimming search results: lead with the concrete thing they’ll learn/build. Wrap it in double quotes so : and () don’t break the YAML.
image: — the social-card preview. Use a root-absolute path (leading /, no /physcomp — jekyll-seo-tag prepends site.url + baseurl automatically), or a full external URL. It must be a static image — social crawlers never render video as the card. Pick, in order of preference:
- The page’s own hero image, if it’s a
.png/.jpg(or a.gifwhose first frame reads well — platforms show GIFs as a static first frame). - For pages whose hero is an MP4
<video>: runscripts/generate_og_posters.py, which usesffmpeg’sthumbnailfilter to extract a representative still into<module>/assets/og/<lesson>.jpgand setsimage:for you (dry run by default; pass--run, or a list of.mdfiles to limit scope). Requiresffmpegon PATH. - For pages whose hero is a YouTube embed: the thumbnail
https://img.youtube.com/vi/<VIDEO_ID>/hqdefault.jpg(hqdefaultalways exists;maxresdefaultdoes not). - If there’s no good static image (e.g. a section index page), omit
image:— the generic site card (/assets/images/physcomp-og-card.jpg, set in_config.ymldefaults) is used automatically.
The ideal OG image is 1200×630 (1.91:1); existing figures rarely match this exactly, which is fine for now. A future improvement (once we’re off the github-pages gem) is auto-generating branded 1200×630 cards with the page title overlaid.
To verify after a build, grep the output, e.g.:
grep -oiE '<meta (name|property)="(og:image|og:description|description)" content="[^"]*"' _site/arduino/led-fade.html
New pages and enforcement
This is required, not optional. A CI check (scripts/check_seo_frontmatter.py, run by the Content lint workflow on every pull request) fails the PR if any published page is missing description:. So when you author a new lesson, start from this minimal front matter:
---
layout: default
title: "Your Lesson Title"
description: "One or two sentences (≤160 chars) on what the reader learns or builds."
# image: ← add per the rules above; for an MP4 hero, run the poster script (below) instead
parent: Your Section
nav_order: 1
---
If a page isn’t ready to publish, mark it nav_exclude: true (or search_exclude: true) and the check skips it until you publish it. The image: key is advisory — the check only reminds you when an MP4-hero page has no poster yet.
For a new page whose hero is an MP4 <video>, generate its social poster (and have image: set for you) with:
python scripts/generate_og_posters.py --run <module>/<your-page>.md
Accessibility and content QA
Two CI checks in the Content lint workflow guard accessibility and link health. They are complementary — neither subsumes the other:
| Check (job) | Tool | Runs against | Catches |
|---|---|---|---|
media-a11y | scripts/check_a11y.py | Markdown source | YouTube <iframe> without title=, <video> without aria-label, image with empty/missing alt |
link-check | html-proofer | built _site/ | broken internal links, broken #anchors, missing alt attribute, malformed HTML |
We use the off-the-shelf html-proofer for the commodity problem (links/HTML); check_a11y.py only covers the source conventions html-proofer can’t see (it permits empty alt="" as “decorative” and has no notion of iframe titles or video labels).
Authoring rules (all enforced):
- YouTube embeds — give the
<iframe>atitle=describing the video, e.g.<iframe title="An RGB LED fading between colors" src="https://www.youtube.com/embed/…" …>. <video>heroes/demos — add anaria-label=describing the clip.- Images — informative images need descriptive alt:
. Don’t start with “Image of”; don’t dump the filename. A genuinely decorative image may use emptyalt="", butcheck_a11yflagsin source, so make alt explicit. - Drafts / WIP — a page that intentionally references not-yet-created assets should be
nav_exclude: true(themedia-a11ycheck skips drafts). If a published page must keep a not-yet-added asset, add its built path to the--ignore-fileslist incontent-lint.ymlwith a tracking issue (don’t ignore silently).
Running html-proofer locally. It needs libcurl (via typhoeus/ethon), which isn’t present on stock Windows — so it runs in CI (Ubuntu) but may fail to even load on native Win11 (Could not open library 'libcurl'). Options: rely on CI; run it under WSL2 or macOS (libcurl present, matches CI); or on native Win11 install it once with ridk exec pacman -S mingw-w64-ucrt-x86_64-curl. To run it (after a build):
bundle exec jekyll build --baseurl "/physcomp"
gem install html-proofer -v 5.0.9
htmlproofer ./_site --disable-external --swap-urls "^/physcomp:" \
--ignore-files "/\/signals\/.+\/index\.html/,/\/signals\/IntroTo[A-Za-z]+\.html/,/\/arduino\/accel\.html/,/\/esp32\/capacitive-touch\.html/"
check_a11y.py is pure Python (no libcurl) and runs anywhere: python scripts/check_a11y.py (add --summary for counts, --ci to fail on any issue).
Code highlighting
Using Jekyll’s highlight functionality
This is a test.
void loop() {
digitalWrite(led, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(led, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}Using Markdown’s tickmarks
void loop() {
digitalWrite(led, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(led, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
Using gist-it.appspot.com to embed code directly from GitHub
This is awesome! Can embed code directly! If this works, it should embed the code Blink.ino directly below.
Update: gist-it.appspot.comappears to be down.

Using emgithub.com to embed code directly from GitHub
Alternatively, as it seems like gist-it.appspot.com is down, we could use emgithub.com
Same thing without special sauce except copy button:
Same thing without borders, line numbers, file meta data, and the copy button:
Styling tables
| Column 1 | Column 2 |
|---|---|
border-bottom-right-radius | Defines the shape of the bottom-right |
To set the size of a table, we can use inline spans.
| text | description |
|---|---|
border-bottom-right-radius | Defines the shape of the bottom-right |
Make a Note (Call Out Box)
There are a variety of ways to make “call out boxes” in markdown.
Option 1: Two horizontal lines
The simplest and most universal way—recommended by this Stack Overflow post—is to draw two horizontal lines surrounding the content like this:
NOTE
It works with almost all markdown flavours (the below blank line matters). This is from link.
Option 2: Use block quotes
NOTE: You could also try a block quote format from link.
Option 3: Use tabs
This version is using tabs:
Start on a fresh line
Hit tab twice, type up the content
Your content should appear in a box. However, doesn't appear to now support markdown. For example, **this** should be bold. However, I can still use html it appears? For example, <b>this</b> is bold? Or maybe not! So, perhaps this is treated as a code block or something...
This version is using tick marks (rather than tabs) but it should render in the same way:
Use tickmarks
Option 4: Custom CSS
But if we want to do something more complicated, it’s going to take custom css. For example, I quite like the call-out boxes on Boser’s Berkeley teaching page IoT49:

This would take some experimentation and custom css to get right, however.
How to add custom CSS to markdown
Adding custom CSS to markdown is relatively straightforward.
Modify custom.css
First, add your custom CSS to assets\css\custom.css. Let’s add the following new CSS class called .test-css:
.test-css{
font-size: 14 pt;
font-family: 'Courier New', Courier, monospace;
}
Use custom CSS
Now, let’ use this new CSS class to style our markdown.
This paragraph is now using the .test-css style. We do this by using this syntax {: .test-css} below the element we want styled.
So, the markdown looks like this:
This paragraph is now using the `.test-css` style. We do this by using this syntax `{: .test-css}` below the element we want styled.
{: .test-css}
LaTeX
Adding LaTeX support
After a bit of experimentation, I got LaTeX to work using a remote Jekyll template and GitHub Pages. Steps:
- I largely followed the advice from this blog post
- Since I’m currently using
remote_theme: pmarsceill/just-the-docs, I was a bit confused about how to make local configuration changes since most online blogs, forum posts talk about editing content in the_includesfolder; however, I didn’t have this in my local dev environment. So, what to do? - I manually made a
_includesfolder with the filenamehead_custom.htmland put in there:
{% if page.usemathjax %}
<script type="text/javascript" async
src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
{% endif %}Using LaTeX on markdown pages
On pages where you want to use LaTeX, then add usemathjax: true to the header content
Here’s a test LaTeX equation. If it works, this should render correctly.
\[\frac{\partial f(y)}{\partial x} = \frac{\partial f}{\partial y} \times \frac{\partial y}{\partial x}\]Because I’m forever a LaTeX n00b, I found this online WYSIWYG LaTeX math editor. For a discussion of other WYSIWYG editors, see this Stack Overflow post.
Disqus
I tried to get Disqus working with Jekyll by following their official instructions; however, it just wouldn’t work and I didn’t have significant time to try and troubleshoot/debug. I kept getting the non-help error printed out in Chrome’s dev tool console:
Uncaught SyntaxError: Unexpected end of input led-on.html:1
And in FireFox:
SyntaxError: missing } after function body led-on.html:1:754
note: { opened at line 1, column 287 led-on.html:1:287
But I thought I’d try once more and I came across a blog posting that had the solution The “Universal Code” that Disqus has you embed on your website includes // single line comments and /* multi-line */ comments. However, when Jekyll builds the website, it places the entire produced html on one line (read: not beautified), so the single-line comments disrupt the code. Here’s the code that doesn’t work.
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC
* VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT:
* https://disqus.com/admin/universalcode/#configuration-variables */
var disqus_config = function () {
this.page.url = document.location.href; // Replace PAGE_URL with your page's canonical URL variable
this.page.identifier = document.location.pathname; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
};
(function () { // DON'T EDIT BELOW THIS LINE
var d = document,
s = d.createElement('script');
s.src = 'https://physical-computing.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by
Disqus.</a></noscript>
</div>And here’s the code that does work with the single line comments replaced with multi-line comments:
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC
* VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT:
* https://disqus.com/admin/universalcode/#configuration-variables */
var disqus_config = function () {
this.page.url = document.location.href; /* Replace PAGE_URL with your page's canonical URL variable */
this.page.identifier = document.location.pathname; /* Replace PAGE_IDENTIFIER with your page's unique identifier variable */
};
(function () { /* DON'T EDIT BELOW THIS LINE */
var d = document,
s = d.createElement('script');
s.src = 'https://physical-computing.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by
Disqus.</a></noscript>
</div>Troubleshooting video playback locally
Jekyll’s built-in WEBrick server doesn’t support HTTP range requests, which browsers need to stream <video> elements. If videos fail to load or play, try serving the built site with a different local server:
bundle exec jekyll build
python3 -m http.server 4000 --directory _site
Alternatively, npx serve _site works well. Both support range requests and handle large media files reliably.
If videos are still slow, check file sizes. Compress large .mp4 files with ffmpeg:
ffmpeg -i input.mp4 -crf 28 -preset fast -movflags +faststart output.mp4
The -movflags +faststart flag moves metadata to the front of the file so browsers can begin playback before the full download completes.
Tools
Making animated gifs
To create animated gifs, I use https://ezgif.com/.
Templates
- Minimal Mistakes
- “Just the Docs”. Probably my favorite template that I’ve evaluated so far