Session log — A short description under every lesson on s2l.online

← All session logs

Session log — A short description under every lesson on s2l.online

27 April 2026 · Hasmukh with Claude · a small, well-shaped change to EP Courses: each lesson can now carry a one-or-two-line short description, edited in the admin, translated per language, and rendered under the lesson title on every course landing page. End-to-end: database column, admin editor, save handler, frontend markup, frontend CSS, all with the existing translation system honoured.

Brief

1. The brief, and the clarifying questions

HasmukhIn my s2l.online site can we add in the lessons for courses card a description field?

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.

OutcomeA focused, narrow change. One field. One admin location. One frontend location. No theme touch, no viewer touch, no migration of existing content.
Step 1

2. Discovery: what the lesson card already showed

EP Courses lives in /var/www/s2l/user-content/plugins/ep-courses/. The course landing page is rendered by the

Course not found.

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.

Step 2

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
OutcomeSchema migrated, zero data loss, zero downtime. Every existing lesson now has a description column with a NULL value, ready to be filled in.
Step 3

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.

OutcomeThe lesson editor now has eleven short-description boxes — one English, ten language translations — all aligned with the existing translation pattern.
Step 4

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:

  1. A new local $description variable, populated from the payload and run through real_escape_string.
  2. The UPDATE SQL gained a description = '$description' assignment, slotted next to title.
  3. The INSERT SQL 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.

Step 5

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-description to the list of inputs cleared, and added .ep-lesson-trans-description to 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. Added description at the top level of the returned object. Inside the translations loop, added description as a key on each per-language object alongside title, content, and video_url; also added description to the "is this language worth saving?" guard so a language whose only filled field is the description is not silently dropped.
OutcomeOpen editor: description loads. Save: description writes. Switch languages: each language carries its own description through.
Step 6

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.

Step 7

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-redundant flex: 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.

Step 8

9. Verification on the live course page

Three checks, in order from cheapest to most informative:

  1. php -l plugin.php on the server — No syntax errors detected. The file was uploaded straight after.
  2. A DESCRIBE pm_ep_lessons query confirmed the new description column was present, type text, default NULL, after one trigger page load.
  3. GET /sars-efiling (the existing SARS course landing page) — rendered 200, every lesson row carried the new div.ep-landing-lesson-text wrapper 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>
OutcomeSchema, admin, save handler, render, styling: all five layers in place. The site looks unchanged until a lesson is given a description, at which point it appears under the title on the course page.
Going forward

10. Going-forward workflow

For every lesson Hasmukh wants to enrich:

  1. Open the EP Courses admin, pick the course, click Lessons, click the lesson to edit.
  2. In the new Short description box (just under the Title) type one or two lines summarising what the lesson covers.
  3. (Optional) For any language the course is published in, open the matching translation block and translate the short description the same way.
  4. 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.