Blog /
Build a course-catalog app that pulls from a custom post type
If you sell courses on a WordPress site, you have an LMS plugin (LearnDash, LifterLMS, Sensei, Tutor LMS). The LMS plugin owns the lesson player, the quiz engine, the progress tracking, and the certificate issuance. It does this well.
What none of them do well: the catalog page. The marketing surface where prospective students see all the courses, filter by topic, see featured imagery, click through to enroll.
The stock catalog page from every LMS plugin I have used looks like an admin grid. You can customize it with PHP. You can override its template in your child theme. You can pay for a “marketing pack” add-on. All three paths are more work than the catalog page deserves.
The DesignSetGo path is the inverse. The LMS plugin keeps owning the lesson, the quiz, the progress. A DesignSetGo App reads the course CPT, renders the marketing catalog, and links each card out to the LMS plugin’s enrollment URL. One bundle. Your design. No theme override.
This post walks the build. Sized for an afternoon.
What you are building
A catalog page at /courses/ (or /apps/courses/ if you do not want to route it). The page shows:
- A grid of course cards. Each card has the featured image, title, instructor name, a one-line description, and the price.
- Filter controls above the grid: a category multi-select (read from the course taxonomy), a skill-level filter, a search box.
- “Enroll” or “Learn more” CTAs on each card, linking to the LMS’s enrollment URL for that course.
Rendered in inline mode so the catalog is SEO-indexable (people search “online courses in [topic]” and find the catalog page).
Step 1: scaffold
npx designsetgo apps init course-catalog
cd course-catalog
Open in Claude Code. The prompt:
Build a course catalog page. Read posts from the
sfwd-coursesCPT (LearnDash). Ifsfwd-coursesdoesn’t exist, fall back tollms_course(LifterLMS) orcourse. Render a responsive grid of cards: featured image (16:9), course title, instructor (read from_course_authormeta or post author), one-line excerpt, price (read from_pricemeta). Above the grid: a category multi-select using theld_course_categorytaxonomy (or fallback), a skill-level radio (Beginner / Intermediate / Advanced; read from askill_leveltaxonomy or a_skill_levelmeta), a search box. Inline mode for SEO. CTA per card: “Learn more” linking to course.link. Style: clean, education-focused, with a primary accent color. Mobile-first.
Claude Code writes the bundle. The shape:
import { dsgo } from '@designsetgo/app-client';
await dsgo.ready;
const courses = await dsgo.posts.list({
type: 'sfwd-courses', // or llms_course, etc.
per_page: 50,
_embed: true,
});
const cats = await dsgo.taxonomy.terms({
taxonomy: 'ld_course_category',
per_page: 30,
});
renderFilters(cats);
renderCards(courses);
Step 2: the manifest
{
"manifest_version": 1,
"id": "course-catalog",
"name": "Course Catalog",
"version": "0.1.0",
"entry": "index.html",
"isolation": "inline",
"display": {
"modes": ["page"],
"page": {
"wrap": "theme",
"sitemap": true,
"title": "Course Catalog | {{site.name}}",
"meta_description": "Browse our courses."
}
},
"permissions": { "read": ["posts", "taxonomy"], "write": [] },
"runtime": { "sandbox": "strict", "external_origins": [] }
}
Inline mode for SEO (see the listings browser post for the deep dive on inline mode’s SEO benefits). Theme wrap so the catalog renders inside your site’s normal header/footer. Sitemap inclusion so Google finds it.
The permissions.read declares posts (which now includes CPTs after last week’s release) and taxonomy (for the category and skill-level filters). Nothing else.
Step 3: deploy
npx designsetgo apps deploy
The page is live at /apps/course-catalog/. Open it. Filter by a category. Search for a course title. Click a card. The CTA links to the LMS’s enrollment URL.
To mount it at /courses/ instead of /apps/course-catalog/, the same options apply as for the listings browser: either Pro’s slug rewrite, or a WordPress page titled “Courses” with the catalog as a block embed.
What you keep using the LMS for
This is the important framing. The DesignSetGo App is not replacing your LMS plugin. It is replacing your LMS plugin’s marketing template. Two very different things.
The LMS keeps doing:
- The lesson player and video embed.
- The quiz engine and grading.
- Progress tracking per student.
- Certificate issuance.
- The enrollment flow (the “Click here to buy” action).
- Coupon codes, member tiers, course bundles.
The DesignSetGo App does:
- The catalog page (this tutorial).
- Any custom landing page per course (with
dynamic_route: "wp:cpt:sfwd-courses"). - Any custom learner dashboard (“My courses” view, customized).
- Any custom marketing widget (“Recommended for you,” “Recently added”).
The two compose. The catalog links to the LMS’s enrollment URL; the LMS handles the purchase; the LMS marks the student as enrolled; the LMS’s lesson player renders the actual course. The DesignSetGo App is the front door. The LMS is the building.
Why this matters
The LMS catalog page is one of the highest-conversion surfaces on an education site. It is where a prospective student decides which course to buy. The default catalog is generic, the customization path is PHP, and most education sites either accept the default (and lose conversion) or pay for a marketing-pack add-on (which is another plugin to maintain).
The DesignSetGo path: take an afternoon, build a catalog that looks like the rest of your site, deploy it, ship. The next time you want to redesign the catalog, you edit your bundle. The LMS plugin updates have no effect on your catalog’s appearance because the LMS plugin is not rendering the catalog anymore.
The plugin-author cost: zero. The maintenance cost: one bundle to keep current. The conversion lift: real, because the catalog is now part of your marketing system rather than an afterthought rendered by the LMS.
Optional upgrades
Three additions that take the catalog from “ship it” to “this is a real marketing surface”:
Per-course detail pages. A dynamic-route app that mounts at every course permalink and renders a custom landing page (hero, syllabus, instructor bio, social proof, enrollment CTA). Dynamic routes deep dive walks the pattern.
Student-aware filtering. “Show me courses I have not enrolled in yet.” Reads the LMS plugin’s enrollment metadata via the bridge (most LMSes expose enrollment as user meta or as an enrollment custom table). Hides courses the visitor is already enrolled in; highlights the next recommended course based on what they have finished.
AI-driven recommendations. “Tell me what to take next” button that calls dsgo.ai.prompt() with the visitor’s course history and asks for a recommendation. Uses the Connector configured on the site. The prompt is one paragraph; the implementation is 10 lines.
The optional upgrades each take another hour or two. Bundle them once the basic catalog is live and you have real visitor data on what they search and filter for.
Where this goes
The course catalog is one of three DesignSetGo Apps every education site eventually wants. The other two: a student dashboard (custom view of “courses I am enrolled in,” with progress bars and the next-lesson CTA) and a certificate gallery (a public-facing showcase of certificates the site has issued, useful for marketing). Both are the same pattern: read from the LMS plugin’s CPTs and user meta, render in your design system.
The wedge: shipping the catalog this afternoon. The vertical pack: all three apps as one bundle, worth its own post when there is demand.
The shorter version
The LMS plugin renders the lessons. The DesignSetGo App renders the marketing. Catalog page in inline mode, reading the LMS’s course CPT and taxonomy, deployed in an afternoon. Use both; do not fight either.
Further reading
- Reading your WordPress taxonomy from an app: the taxonomy surface walk.
- A real-estate listings browser as an inline-mode app: same pattern, different vertical.
- examples/content-navigator: the example bundle that this catalog is sketched from.