Session log — A short description under every lesson on s2l.online
Session log — A short description under every lesson on s2l.online
What happened, in order
- The brief, and the clarifying questions
- Discovery: what the lesson card already showed
- A new column on the lessons table
- A "Short description" field in the lesson editor
- Wiring it through the save handler
- The JavaScript that ties the form together
- Rendering the description under the title
- The styling: a single small block
- Verification on the live course page
- Going-forward workflow
1. The brief, and the clarifying questions
One sentence, but with two open ends worth nailing down before any code moved. The first: where should the description appear — just on the course landing page (where learners are choosing what to watch), or also inside the lesson player while they are watching? The second: where should the description be edited — was the existing admin lesson editor the right home for it?
Two short replies later both questions were settled: edit in the existing admin lesson editor, next to the lesson title, and show under the lesson title on the course landing page only. The viewer was deliberately left alone — once a learner is watching, they have already chosen the lesson; a description there would be dead weight.
2. Discovery: what the lesson card already showed
EP Courses lives in Course not found./var/www/s2l/user-content/plugins/ep-courses/. The course landing page is rendered by the shortcode, which itself reads from two tables: pm_ep_courses for course-level fields and pm_ep_lessons for the lessons. The lesson list on the landing page is plain HTML — an ordered list, one li per lesson, each carrying just a title and (if set) a duration in minutes.
The lessons table already had room to breathe: title, content (the long lesson body), translations (a JSON column for non-English copies of every translatable field), video_url, video_type, sort_order, duration_minutes, status, plus four columns added in earlier work for Vimeo metadata. There was no description column — a real gap, because the lesson content field is the long rich-text body, not a card-sized summary. The description field had to be new.
3. A new column on the lessons table
EP Courses already has a clean pattern for non-destructive schema upgrades: a private method maybe_upgrade_schema() that runs every time the plugin loads and checks information_schema.COLUMNS before adding any column. Each missing column is added in place; existing data is untouched.
One line was appended to the columns array inside that method:
'description' => "TEXT DEFAULT NULL"
The TEXT type was deliberate: not VARCHAR(255), because Hasmukh might one day want a slightly longer summary for a richer course; not LONGTEXT, because that would be wasteful storage for what is meant to be one or two lines. TEXT sits in the middle — up to 65,535 bytes, more than enough for any short description.
A single page load on s2l.online triggered the upgrade. A quick check of the lessons table confirmed the new column was in place with no other changes:
description text YES NULL
description column with a NULL value, ready to be filled in.4. A "Short description" field in the lesson editor
The lesson editor lives inside the EP Courses admin shortcode, rendered in plugin.php as a hidden div#ep-lesson-editor. The HTML is built up line by line; the existing fields go: hidden id, hidden course id, Title, Content, Video Type, Video URL, Vimeo preview block, hidden Vimeo metadata, Sort Order, Duration, Status, then the Translations block.
The new "Short description" field was inserted between Title and Content — the most natural reading order for an editor (write the title, write the elevator pitch, then the long body). It uses a two-row textarea with a placeholder that doubles as in-context guidance:
<textarea id="ep-lesson-description" rows="2"
placeholder="One or two lines shown under the lesson title on the course page"></textarea>
The Translations block already iterates through ten South African languages and exposes a Title, Content, and Video URL field per language. A "Short description" field was added to that loop as a peer of Title — same two-row textarea, same data-lang attribute pattern, same DOM class scheme (.ep-lesson-trans-description[data-lang="..."]). Translators can now translate the short description in exactly the same way they translate the title and the content.
5. Wiring it through the save handler
The admin save flow is handle_save_lesson($data), an AJAX endpoint that takes a JSON payload, escapes every field, then runs either an UPDATE or an INSERT on pm_ep_lessons. Three small edits to that one method:
- A new local
$descriptionvariable, populated from the payload and run throughreal_escape_string. - The
UPDATESQL gained adescription = '$description'assignment, slotted next totitle. - The
INSERTSQL gained a matching column name and value pair in the same position.
Translations are stored in a JSON column called translations — one object per language, each with whatever fields are set. The save handler did not need a code change for the per-language descriptions: the JSON is encoded on the JavaScript side and the PHP simply takes the whole object and stores it. Adding a description key to the per-language objects in the JS was enough to carry the data through.
6. The JavaScript that ties the form together
Three small functions in js/ep-courses.js needed to know about the new field. Each in turn:
clearLessonForm()— called when the editor opens for a brand-new lesson. Added#ep-lesson-descriptionto the list of inputs cleared, and added.ep-lesson-trans-descriptionto the cross-language reset selector.populateLessonForm(lesson)— called when the editor opens for an existing lesson, with the lesson data fetched from the server. Added a single line:$('#ep-lesson-description').val(lesson.description || ''). Inside the translations loop, added$('.ep-lesson-trans-description[data-lang="' + lang + '"]').val(t.description || '').collectLessonForm()— called when the Save button is pressed; gathers every input value into a single object and posts it to the AJAX endpoint. Addeddescriptionat the top level of the returned object. Inside the translations loop, addeddescriptionas a key on each per-language object alongsidetitle,content, andvideo_url; also addeddescriptionto the "is this language worth saving?" guard so a language whose only filled field is the description is not silently dropped.
7. Rendering the description under the title
The course landing page builds the lesson list as an ordered list of li elements in plugin.php. Before this change, every li contained a title span and (if set) a duration span side by side, as direct children of a flex row.
To add a description that sits under the title without disturbing the duration on the right, the title block needed a wrapper. The new structure inside each li:
<li>
<div class="ep-landing-lesson-text">
<span class="ep-landing-lesson-title">...</span>
<span class="ep-landing-lesson-desc">...</span> <!-- only if set -->
</div>
<span class="ep-landing-lesson-duration">...</span> <!-- only if set -->
</li>
The description string itself runs through the existing get_translated_field() helper, which means: in English it returns the description column; in any other language it first checks translations[language].description and falls back to English if no translation has been written. Same behaviour as the title and content. The line is wrapped in htmlspecialchars() the same way the title is, so any apostrophes or ampersands a writer types will render safely.
Lessons with no description simply omit the span.ep-landing-lesson-desc entirely — the wrapper renders with just the title, and the row looks visually identical to how it did before.
8. The styling: a single small block
The existing CSS treated .ep-landing-lesson-title as a flex child filling the row. With the title now wrapped inside .ep-landing-lesson-text, the flex grow had to move up to the wrapper. Three rules added, one rule trimmed:
.ep-landing-lesson-text— flex-direction column, 4px gap,flex: 1,min-width: 0(the last to prevent long titles overflowing the flex row)..ep-landing-lesson-title— kept the same look (medium weight, dark text), removed the now-redundantflex: 1..ep-landing-lesson-desc— muted grey (#555), 14px, line-height 1.4. Sits under the title with a 4px gap. Quiet enough to read as supporting copy, not a heading..ep-landing-lesson-duration— unchanged.
EP Courses serves its frontend stylesheet directly out of the plugin folder, so no theme-CSS recompile was needed; the file change is the publish step.
9. Verification on the live course page
Three checks, in order from cheapest to most informative:
php -l plugin.phpon the server — No syntax errors detected. The file was uploaded straight after.- A
DESCRIBE pm_ep_lessonsquery confirmed the newdescriptioncolumn was present, typetext, defaultNULL, after one trigger page load. GET /sars-efiling(the existing SARS course landing page) — rendered200, every lesson row carried the newdiv.ep-landing-lesson-textwrapper around its title, the duration still rendered to the right, and rows without a description in the database simply omitted the description span. The page looked visually identical to before, because no description has been filled in yet — exactly the desired behaviour.
Backups taken before the change, in case rollback is needed:
/var/www/s2l/user-content/plugins/ep-courses/plugin.php.bak.<timestamp>
/var/www/s2l/user-content/plugins/ep-courses/js/ep-courses.js.bak.<timestamp>
/var/www/s2l/user-content/plugins/ep-courses/css/ep-courses-frontend.css.bak.<timestamp>
10. Going-forward workflow
For every lesson Hasmukh wants to enrich:
- Open the EP Courses admin, pick the course, click Lessons, click the lesson to edit.
- In the new Short description box (just under the Title) type one or two lines summarising what the lesson covers.
- (Optional) For any language the course is published in, open the matching translation block and translate the short description the same way.
- Save. Refresh the course landing page. The new line appears in muted grey under the lesson title.
No CSS recompile, no menu change, no cache flush. The same workflow holds for new lessons created from this point on — the field is just there in the editor.