Overview#
Lingua is a complete website for a language school that teaches adults in small groups, in person
and online, built as a WordPress block theme. The fictional school is Lingua Language School at
14 Harbour Lane, Bristol, teaching Spanish, French, Italian, German, Japanese and Portuguese. The
theme has a home page, a courses page with filters, a course page with tabs, a placement test that
recommends a course, a teachers page with a language filter, a weekly timetable with filters, a
fees and enrolment page with price tabs and a four-step enrolment form, an about page with an FAQ,
a contact page and a 404 page. It is version 1.0.0 (see template.json and style.css), works
with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the HTML template of the
same name. See it live at freeframe.dev/templates/lingua.
Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text,
links, buttons, lists and pictures in the block editor without touching code. Colours, the fonts
and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and
the whole site follows. The course page, the placement test's recommendations and the enrolment
form's fees are worked out in the browser by assets/js/main.js from the course data at its top.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
lingua.zipfrom freeframe.dev/templates/lingua (the WordPress tab). - In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Lingua.
- Go to Appearance > Lingua setup and click Create the demo pages. This creates the Home, Courses, course, Placement test, Teachers, Timetable, Fees and enrolment, About and Contact pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the two header menus. Pages that already exist with the same slug are left alone, so it is safe to click it again.
- In Settings > General, set the Site Title to your school's name and the Tagline to a short description: the header and footer show the title, and the home page's browser title is "Title — Tagline".
- Set your courses, teachers and fees (see Courses, teachers and fees).
You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.
To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm dev signs you in as admin (password password) and creates the demo pages. The site is
rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the
WordPress admin are not kept.
Pages#
Each page is a page pattern (in Patterns > Lingua pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Lingua.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (greetings around a classroom picture), languages, how-it-works, formats, starting-soon, testimonials, team-teaser, placement-cta |
| Courses | /courses/ |
page-courses.php |
courses-header, course-list (filters and thirteen course cards), levels |
| Course | /course/ |
page-course.php |
course-breadcrumb, course-detail (facts, tabs, fee card), related-courses |
| Placement test | /placement/ |
page-placement.php |
placement-header, placement-test (the questions and the result), placement-help |
| Teachers | /teachers/ |
page-teachers.php |
teachers-header, teacher-list (filter and eight teachers), teach-with-us |
| Timetable | /timetable/ |
page-timetable.php |
timetable-header, timetable (filters and the week) |
| Fees and enrolment | /enrol/ |
page-enrol.php |
enrol-header, prices (tabs), enrol-form (the four steps and the summary) |
| About | /about/ |
page-about.php |
about-header, story, method, school, faq |
| Contact | /contact/ |
page-contact.php |
contact-header, contact (form, map, hours and directions) |
| 404 | any missing URL | templates/404.html |
404 |
There is one course page: it shows the course named in the URL (/course/?id=fr-a1-evening), as
the HTML version does. Posts use templates/single.html; the blog, archives and search results
use templates/index.html, templates/archive.html and templates/search.html.
Project structure#
freeframe-lingua/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with their strings, page and image URLs),
editor styles, the `js` class script, theme colour, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Lingua setup: creates the demo pages and the two menus
templates/ page.html (pages), page-with-title.html, single.html, index.html,
archive.html, search.html, 404.html
parts/ header.html and footer.html (they show the header and footer patterns)
patterns/ every section and page (PHP files with block markup)
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/main.js the site's JavaScript, with the course data at the top
assets/icons/ SVG icons for the Icon block
assets/img/ logo, favicon, social image, classroom and school illustrations, map,
teachers/ (the eight portraits)
assets/fonts/ Lexend and Young Serif and their licenses (OFL.txt)
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/icons.css icons drawn before or after text with CSS (generated)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-lingua.pot, the template for translations
blueprint.json WordPress Playground setup used by `pnpm dev`
screenshot.png the picture shown in Appearance > Themes
package.json the CSS build and the local WordPress (Node.js tooling)
readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colour | Used for |
|---|---|
| Base (paper) | the page background |
| Contrast (ink) | text, outlines, the cards' offset shadows, dark sections |
| Paper 2, Line | panels and borders |
| Muted | secondary text |
| Brand (ultramarine), Brand dark, Brand soft | the primary button, links, selected choices |
| Tomato, Tomato ink | the decorative accent (eyebrow bubbles, the hand-drawn underline) and accent text, errors |
| Sun, Mint | level badges, the call-to-action band, the success tick |
The theme's stylesheet reads every colour from the palette (bg-brand, text-tomato-ink... are
the presets), so changing Brand in Styles recolours every button and link at once. Keep enough
contrast between Brand and white, and between Tomato ink and Base, for text (4.5:1).
Each language also has a light background and a readable text colour for its chips, cards and
timetable entries (tone-es, tone-fr, tone-it, tone-de, tone-ja, tone-pt). They are
defined in src/theme.css (--color-es, --color-es-ink...), not in the palette; change them
there and run pnpm build.
Typography#
The fonts are Young Serif (headings and display text, font-display) and Lexend (body text, a
typeface designed for reading ease), bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google. To use other fonts, install
them in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select them,
or replace the fontFace entries in theme.json. The utility classes use the families
--wp--preset--font-family--lexend (font-sans) and --wp--preset--font-family--young-serif
(font-display), so rename those slugs only together with src/theme.css.
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The
stylesheet uses them too: changing Large in Styles changes every text-lg in the design.
Content#
Edit pages in Pages: click any text to change it, select a button to change its link, select a picture and use Replace. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Lingua. Search for "Lingua", "Harbour Lane", "Bristol", the phone number and the email address when you make it yours: they appear in the pages, the header and the footer.
Some content is in Custom HTML blocks, because blocks can't express it: edit those in the block's HTML view.
| Pattern | Custom HTML | What it is |
|---|---|---|
header |
the menu button | a button driven by assets/js/main.js |
home-hero |
the classroom picture with its greeting bubbles | an illustration |
course-list, timetable |
the filter forms | see Course filters and Timetable |
timetable |
the week and its colour key | see Timetable |
course-detail |
the whole course section | filled in from the data, see Course page |
related-courses |
the three course cards | written by the script for the course shown |
placement-test |
the whole test | see Placement test |
teacher-list |
the language filter buttons | see Teacher filter |
prices |
the tab buttons | see Price tabs |
enrol-form |
the whole enrolment form and its summary | see Enrolment form |
contact |
the contact form and its thank-you message | see Forms |
404 |
the "404" with speech bubbles | an illustration |
footer |
the newsletter form | see Forms |
Blocks can't store data- attributes, so the theme writes them as classes on blocks and the first
lines of assets/js/main.js turn them back into attributes before anything else runs:
ffd-name--value becomes data-name="value" (an underscore stands for a space),
ffa-name--value becomes the attribute name="value" (ffa-role--tabpanel), and ff-hidden
hides a block until the script shows it. In the editor, hidden blocks are shown with a dashed
outline. The ones you will meet:
- Course cards (Article groups in
starting-soonandcourse-list):ffd-course-card,ffd-lang--spanish(a language id),ffd-level--a1(a1toc1) andffd-format--in-person(in-personoronline); the filters read them. - Teacher cards:
ffd-teacher-cardandffd-langs--spanish_portuguese(the languages they teach, separated by underscores). - The live counts and empty states:
ffd-course-count,ffd-teacher-count,ffd-course-empty. - The price panels on the fees page: groups with the anchor
panel-group(andpanel-private,panel-companies) and the classesffa-role--tabpanel,ffa-aria-labelledby--tab-groupandff-hiddenfor the ones shown later. revealfades a block in as it scrolls into view;link-cardon a group makes the whole group clickable through its first link.
Images and artwork#
- Pictures (the classroom, the online class, the school building) are SVG illustrations in
assets/img/, shown with Image blocks inside boxes that crop them (artboxes with an aspect ratio). Replace them with photos from the Media Library (landscape, at least 1200 × 800 px); the crop comes from the box. - Teacher portraits are
assets/img/teachers/<teacher id>.svg(600 × 500). On the Teachers and Home pages they are Image blocks; replace them there. The course page's "Your teacher" tab is written by the script from the same files: replace the files (same names), or change the path inteacherHTML()inassets/js/main.js. - Logo: the header and footer show
assets/img/logo.svgin an Image block next to the Site Title block. Replace the image in Appearance > Editor > Patterns > Header (and Footer); the name comes from Settings > General > Site Title. - Map:
assets/img/map.svgon the contact page; replace it with your own map picture, or an embed from your map provider. - Icons are Icon blocks: select one and pick another icon from the Lingua collection (the
theme's icons) or WordPress's own. The arrow in buttons is drawn by CSS (
icon-after-arrow-right). - Social image:
assets/img/og-image.png(1200 × 630, sourceog-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing. - Favicon:
assets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Navigation, header and footer#
The header and footer are template parts (parts/header.html, parts/footer.html) showing the
header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.
The header has two Navigation blocks with their own menus, created by the setup: "Main menu" (Courses, Timetable, Placement test, Teachers, About, Contact) on wide screens and "Mobile menu" (Home, the same pages and Fees & enrolment) in the full-height mobile menu. Edit them in Appearance > Editor > Navigation. WordPress marks the link to the current page; on the course page the script marks Courses, as the HTML version does. The "Enrol now" buttons and the phone number under the mobile menu are blocks in the header pattern.
The footer's link columns are List blocks: click a link to change it, press Enter to add one. The copyright year is added by PHP.
Courses, teachers and fees#
The course page, the related courses, the placement test's recommendations and the enrolment form
use the data at the top of assets/js/main.js (part "1. Data"):
| Data | Meaning |
|---|---|
LANGS |
id (used in URLs and classes), code, name, tone (the colour class suffix) |
LEVELS |
the CEFR levels a1 to c1: code, name, about |
KEEP_DEMO_CURRENT, TERM |
true and { starts: '2027-01-11', ends: '2027-03-26' }: the term the courses belong to. Once it has started, the script moves the term and every course start on by whole weeks to the same term next year (see below). Set it to false once COURSES and TERM hold your real dates |
COURSES |
id, lang, level, format (in-person or online), title, days (0 = Monday … 5 = Saturday), start, end, weeks, starts (first class, YYYY-MM-DD), fee (pounds per term), teacher (a teacher id), seats (places left), room, summary, description |
TEACHERS |
id (also the portrait's file name), name, role, from, years, bio, quote |
SYLLABUS, OUTCOMES |
five modules and four "you'll be able to" lines per level, shared by every language |
BOOK_PRICE, CONCESSION |
the course book (£24) and the concession discount (0.15, 15% off the course fee) |
To add a course, add an entry to COURSES with a new id, then add its card to the Courses page
(duplicate a card in course-list, change its text, link and ffd- classes), its sessions to the
timetable's Custom HTML and its option to the enrolment form's course list (in enrol-form).
{ "id": "es-c1-online", "lang": "spanish", "level": "c1", "format": "online", "title": "Advanced Spanish", "days": [3], "start": "19:00", "end": "20:30", "weeks": 10, "starts": "2027-01-14", "fee": 240, "teacher": "tomas-ibarra", "seats": 8, "room": "Online", "summary": "…", "description": "…" }The pages show the same courses as blocks (cards, the timetable, the fee cards): keep them in step
with the data. When you change a course's text in the data, add the new text to
freeframe_lingua_script_strings() in inc/setup.php if you translate the site (the script
translates the visitor-facing text in the data with it). Make these changes in a child theme so
updates don't overwrite them (see Child themes).
Demo dates. The demo is written for the winter term in TERM (11 January to 26 March 2027).
While KEEP_DEMO_CURRENT is true and that term has started, the script moves the term and every
course's starts forward to the same term the next year, by whole weeks so weekdays never change
(11 January 2027 becomes 10 January 2028), so visitors never see a course that has already begun. It
also updates the dates written in the patterns: <span data-term="year">, data-term="starts" and
data-term="ends" (the Courses and Timetable headers) and data-course-from="<course id>" on each
course card's "weeks from" line. Set KEEP_DEMO_CURRENT = false when you enter your real term
dates, and keep the dates in the patterns in step with them.
Dates are written as text ("2027-01-11") and formatted without Date objects, so the site shows
the same dates in every time zone. Prices are formatted by pounds() (enrolment summary, in pence)
and as '£' + fee (cards); change those for another currency.
Features#
Course filters#
The Courses page's filter form (data-course-filters) hides the cards whose ffd-lang--,
ffd-level-- and ffd-format-- classes don't match, updates the live count ("Showing 3 of 13
courses", announced to screen readers) and shows the empty state when nothing matches. The filters
are written to the URL, so /courses/?lang=french&format=online can be shared. Values in the URL
are read in any case (?LANG=French) and ignored unless they are one of the form's options. The
language cards on the home page link to /courses/?lang=<id>.
Course page#
/course/?id=<course id> shows one course: the page ships with Spanish for Beginners as a
fallback and the script (initCoursePage()) fills in the title, chips, facts, overview, syllabus,
teacher, fee, places left, the enrolment and placement links, the page title and three related
courses from the data. Ids are read in any case; an unknown id shows the first course.
The overview, syllabus and teacher are accessible tabs (initTabs()): arrow keys move between
them, Home and End jump to the first and last, and only the selected tab is in the tab order.
"Enrol on this course" opens the enrolment form with the course chosen
(/enrol/?course=<id>#enrol).
Placement test#
A Custom HTML block in placement-test (initPlacement()): the visitor chooses a language, then
answers ten "can do" statements, each "Not yet" (0), "With some effort" (1) or "Easily" (2). The
total (0 to 20) gives the level through PLACEMENT_BANDS (5, 9, 13 and 17 points for A2, B1, B2 and
C1). The result names the level and recommends the courses at that level in that language, or the
nearest level (the lower one when two are as close) with a note when there is none. Each step
moves focus to its question, Back keeps the answers, and a step can't be skipped without an answer.
The result is saved in the browser and shown again on the next visit, with "Start again".
/placement/?lang=french starts a new test with French chosen. The statements are in the Custom
HTML; keep ten of them, or change PLACEMENT_QUESTIONS and the bands.
Teacher filter#
The buttons (data-teacher-filter, aria-pressed) show the teachers whose ffd-langs-- class
includes the language and update the count. /teachers/?lang=japanese opens a language directly.
Timetable#
The week is a Custom HTML block: one column per day, one link per class (data-session with the
language, format and time of day: morning, lunchtime or evening). The filters (language,
format, time of day or Saturdays) hide the classes that don't match, show "No classes" on empty
days, update the count and are kept in the URL (/timetable/?time=saturday). Each class links to
its course page.
Price tabs#
The fees page's prices (group courses, private lessons, companies) are tabs like the course page's.
/enrol/#private or #companies opens a tab and scrolls to it. To add a tab, add a button in the
tab list's Custom HTML (id="tab-online", aria-controls="panel-online") and a group with the
anchor panel-online and the classes ffa-role--tabpanel ffa-aria-labelledby--tab-online ffa-tabindex--0 ff-hidden.
Enrolment form#
The enrolment form is a Custom HTML block in enrol-form (initEnrol()), in four steps:
- Course: the course list, with its days, times, start date, teacher and places left.
- Your details: first and last name, email (checked), an optional phone number (checked when given) and whether the visitor has studied the language before.
- Options: pay in full or in three monthly instalments, the concession rate (15% off the course fee) and the course book.
- Review: everything chosen, the total and the terms checkbox.
A live summary beside the form shows the course fee, the concession, the book, the total and the
three instalments (the last one absorbs the rounding, so they add up exactly). The step list marks
the current step (aria-current="step"), errors appear under the fields with focus on the first
problem, and focus moves to each step's heading. ?course=<id> (any case) chooses the course.
Nothing is sent. The place to send the enrolment is marked in the submit handler
(// To send for real, post the enrolment to your booking system here); replace it with a request
to your booking system, and check fees and places again on your server.
Header, mobile menu, scroll reveals and back to top#
- The header gains a border once the page scrolls.
- The menu button opens the full-height mobile menu, updates
aria-expandedand its label, and Esc closes it and returns focus to the button. - Blocks with the
revealclass fade in as they scroll into view; revealing one also reveals everything above it. The home page's greeting bubbles bob gently. With reduced motion, or without JavaScript, everything shows at once and nothing moves. - A "Back to top" button appears after about one screen of scrolling; it moves focus to the main
content and hides while it would cover the sticky fee or enrolment summary. To remove it, delete
the "Back to top" part of
assets/js/main.jsand the.back-to-topstyles insrc/theme.css, then runpnpm build.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, ultramarine), Secondary (white), Sunflower |
Forms#
| Form | Page | Checks |
|---|---|---|
| Enrolment | Fees and enrolment | each step |
| Placement test | Placement test | an answer on every step |
Contact (data-form="contact") |
Contact | name, email, message (at least 20 characters) |
Newsletter (data-form="newsletter") |
footer of every page |
Every form[data-form] is handled by initForms() in assets/js/main.js: errors appear under
each field, focus moves to the first problem, and the success message is shown.
/contact/?topic=private chooses the contact form's topic (general, courses, placement,
private or companies, in any case). The forms don't send anything until you connect them.
Option 1: a form plugin#
Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact Form 7, Jetpack Forms...),
build the form there, then replace the form's Custom HTML block (and its success message) with
the plugin's block. Give its fields the class field and its labels field-label to match the
theme.
Option 2: a form service#
Keep the theme's form and send it to a service such as Formspree: in
initForms() (in a child theme's copy of assets/js/main.js), replace the comment in the submit
handler with a request, and show the success message when it succeeds:
fetch('https://formspree.io/f/your-id', { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
.then(function (res) {
if (!res.ok) throw new Error('Request failed');
form.reset();
if (success) { success.classList.remove('hidden'); success.focus(); }
});and remove the lines after it that show the success message straight away. Branch on
form.dataset.form (contact, newsletter) for different endpoints, and update the success
messages, which say the forms are demos. The enrolment form posts the same way from its submit
handler in initEnrol().
Saved data (localStorage)#
| Key | Shape |
|---|---|
lingua:enrol:v1 |
{ course, first, last, email, phone, experience, plan, concession, book, heard }: the enrolment in progress |
lingua:placement:v1 |
{ lang, answers }: the last placement result (ten answers, 0 to 2) |
Every read is checked. The enrolment draft (sanitizeDraft()) keeps only a known course id, text
up to its field's length without control characters, and known values for the experience, plan
and "how did you hear" fields, with true for the checkboxes; anything else is dropped and the
stored value repaired. A course in the URL wins over the draft. The draft is cleared after the
form is confirmed. A placement result (sanitizePlacement()) needs a known language and exactly
ten answers of 0, 1 or 2; otherwise it is removed. Corrupt JSON is removed too. To reset, clear
the site's storage in the browser or remove the two keys.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Teachers — Lingua"); the home page shows the site name and tagline (Settings > General); the
course page's title follows the course shown. The theme doesn't output meta descriptions or Open
Graph tags; use an SEO plugin such as Yoast SEO, Rank Math or The SEO Framework, and give it
assets/img/og-image.png (or your own 1200 × 630 image) as the default social image. WordPress
already generates a sitemap at /wp-sitemap.xml. For a school, add your address, phone and
courses as structured data (EducationalOrganization and Course) with the SEO plugin.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, navigation with labels, main, footer), one h1 per page, visible focus rings, the mobile
menu button with aria-expanded, accessible tabs, radio groups with legends in the placement test
and enrolment form, focus moved to each new step, live counts and fee summaries announced to
screen readers, form errors announced through aria-describedby and aria-invalid, alt text on
pictures, colour never used alone (every language chip has its name), and reduced-motion support
for every animation. The demo passes axe (WCAG 2.1 A and AA).
When you edit: keep one Heading 1 per page, give pictures alt text (or mark them decorative), keep
link text meaningful, and label any field you add to a form (<label for="...">).
Building and deploying#
You only need this if you change the theme's files. The stylesheet assets/css/theme.css is
compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/,
templates/, inc/ and assets/js/:
pnpm install
pnpm build # compile assets/css/theme.css and assets/css/editor.css
pnpm watch # recompile assets/css/theme.css on every change (run pnpm build before you commit)Run pnpm build after adding a utility class to a pattern or template, otherwise the class has no
style. Classes you type in the editor's Additional CSS class(es) field are only styled if the
stylesheet already contains them.
To deploy, zip the theme folder (named freeframe-lingua, without node_modules/) and upload it
in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.
Translations#
The theme is translation-ready with the text domain freeframe-lingua. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (course,
teacher and syllabus text, labels, fees, messages) are translated in PHP
(freeframe_lingua_script_strings() in inc/setup.php). Create a translation with Loco
Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-lingua wp-content/themes/freeframe-lingua/languages/freeframe-lingua.potSave freeframe-lingua-es_ES.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Lingua setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content. Month names are
translated with the other strings. Filter values (data-filter, data-teacher-filter, the ffd-…
classes, the course ids) stay in English; translate only their labels.
Child themes#
Use a child theme for changes you want to keep when Lingua is updated (your courses in
assets/js/main.js especially). Create wp-content/themes/lingua-child/style.css:
/*
Theme Name: Lingua Child
Template: freeframe-lingua
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. To use your own copy of the script, copy assets/js/main.js into the child theme and,
in its functions.php, load it in place of the parent's (wp_deregister_script( 'freeframe-lingua' )
and wp_enqueue_script() on wp_enqueue_scripts, priority 30), keeping the data the parent
prints before it (window.freeframeLinguaL10n, window.freeframeLinguaPages,
window.freeframeLinguaAssets). The parent's stylesheet only contains the classes the parent uses:
if your child theme's patterns use new utility classes, compile a stylesheet for the child.
Troubleshooting#
- The pages look unstyled or new classes do nothing. The stylesheet only contains classes it
found when it was built. Run
pnpm buildafter editing patterns, then reload. - "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Lingua.
- A course card doesn't show up in a filter. Check its
ffd-lang--,ffd-level--andffd-format--classes: the values must match the filter's options. - The course page shows the wrong course. It uses
COURSESinassets/js/main.js, not the blocks on the page; theidin the URL must be a course id. - Links from the script go to missing pages. Course cards, the placement result and the course
page link to the pages with the demo's slugs (
/course/,/enrol/,/courses/,/placement/,/teachers/); if you renamed them, change the slugs back or editfreeframe_lingua_page_urls()in a child theme. - Lingua setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Lingua pages.
- An old placement result keeps showing. It is saved in the browser: click "Start again", or
open
/placement/?lang=<language>to start a new test. - A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms), choose the Page with title template in the page's settings.
Differences from the HTML version#
The pages, copy, design and interactions are the same. What WordPress changes:
- Straight apostrophes typed in text blocks are shown curly, as WordPress does everywhere.
- Data and ARIA attributes on content blocks are written as classes and restored by the script (see Content).
- Definition lists (the course levels, the about page's figures, the fees' notes, the reception
hours) are groups of paragraphs, because blocks can't make
<dl>lists. - The FAQ's plus-in-a-circle is drawn by CSS on the Details block.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use, including the SVG artwork. Lexend and Young Serif
are licensed under the SIL Open Font License 1.1 (assets/fonts/*/OFL.txt). Lingua Language
School, its teachers and its students are fictional.