| layout | default |
|---|---|
| title | Website Dev Notes |
| has_toc | false |
| nav_exclude | true |
| usemathjax | true |
| usetocbot | true |
{: .no_toc }
{: .no_toc .text-delta }
- TOC {:toc}
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
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.
I've been using VS Code with some popular markdown extensions to develop the website.
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)
Including other markdown pages: https://stackoverflow.com/a/41966993/388117.
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/og/led-fade.jpg
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
{{ site.baseurl }} — 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.htmlThis 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>.mdTwo 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).
Standard: write code in fenced blocks (triple backticks) with a required
language token on the opening fence. Use cpp for all Arduino/ESP32
sketches (our house style), and a real token for everything else — javascript,
html, css, json, python, bash, etc. For terminal sessions, program
output, file trees, or other non-source text, use text (highlighters render it
verbatim, and it still satisfies the language requirement).
```cpp
void loop() {
digitalWrite(led, HIGH); // turn the LED on
delay(1000); // wait a second
}
```Do not use Jekyll's {% raw %}{% highlight %}{% endraw %} Liquid tags. They
were fine historically, but the whole site was migrated to fenced blocks (issue
#99) for portability, tooling, and to de-risk a future framework move where
Liquid tags would all need rewriting.
This is enforced in CI. The code-blocks job in
.github/workflows/content-lint.yml runs markdownlint with the scoped config
.markdownlint-code.jsonc:
- MD040 — every fenced block must declare a language token.
- MD046 — code must be fenced, never indented (an indented block can't
carry a language, so it would silently dodge MD040). For a deliberate indented
demo, opt out inline with
<!-- markdownlint-disable MD046 -->…<!-- markdownlint-enable MD046 -->. - MD048 — fences use backticks (
```), not tildes.
The config is intentionally separate from the editor's .markdownlint.jsonc so
the gate only checks these three rules (not the many unrelated style nits the
older content predates). Run it locally with:
npx markdownlint-cli@0.48.0 -c .markdownlint-code.jsonc "**/*.md" --ignore "_site" --ignore "node_modules"Liquid inside a code block. Fenced blocks do not stop Jekyll from
executing Liquid, so if a block must show literal Liquid (e.g. an
if page.usemathjax conditional), wrap it in raw/endraw tags and Jekyll
prints it verbatim instead of running it — see the
Adding LaTeX support example below, which does exactly
this.
This is awesome! Can embed code directly! If this works, it should embed the code Blink.ino directly below.
<script src="http://gist-it.appspot.com/https://github.com/jonfroehlich/arduino/blob/master/Basics/digitalWrite/Blink/Blink.ino?footer=minimal"></script>Update: gist-it.appspot.comappears to be down.
Alternatively, as it seems like gist-it.appspot.com is down, we could use emgithub.com
<script src="https://emgithub.com/embed.js?target=https%3A%2F%2Fgithub.com%2Fjonfroehlich%2Farduino%2Fblob%2Fmaster%2FBasics%2FdigitalWrite%2FBlink%2FBlink.ino&style=github&showBorder=on&showLineNumbers=on&showFileMeta=on&showCopy=on"></script>Same thing without special sauce except copy button:
<script src="https://emgithub.com/embed.js?target=https%3A%2F%2Fgithub.com%2Fjonfroehlich%2Farduino%2Fblob%2Fmaster%2FBasics%2FdigitalWrite%2FBlink%2FBlink.ino&style=github&showCopy=on"></script>Same thing without borders, line numbers, file meta data, and the copy button:
<script src="https://emgithub.com/embed.js?target=https%3A%2F%2Fgithub.com%2Fjonfroehlich%2Farduino%2Fblob%2Fmaster%2FBasics%2FdigitalWrite%2FBlink%2FBlink.ino&style=github"></script>| 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 |
There are a variety of ways to make "call out boxes" in markdown.
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.
NOTE: You could also try a block quote format from link.
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
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.
Adding custom CSS to markdown is relatively straightforward.
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;
}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.
{: .test-css}
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}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:
{% raw %}
{% if page.usemathjax %}
<script type="text/javascript" async
src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
{% endif %}{% endraw %}
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.
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.
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>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 _siteAlternatively, 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.mp4The -movflags +faststart flag moves metadata to the front of the
file so browsers can begin playback before the full download completes.
To create animated gifs, I use https://ezgif.com/.
- Minimal Mistakes
- "Just the Docs". Probably my favorite template that I've evaluated so far

