← Back to portfolio

Case study · Qingban youth STEM platform

Fifty-nine lessons, and a record of who finished

Qingban is a free STEM platform for left-behind children and the children of migrant workers: four course tracks, 59 recorded lessons, no fee, and no account needed just to watch. The videos were not the slow part. Making the word finished mean something a parent, a teacher or a funder could check was the slow part.

Role
Founder and independent developer: server build, CMS secondary development, templates, course publishing, certificate system, ongoing operation
Stack
PbootCMS 3.2.16 · PHP 8.2 · MySQL · Nginx · Bootstrap 4 · jQuery
Scope
59 videos · 40 columns (20 Chinese, 20 English) · 152 entries · 15 bilingual templates
Code
9,213 lines written on top of PbootCMS: 1,702 lines of PHP plus 7,511 lines of front end
Status
Live and running, free and open to any student
Evidence
The live site, a v1.0 build document and a user manual for students and administrators

01A video goes up, then what

For a child in a county town with no coding class and no one at home who writes code, a recorded lesson is already a real improvement. The gap this platform is aimed at is not enthusiasm. It is the next question, which arrives immediately: is anyone actually getting through the material, or are the videos being opened and closed?

A view count cannot answer that. Autoplay counts. Dragging the scrubber to the end counts. Opening a lesson and closing it ten seconds later counts. On its own, the number tells an operator that the page works, and it tells a parent nothing.

Qingban home page: course grid with lesson cards
The home page. Sixteen lesson cards per screen, each with a cover, a title and a subject tag. The course titles are column entries in the CMS, so a new track appears here as soon as an administrator creates the column.

02What I did not build

  • No app. A URL works on a phone browser, and asking a family to install something is a step where people drop out. The site is responsive from 320px up.
  • No framework of my own. This runs as secondary development on PbootCMS 3.2.16, and the reason is administrative rather than technical: the people who publish courses need to add a column, swap a cover and post an activity notice without calling me.
  • No recommendations and no leaderboard. What a thirteen-year-old studies next should not be decided by a model trained on other people’s children, and a public ranking mostly tells the kids with the least support where they stand.
  • No payment entry of any kind. Once a charity site accepts money, it also owes an audit trail, and that is a different project with a different team.

03Defining "finished"

Progress is recorded from the video element itself, not from a button the student presses. While a lesson plays, the player reports its current position every ten seconds; the last position is kept so that reopening the page resumes where the student stopped. This runs on a separate table rather than the CMS content tables, because progress writes are frequent and the content tables are read on every page.

# lesson state, from the stored percentage
p = 0 or no row → not started
0 < p < 95 → in progress
p ≥ 95        → completed

The 95% line is a compromise, not a measurement of learning. Five percent is left at the end so that a student who stops during the credits is not stuck one second short of a certificate. The certificate number encodes what happened rather than who deserves it: CERT, the completion date, the four-digit course id, the six-digit user id. Printed from the browser with a stylesheet that drops the navigation, it fits on one sheet of A4.

What the rule actually proves: the playhead reached 95% of the video. It does not prove the student understood the lesson, and there is no quiz on this platform that would.

04Two languages means two column trees

Chinese and English are not a translation layer here. Each language owns its own columns and its own entries: 40 columns in total, 20 per language, holding 152 entries at 76 each. Switching language swaps the labels through a cookie plus a CSS class, and fifteen templates carry both wordings. The hreflang tags point Chinese, English and a default at the same page so a search engine indexes them as alternates.

Course category page listing lessons with tags
A course column, English side. Same lessons, same order, separate entries.
Public welfare activities list page
Activity notices run on the same CMS, so posting one is a form, not a deployment.

05The constraints were the project

Most of what shaped this build came from the hosting arrangement rather than from the design. One server, no cluster, and a CMS whose URL mode could not be changed without breaking links that had already been shared.

  • Compatibility URL mode. The CMS runs with url_rule_type=3, and in that mode the web server will not pass a POST to a dynamic URL. Every progress report, therefore, is a GET with its parameters in the query string.
  • An empty language field breaks the menu. If the language code on a column is blanked, the navigation disappears in whole blocks. That column has to keep its value even when the site is being reorganised.
  • Caches can be emptied but not removed. Compiled templates and cached pages live in two directories that must keep existing; deleting the directories themselves takes the site down until they are recreated by hand.
  • Backups need a file extension the web server refuses to serve. Database dumps and archives carry a download-only suffix, because the server is configured to block direct requests for .sql and .tar.gz.
  • File permissions were off limits. Permissions on existing files and directories could not be changed, which rules out several of the usual shortcuts and means any new code has to fit the layout that was already there.

06What it runs on

59 recorded lessons: 47 Python, 6 maths, 6 physics
40 / 152 columns and entries, each split evenly between the two languages
9,213 lines written on top of the CMS: 1,702 PHP, 7,511 front end
1 server, no cluster, HTTPS with a free certificate

Nginx in front, PHP 8.2 behind it, MySQL on the same machine. The firewall opens four ports and nothing else. Intrusion blocking bans an address for half an hour after five failed logins. The database listens on loopback only and the application connects with its own user, so a leaked credential does not open the whole server. Certificates renew themselves, and a monitoring check warns when renewal is within thirty days of failing.

07What this does not show

  • There is no learning-outcome data. Completion rate is the only measure the platform has, and completion is where the playhead stopped.
  • No load test has been run. One server serves the whole thing, and video bandwidth will be the first limit to arrive, well before CPU is.
  • The certificate records attendance, not understanding. Anyone reading it as a qualification is reading it wrong, and the page that issues it does not claim otherwise.
  • Next: move the videos to object storage behind a CDN, encode a lower-resolution version for weak connections, and add referrer checks so the lessons cannot be hotlinked into someone else’s site.

A view count is for the operator. A completion record is for the child and the family. Keeping those two numbers apart is what made the rest of the design decisions easy.

The platform is not trying to be clever. It is trying to be checkable, and checkable turns out to cost more code than publishing does: a table, a timer, a state rule, a certificate number, and a print stylesheet. That is the part I would defend in a review.