LW Course Catalog - WordPress plugin — display LearnWorlds courses on any page with a shortcode
LW Course Catalog — Plugin Documentation *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { --brand: #2563eb; --brand-dark: #1d4ed8; --brand-light: # eff6ff; --brand-muted: #bfdbfe; --text: #1e293b; --text-muted: #64748b; --border: #e2e8f0; --bg: #f8fafc; --bg-white: #ffffff; --success-bg: #f0fdf4; --success-text: #166534; --success-border:#bbf7d0; --warn-bg: #fffbeb; --warn-text: #92400e; --warn-border: #fde68a; --info-bg: #eff6ff; --info-text: #1e40af; --info-border: #bfdbfe; --radius: 10px; --radius-sm: 6px; --shadow: 0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04); } html { scroll-behavior: smooth; } body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; font-size: 15px; line-height: 1.7; color: var(--text); background: var(--bg); } /* ── Layout ── */ .layout { display: flex; min-height: 100vh; } .sidebar { width: 260px; flex-shrink: 0; background: var(--bg-white); border-right: 1px solid var(--border); position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 2rem 1.25rem; } .main { flex: 1; max-width: 820px; padding: 3rem 3rem 4rem; } @media (max-width: 768px) { .layout { flex-direction: column; } .sidebar { width: 100%; height: auto; position: static; border-right: none; border-bottom: 1px solid var(--border); } .main { padding: 2rem 1.25rem 3rem; } } /* ── Sidebar ── */ .sidebar-logo { display: flex; align-items: center; gap: 10px; margin-bottom: 2rem; } .sidebar-logo-icon { width: 36px; height: 36px; border-radius: 8px; background: var(--brand); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 18px; } .sidebar-logo-text { font-weight: 700; font-size: 14px; line-height: 1.3; color: var(--text); } .sidebar-logo-sub { font-size: 11px; color: var(--text-muted); font-weight: 400; } .nav-section { margin-bottom: 1.5rem; } .nav-section-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .5rem; padding-left: .5rem; } .nav-section ul { list-style: none; display: flex; flex-direction: column; gap: 2px; } .nav-section ul li a { display: block; padding: .4rem .75rem; border-radius: var(--radius-sm); font-size: 13px; color: var(--text-muted); text-decoration: none; transition: all .15s; } .nav-section ul li a:hover { background: var(--brand-light); color: var(--brand); } .sidebar-version { margin-top: 2rem; padding: .5rem .75rem; background: var(--bg); border-radius: var(--radius-sm); font-size: 12px; color: var(--text-muted); } .sidebar-version span { color: var(--brand); font-weight: 600; } /* ── Header ── */ .doc-header { margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 1px solid var(--border); } .doc-badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 99px; background: var(--brand-light); color: var(--brand); margin-bottom: .75rem; letter-spacing: .02em; } .doc-title { font-size: 32px; font-weight: 800; color: var(--text); line-height: 1.2; margin-bottom: .5rem; } .doc-subtitle { font-size: 16px; color: var(--text-muted); line-height: 1.6; } .meta-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 1.25rem; } .meta-pill { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 4px 10px; border-radius: 99px; background: var(--bg); border: 1px solid var(--border); color: var(--text-muted); } .meta-pill strong { color: var(--text); } /* ── Sections ── */ .section { margin-bottom: 3.5rem; scroll-margin-top: 2rem; } .section-title { display: flex; align-items: center; gap: 10px; font-size: 22px; font-weight: 700; color: var(--text); margin-bottom: 1.25rem; padding-bottom: .75rem; border-bottom: 2px solid var(--brand-light); } .section-title .s-icon { width: 36px; height: 36px; border-radius: 8px; background: var(--brand-light); color: var(--brand); display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; } .subsection { margin-bottom: 1.75rem; } .subsection-title { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: .75rem; } p { color: var(--text-muted); margin-bottom: .9rem; } p:last-child { margin-bottom: 0; } p strong { color: var(--text); } /* ── Steps ── */ .steps { display: flex; flex-direction: column; gap: 0; margin-bottom: 1.25rem; } .step { display: flex; gap: 0; } .step-left { display: flex; flex-direction: column; align-items: center; width: 40px; flex-shrink: 0; } .step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--brand); color: #fff; font-size: 13px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .step-line { width: 2px; background: var(--brand-muted); flex: 1; margin: 4px 0; } .step:last-child .step-line { display: none; } .step-content { padding: 2px 0 20px 4px; } .step-content strong { display: block; font-size: 14px; color: var(--text); font-weight: 600; margin-bottom: 2px; } .step-content span { font-size: 13px; color: var(--text-muted); } /* ── Tables ── */ .table-wrap { overflow-x: auto; margin-bottom: 1.25rem; border-radius: var(--radius); border: 1px solid var(--border); } table { width: 100%; border-collapse: collapse; font-size: 13px; } thead th { background: var(--bg); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); } tbody td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: top; color: var(--text-muted); line-height: 1.5; } tbody tr:last-child td { border-bottom: none; } tbody td:first-child { color: var(--text); font-weight: 500; white-space: nowrap; } tbody tr:hover td { background: var(--bg); } /* ── Code ── */ code { font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; font-size: 12.5px; background: var(--brand-light); color: var(--brand); padding: 2px 7px; border-radius: 4px; border: 1px solid var(--brand-muted); } pre { background: #0f172a; color: #e2e8f0; padding: 1.25rem 1.5rem; border-radius: var(--radius); font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; line-height: 1.8; overflow-x: auto; margin-bottom: 1.25rem; } pre code { background: none; color: #93c5fd; padding: 0; border: none; font-size: 13px; } pre .comment { color: #64748b; } pre .attr { color: #86efac; } pre .val { color: #fde68a; } /* ── Notices ── */ .notice { display: flex; gap: 10px; padding: .85rem 1rem; border-radius: var(--radius-sm); margin-bottom: 1.25rem; font-size: 13px; line-height: 1.6; } .notice .n-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; } .notice.tip { background: var(--success-bg); border-left: 3px solid var(--success-border); color: var(--success-text); } .notice.warn { background: var(--warn-bg); border-left: 3px solid var(--warn-border); color: var(--warn-text); } .notice.info { background: var(--info-bg); border-left: 3px solid var(--info-border); color: var(--info-text); } /* ── Card grid ── */ .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 1.25rem; } .card-sm { background: var(--bg-white); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; font-size: 13px; } .card-sm-title { font-weight: 600; color: var(--text); margin-bottom: 4px; font-size: 13px; } .card-sm p { font-size: 12.5px; margin: 0; } /* ── Filter logic table ── */ .filter-table { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 1.25rem; } .ft-row { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; align-items: center; padding: 9px 14px; border-bottom: 1px solid var(--border); gap: 8px; font-size: 12.5px; } .ft-row.header { background: var(--bg); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); } .ft-row:last-child { border-bottom: none; } .ft-row:not(.header):hover { background: var(--bg); } .ft-label { color: var(--text-muted); } .pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 99px; white-space: nowrap; } .pill.hide { background: #fee2e2; color: #991b1b; } .pill.show { background: #dcfce7; color: #166534; } .pill.na { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); } .ft-result { font-weight: 600; font-size: 12px; } .ft-result.shown { color: #166534; } .ft-result.hidden { color: #dc2626; } /* ── Shortcode examples ── */ .shortcode-ex { margin-bottom: 1rem; } .sc-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: 6px; } /* ── Troubleshoot ── */ .trouble-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 1rem; } .trouble-item { background: var(--bg-white); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; } .trouble-q { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 5px; display: flex; gap: 8px; align-items: flex-start; } .trouble-q .t-icon { color: var(--warn-text); flex-shrink: 0; margin-top: 1px; } .trouble-a { font-size: 13px; color: var(--text-muted); padding-left: 22px; } /* ── Footer ── */ .doc-footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } .doc-footer a { color: var(--brand); text-decoration: none; } /* Print */ @media print { .sidebar { display: none; } .main { padding: 1rem; max-width: 100%; } } 📚 LW Course Catalog Plugin Documentation Getting started Installation LearnWorlds credentials Plugin settings Configuration Card design Draft & private filtering Pagination Reference Shortcode reference Debug log Troubleshooting Version 1.4.4 · Built by Codendesigner WordPress Plugin LW Course Catalog Display courses from one or two LearnWorlds schools on any WordPress page using a simple shortcode. Visitors browse on WordPress and are redirected to LearnWorlds to enroll and pay. 📦 v1.4.4 🔧 WordPress 5.8+ 🐘 PHP 7.4+ 🏫 Supports 2 schools ⬇️ Installation 1 Upload the pluginIn WordPress admin go to Plugins → Add New → Upload Plugin 2 Select the zip fileChoose lw-course-catalog.zip and click Install Now 3 ActivateClick Activate Plugin after installation completes 4 Open settingsGo to Settings → LW Course Catalog to configure your credentials ✅ No other plugins are required. The plugin is self-contained with its own caching, logging, and admin UI. 🔑 Getting your LearnWorlds credentials All three values you need are on one page inside your LearnWorlds school admin. Navigate to: https://yourschool.com/author/settings_api What to copy Value Where to find it Notes API URL The "API URL" field with a Copy button Looks like https://yourschool.com/admin/api/ — copy it exactly as shown Client ID Click the eye 👁 icon to reveal, then Copy Required alongside the Access Token Access Token Under "Access Tokens" section → click Create → eye icon → copy Recommended auth method. Long-lived, no expiry unless revoked. Client Secret Click Copy next to the masked field Optional — only needed if not using an Access Token ℹ️ The API URL must include the full path (/admin/api/). Do not use just the root domain — the plugin uses the exact URL you provide to build all API calls. ⚠️ If your school uses a custom domain (e.g. www.foodsurety.com), use that custom domain in the API URL — not the .learnworlds.com subdomain. ⚙️ Plugin settings Go to Settings → LW Course Catalog → ⚙️ Settings tab. There are two school sections — School A is required, School B is optional. School A / School B fields Field Required Description API URL Yes Full API URL from LearnWorlds settings. Example: https://yourschool.com/admin/api/ Access Token ⭐ Recommended Long-lived token. Paste the full string. If provided, Client Secret is not needed. Client ID Yes Required alongside Access Token for the Lw-Client request header. Client Secret Optional Only used if no Access Token is provided. Plugin falls back to OAuth2 client credentials flow. Cache settings Field Default Description Cache Duration 12 hours How often course data is re-fetched from LearnWorlds. Options: 1h, 3h, 6h, 12h, 24h. Shorter = fresher data, more API calls. Longer = faster page loads. ℹ️ Leave School B fields completely blank until your second school is ready. It will be automatically included once configured. The plugin will not show errors for an empty School B. Status tab The 📊 Status tab shows the cache state for each school (how many minutes remain before the next auto-refresh), the current configured URLs, and two action buttons: Button What it does Refresh Cache Now Immediately clears the cache and re-fetches all courses from all configured schools Test Connection & Re-fetch Same as above but also redirects you to the Debug Log tab to see the full fetch result 🎨 Card design settings Go to Settings → LW Course Catalog → 🎨 Card Design tab. All settings apply globally to every course card on the site. Display toggles Show thumbnail Toggle the course image on or off Show description Toggle the excerpt text on or off Show price Toggle the price badge on or off Show categories Toggle category badges on or off Show Enroll button Links directly to the payment/checkout page Show Learn More button Links to the course detail/landing page Course visibility Setting Default Description Hide draft courses On ✅ Hides courses with status: draft. Has absolute priority — a draft course is always hidden when this is on, regardless of the private setting. Hide private courses On ✅ Hides courses with access: private. Only evaluated after the draft check passes. ✅ When you save Card Design settings, the cache is automatically cleared. The change takes effect on the next page load — no manual refresh needed. Text & layout Setting Default Description Enroll button text Enroll Now Label on the primary CTA button. Can also be overridden per-shortcode. Learn More button text Learn More Label on the secondary button. Can also be overridden per-shortcode. Description length 20 words How many words to show in the course description excerpt (5–100). Courses per page 12 How many cards to show before Prev/Next pagination appears. Can be overridden per-shortcode with the per_page attribute. Colours Setting Default Card background #ffffff Enroll button background #2563eb Enroll button text colour #ffffff Learn More button background #f1f5f9 Learn More button text colour #1e293b 🔍 Draft & private filtering The plugin uses Option C — Draft takes absolute priority. A course goes through two gates in strict order. If it is rejected at gate 1 (draft), it never reaches gate 2 (private). Course status + access Hide draft setting Hide private setting Result Published + Public either either ✅ Always shown Published + Private either ON ❌ Hidden Published + Private either OFF ✅ Shown Draft + Public ON irrelevant ❌ Hidden (draft wins) Draft + Private ON irrelevant ❌ Hidden (draft wins) Draft + Private OFF ON ❌ Hidden by private rule Draft + Private OFF OFF ✅ Shown ℹ️ The key principle: Draft means "not ready for anyone". Private means "ready but restricted". If Hide Draft is on, draft status overrides everything — even if private courses are being shown. 📄 Pagination API-side fetch (all courses) On first load the plugin fetches every page of courses from LearnWorlds automatically. LearnWorlds returns 50 courses per API page. For a school with 424 courses this means 9 API requests, all made server-side and cached. Pagination uses the meta.page and meta.totalPages fields from the LearnWorlds API response. The plugin keeps fetching until page === totalPages. ⚠️ The first cache build on a large school (400+ courses) takes 15–40 seconds. All subsequent page loads for every visitor are served from cache instantly. Display-side pagination (Prev/Next) After all courses are fetched and cached, they are displayed in pages controlled by the Courses per page setting (default: 12). Prev/Next buttons appear automatically below the grid when the total exceeds the per-page limit. The category filter bar and pagination work together — filtering resets to page 1 and the page count updates to reflect the filtered total. Cache lifecycle Event Cache behaviour First page load after activation Cache is empty — plugin fetches all courses and stores them Subsequent page loads Served from cache instantly — no API calls Cache TTL expires (default 12h) WP-Cron automatically refreshes in the background Settings saved Cache is busted immediately — fresh fetch on next load Card Design settings saved Cache is busted — draft/private filter changes take effect Manual Refresh Cache Now Immediate bust and re-fetch triggered from admin 🧩 Shortcode reference Place [lw_course_catalog] on any WordPress page, post, or widget area. All attributes are optional — defaults come from the Card Design settings. Attribute Values Default Description school both / a / b both Which school's courses to display columns 1 – 4 3 Grid column count. Automatically collapses to 2 on tablet, 1 on mobile. per_page Any number From settings Courses per page before pagination. Overrides the Card Design setting for this instance only. category Category name — Pre-filter to a single category. Example: category="Food Safety" enroll_text Any text From settings Override the Enroll button label for this shortcode instance more_text Any text From settings Override the Learn More button label for this shortcode instance Examples Basic — all courses from both schools [lw_course_catalog] School A only, 2-column grid [lw_course_catalog school="a" columns="2"] Filter to a specific category, 6 per page [lw_course_catalog category="Food Safety" per_page="6"] Custom button text [lw_course_catalog enroll_text="Buy Now" more_text="View Course"] Full example combining multiple attributes [lw_course_catalog school="a" columns="3" per_page="9" category="HACCP" enroll_text="Enroll Today"] Button destinations Button Destination Learn More The course detail/landing page on LearnWorlds — e.g. https://yourschool.com/course/intro-haccp Enroll Now The payment page on LearnWorlds — e.g. https://yourschool.com/payment?product_id=intro-haccp&type=course Both buttons open in a new tab. No payment or registration happens on WordPress — everything is handled by LearnWorlds. 🔎 Debug log Go to Settings → LW Course Catalog → 🔍 Debug Log tab. The log records every step of the API fetch process — authentication, pagination, filtering, and caching. Log levels Level Colour What it means ERROR 🔴 Red Something failed — authentication error, API error, or network problem. Always investigate these. WARN 🟡 Yellow A non-fatal issue — e.g. School B not configured, or a fallback was used. INFO 🔵 Blue Normal progress messages — token obtained, page fetched, courses cached. DEBUG ⚫ Gray Detailed technical data — exact URLs called, raw API meta, field names. Useful for diagnosing image or pagination issues. Actions Action Description Run Connection Test Clears the cache and triggers a fresh fetch from all configured schools. All steps are logged in real time. Use this after changing credentials. Clear Log Removes all log entries. The log automatically keeps the most recent 200 entries. Filter buttons Show only Errors, Warnings, Info, or Debug entries. Useful for quickly spotting problems. Reading a successful fetch log After a successful connection test you should see entries like this (newest first in the log): INFO [School A] ✅ Fetch complete. Total courses: 412 across 9 pages INFO [School A] Page 9 done: 24 items, 4 skipped, has_more=NO, total=412 INFO [School A] Page 8 done: 50 items, 6 skipped, has_more=YES, total=388 ... INFO [School A] Page 1 done: 50 items, 10 skipped, has_more=YES, total=40 INFO [School A] Using static access token INFO [School A] Starting API fetch 🛠️ Troubleshooting ⚠️ No courses shown at all Check the Settings tab — ensure the API URL, Client ID, and Access Token are all filled in correctly. Then go to the Debug Log tab and click Run Connection Test. Read the ERROR entries to see exactly what failed. ⚠️ Only 40–50 courses showing instead of 400+ This was a pagination bug fixed in v1.4.2. Update to v1.4.4 and run a Connection Test. The log should show "Fetch complete. Total courses: 400+" across multiple pages. ⚠️ Page 2 of the catalog is empty This was a display pagination bug fixed in v1.4.0. Update to v1.4.4. The JS now controls all card visibility — no conflict with server-side class names. ⚠️ Auth failed — HTTP 404 error The API URL is incorrect. It must include the full path: https://yourschool.com/admin/api/. Do not use the root domain alone. Copy it exactly from LearnWorlds Settings → Developers → API. ⚠️ Course images not showing Check the Debug Log → expand the "Raw page 1 response" DEBUG entry → look at the "first_course" field — it lists all field names in the API response. The plugin tries 15 image field name variants. If none match, let us know the exact field name shown and it can be added. ⚠️ Draft or private courses still appearing Go to Card Design tab, check the relevant hide option, and save. The cache auto-clears on save. If still showing, run a manual Connection Test to force a full re-fetch with the new filter applied. ⚠️ Enroll button going to the wrong page The enroll URL is built from the course slug and package ID returned by the API. Check the Debug Log for the raw course data to verify the slug. If LearnWorlds provides a payment_url field directly, the plugin uses that instead. ⚠️ Settings not saving Your WordPress user must have the manage_options capability (Administrator role). Lower-level roles cannot save plugin settings. ℹ️ For persistent issues, share the full Debug Log (especially ERROR and DEBUG entries) with your developer. The log contains the exact API responses needed to diagnose any problem. LW Course Catalog v1.4.4 · Built by Codendesigner WordPress Plugin · LearnWorlds Integration