A static HTML page that mimics a LinkedIn company feed for the Sydney
University Project Management Society (PMSoc), styled to the SoPM x PMSoc
BBQ flyer palette. Built as a static snapshot (not a live LinkedIn embed) so
it can be embedded via <iframe> in Canvas (Instructure) without CSP/framing
issues.
PMSoc-LinkedIn.html (repo root) — this is what GitHub
Pages serves and what Canvas iframes point at.https://usydsopm.github.io/canvas/PMSoc-LinkedIn.html
(once GitHub Pages is enabled on this repo — see below).PMSoc-LinkedIn.html <- generated output, do not hand-edit
build/
posts.json <- the 5 posts shown, newest first
canvas_template.html <- page shell/CSS/JS (hand-edit this for style changes)
build_canvas_html.py <- reads posts.json + template -> writes PMSoc-LinkedIn.html
assets/ <- base64 data-URI text files (post photos, logos, font)
Each post in posts.json has a real date_iso (e.g. "2026-09-05"), not a
frozen “2 days ago” string. The page computes “X days/months/years ago” in
JavaScript every time it’s opened, so it never goes stale — you only need to
rebuild when the actual list of posts changes, not on a schedule for dates
alone.
build/assets/
(resize to ~480px wide, JPEG quality ~78, to keep the page small — a data
URI string starting data:image/jpeg;base64,..., no line breaks).build/posts.json:
{
"date_iso": "YYYY-MM-DD",
"title": "...",
"text": "...",
"img_asset": "postN_datauri.txt",
"url": "https://www.linkedin.com/feed/update/urn:li:activity:.../"
}
build/assets/.python3 build/build_canvas_html.py
PMSoc-LinkedIn.html, build/posts.json, and any new/removed
files under build/assets/.GitHub Pages can’t be turned on via this repo’s automation token (it doesn’t
have the pages permission). A repo admin needs to enable it once:
Settings → Pages → Build and deployment → Source: “Deploy from a branch” →
Branch: main / (root) → Save. After that, every push to main updates
the live Pages URL automatically — no further manual steps.
In the Canvas Rich Content Editor, switch to the HTML view and add:
<iframe src="https://usydsopm.github.io/canvas/PMSoc-LinkedIn.html"
style="width:100%;height:1400px;border:0;"></iframe>
Adjust height to taste. Check with USYD’s Canvas/LMS admin team first that
custom iframe embeds from github.io are permitted (some Canvas instances
restrict embeddable domains to an allowlist).
A static HTML page styled to look like the real pmsoc.usyd Instagram
profile: a profile header (avatar, Follow/Message buttons, post/follower/
following counts, bio), a Posts/Reels/Tagged tab row, and a 3-column post
grid on the left mirroring the real grid order — the profile’s 3 currently
pinned posts first, then its 9 most recent posts. Instagram lets an
account pin up to 3 posts to the top of its grid regardless of date, so
those 3 are kept separate from “most recent” and can be much older (the
current pinned set includes one from January). Colours match the authentic
Instagram look (white background, black/grey text, Instagram blue) rather
than the SoPM/PMSoc teal branding used on the LinkedIn widget above. Built
as a static snapshot (not a live Instagram embed), so it can be embedded via
<iframe> in Canvas (Instructure) without CSP/framing issues.
To the right of the grid is a detail panel showing whichever post was last
clicked, bigger, with its full caption — clicking any of the 9 grid tiles
swaps the panel’s photo/caption/date in place (via JavaScript) rather than
navigating away, so students can browse without leaving Canvas. A plain
click only ever updates the panel; middle-click or cmd/ctrl-click on a tile
still opens the real Instagram post in a new tab, since each tile stays a
real <a href> underneath. Every action icon (like/comment/send/save) and
the “View post on Instagram” link in the detail panel, and the Follow/
Message buttons and avatar/username in the header, always link out to the
real pmsoc.usyd profile or that specific post — nothing in the header or
detail panel is decorative-only.
PMSoc-Instagram.html (repo root) — this is what GitHub
Pages serves and what Canvas iframes point at.https://usydsopm.github.io/canvas/PMSoc-Instagram.html
(once GitHub Pages is enabled on this repo — see the GitHub Pages section
above; the same one-time setup covers both widgets).PMSoc-Instagram.html <- generated output, do not hand-edit
build/
ig_posts.json <- the 9 posts shown (3 pinned + 6 recent), in grid order
ig_profile.json <- profile header data (bio, follower/following/post counts, link)
canvas_template_ig.html <- page shell/CSS/JS (hand-edit this for style changes)
build_canvas_html_ig.py <- reads ig_posts.json + ig_profile.json + template -> writes PMSoc-Instagram.html
assets/ <- base64 data-URI text files (post photos, and for
reels their actual video footage; reuses
logo_datauri.txt from the LinkedIn widget's asset set
as the profile avatar)
Same approach as the LinkedIn widget: each post in ig_posts.json has a real
date_iso (e.g. "2026-09-05"), and the detail panel computes “X days/
months/years ago” in JavaScript every time the page is opened, so it never
goes stale. A pinned post’s date is still its real post date (not “today”)
— pinning only affects its position in the grid, matching how Instagram
itself treats pinned posts.
ig_posts.json is an array of exactly 9 posts (a 3x3 grid), in the same
order the real grid shows them: the profile’s currently pinned posts first
(in their pinned order), then its 6 most recent posts after them, newest
first.
build/assets/
(resize to ~480px wide, JPEG quality ~78, to keep the page small — a data
URI string starting data:image/jpeg;base64,..., no line breaks).build/ig_posts.json, one object per post:
{
"date_iso": "YYYY-MM-DD",
"caption": "...",
"img_asset": "postN_ig_datauri.txt",
"url": "https://www.instagram.com/p/.../",
"type": "post",
"pinned": false
}
Set "type" to "reel" for a reel (small play-icon badge in the grid),
"carousel" for a multi-photo/video post (small stacked-squares badge),
or "post" (or omit it) for a normal single-photo post. Set "pinned" to
true for the profile’s currently pinned posts (adds a small pin badge in
the grid) — there should be exactly 3 of these, first in the array. For a
reel, also add "video_asset" so it actually plays in the widget instead
of showing a static thumbnail — see “Making a reel’s video play inline”
below.
python3 build/build_canvas_html_ig.py
PMSoc-Instagram.html, build/ig_posts.json, and any
new/removed files under build/assets/.A reel’s grid tile can play its actual video in the detail panel (with normal browser controls — play/pause, scrubbing, volume, fullscreen) instead of just showing a static thumbnail and linking out. This works by embedding the video itself as a base64 data URI, the same technique used for photos, so the page stays fully self-contained and works offline/inside Canvas.
blob: URL backed by MediaSource
Extensions, which can’t be fetch()-ed directly — the reliable way to
get a real file out of it is to re-record the decoded playback):
<video> element load.video.captureStream() to get a live MediaStream, and feed it to a
MediaRecorder (video/webm;codecs=vp9,opus if supported) with
recorder.start().video.play() from a script, which Instagram’s own
player can interrupt with a competing pause()).Blob, convert it to base64 (FileReader.readAsDataURL), and save it
as data:video/webm;base64,....ffmpeg -i in.webm -c copy out.webm — the
browser’s own recorded file otherwise carries a slightly wrong/rounded
duration in its container header (the video content itself is fine).build/assets/ (e.g.
post9_video_datauri.txt), matching the pattern used for photos."video_asset": "post9_video_datauri.txt" to that post’s entry in
build/ig_posts.json.python3 build/build_canvas_html_ig.py and commit the new
build/assets/*.txt file along with the usual files.Size tradeoff: embedding real video is much heavier than a photo — a
~10-15 second reel typically adds a few MB to PMSoc-Instagram.html (and to
the git repo). This is a deliberate tradeoff for a widget that has to be a
single static file with no external video host; keep an eye on overall page
size if more than a couple of reels get this treatment, and prefer trimming
recording length/quality over skipping the re-mux step if a file feels too
large.
build/ig_profile.json holds everything in the header above the grid:
display_name, handle, bio (use \n for line breaks — rendered with
white-space: pre-line), link_text / link_url (the bio link), posts_count
/ followers_count / following_count, and profile_url / reels_url /
tagged_url (used by the avatar, username, Follow/Message buttons, and the
Posts/Reels/Tagged tabs). mention_handle / mention_url control the one
@handle in the bio that gets turned into a link (e.g. @sydney_uni).
Update these values periodically to keep the follower/following/post counts
roughly current, then re-run python3 build/build_canvas_html_ig.py.
In the Canvas Rich Content Editor, switch to the HTML view and add:
<iframe src="https://usydsopm.github.io/canvas/PMSoc-Instagram.html"
style="width:100%;height:1080px;border:0;"></iframe>
Adjust height to taste (the page lays out the grid and detail panel
side by side above ~760px wide and stacks them on narrower screens/iframes
(with a horizontal divider instead of the vertical one),
so a narrower iframe will need extra height). Check with USYD’s Canvas/LMS
admin team first that custom iframe embeds from github.io are permitted
(some Canvas instances restrict embeddable domains to an allowlist).
Same pmsoc.usyd profile header + bio as the grid widget above, but instead of a 3x3 grid + side detail panel, every post is rendered as a full-size card (photo or playable video, action icons, date, full caption, “View on Instagram” link) stacked vertically so the whole thing scrolls like a real Instagram feed. No pinned-post special-casing here — pinning only affects the real grid’s tile order, so this view is purely chronological, newest first.
Shares its data source with the grid widget: build/ig_posts.json and
build/ig_profile.json. Update posts/profile the same way described in the
“PMSoc Instagram Feed Widget” section above, then re-run both build
scripts (build_canvas_html_ig.py and build_canvas_html_ig_feed.py) so
the grid and feed widgets stay in sync.
PMSoc-Instagram-Feed.html (repo root).https://usydsopm.github.io/canvas/PMSoc-Instagram-Feed.htmlPMSoc-Instagram-Feed.html <- generated output, do not hand-edit
build/
ig_posts.json <- same 9 posts as the grid widget (shared)
ig_profile.json <- same profile header data (shared)
canvas_template_ig_feed.html<- page shell/CSS/JS for this variant
build_canvas_html_ig_feed.py<- reads ig_posts.json + ig_profile.json + template -> writes PMSoc-Instagram-Feed.html
assets/ <- shared with the grid widget (same post photos/video/logo)
From the repo root:
python3 build/build_canvas_html_ig_feed.py
Commit PMSoc-Instagram-Feed.html alongside whatever ig_posts.json /
ig_profile.json / build/assets/ changes prompted the rebuild (the same
commit that updates the grid widget can cover this one too).
<iframe src="https://usydsopm.github.io/canvas/PMSoc-Instagram-Feed.html"
style="width:100%;height:1400px;border:0;"></iframe>
This widget is a single column (max-width 470px, centered) rather than a
two-column layout, so there’s no narrow-screen breakpoint to worry about —
just adjust height to however many posts you want visible before the
iframe itself needs scrolling (the page also scrolls internally, so a
shorter iframe still works, it just shows less at once).