Skip to main content
Skip to content
Browse the docs

Developer Guide

Quick start recipes

Pick the recipe that matches your website. Each one is complete: it calls Lernmark from your server, gives up after 5 seconds, caches successful results for 10 minutes, escapes every value before rendering, and fails closed.

  1. Set your values. Replace your-school and YOUR_INSTITUTION_ID with the values from Before you start. The Apply link uses the same school slug.
  2. Add the recipe to your server-side code.
  3. Check the output against the sample HTML under each recipe, then style it to match your site.

Each recipe fetches the first page of up to 100 programmes, sorted by title. If you list more than 100, read Pagination.

Node.js (no dependencies)

Works in any server-side Node.js 18+ route or rendering function. The cache lives in memory; if your site runs on several servers, use your shared cache with the same 10-minute lifetime.

TypeScript
const BASE_URL = "https://api.lernmark.app";
const SCHOOL_SLUG = "your-school";
const INSTITUTION_ID = "YOUR_INSTITUTION_ID";
const APPLY_URL = `https://www.lernmark.app/apply/${encodeURIComponent(SCHOOL_SLUG)}`;
const CACHE_TTL_MS = 10 * 60 * 1000;

type Programme = {
  id: string;
  title: string;
  award: string | null;
  academic_unit: string | null;
  duration: string | null;
  summary: string | null;
  application_available: boolean;
  slug: string;
  last_updated: string | null;
};

let cache: { expiresAt: number; programmes: Programme[] } | null = null;

async function getProgrammes(): Promise<Programme[]> {
  if (cache && cache.expiresAt > Date.now()) return cache.programmes;
  cache = null; // Never serve an expired response.

  const url = new URL(
    `/api/v1/public/site/${encodeURIComponent(SCHOOL_SLUG)}` +
      `/institutions/${encodeURIComponent(INSTITUTION_ID)}/programmes`,
    BASE_URL,
  );
  url.searchParams.set("per_page", "100");
  url.searchParams.set("sort", "name");

  const response = await fetch(url, {
    headers: { Accept: "application/json" },
    signal: AbortSignal.timeout(5000),
  });
  if (response.status !== 200) {
    throw new Error(`Lernmark returned HTTP ${response.status}`);
  }

  const body = await response.json();
  const programmes = body?.data?.programmes;
  if (!Array.isArray(programmes)) {
    throw new Error("Lernmark returned an unexpected response.");
  }

  cache = { expiresAt: Date.now() + CACHE_TTL_MS, programmes };
  return programmes;
}

function escapeHtml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function renderProgramme(programme: Programme): string {
  const details = [programme.award, programme.duration]
    .filter((value): value is string => Boolean(value))
    .map(escapeHtml)
    .join(" · ");

  return `<li>
  <article>
    <h3>${escapeHtml(programme.title)}</h3>
    ${details ? `<p>${details}</p>` : ""}
    ${programme.summary ? `<p>${escapeHtml(programme.summary)}</p>` : ""}
    ${
      programme.application_available
        ? `<p><a href="${escapeHtml(APPLY_URL)}">Apply for ${escapeHtml(programme.title)}</a></p>`
        : "<p>Applications are not currently open.</p>"
    }
  </article>
</li>`;
}

export async function renderProgrammeList(): Promise<string> {
  try {
    const programmes = await getProgrammes();
    if (programmes.length === 0) {
      return '<p role="status">No programmes are currently listed.</p>';
    }
    return `<ul aria-label="Programmes">\n${programmes.map(renderProgramme).join("\n")}\n</ul>`;
  } catch {
    return '<p role="status">Programme information is temporarily unavailable.</p>';
  }
}

Sample output:

HTML
<ul aria-label="Programmes">
  <li>
    <article>
      <h3>BTS Accounting and Management</h3>
      <p>BTS · 2 years full-time</p>
      <p>Practical accounting, tax, and business management for the Cameroonian market.</p>
      <p><a href="https://www.lernmark.app/apply/siantou">Apply for BTS Accounting and Management</a></p>
    </article>
  </li>
</ul>

PHP (cURL and a file cache)

Works in any server-side PHP 8+ code with the cURL extension. The PHP process must be able to write to the system temporary directory, where the recipe keeps successful responses for 10 minutes.

PHP
<?php

const LERNMARK_API_BASE_URL = 'https://api.lernmark.app';
const LERNMARK_SCHOOL_SLUG = 'your-school';
const LERNMARK_INSTITUTION_ID = 'YOUR_INSTITUTION_ID';
const LERNMARK_CACHE_SECONDS = 600;

function lernmark_apply_url(): string
{
    return 'https://www.lernmark.app/apply/' . rawurlencode(LERNMARK_SCHOOL_SLUG);
}

function get_lernmark_programmes(): array
{
    $url = LERNMARK_API_BASE_URL . sprintf(
        '/api/v1/public/site/%s/institutions/%s/programmes?per_page=100&sort=name',
        rawurlencode(LERNMARK_SCHOOL_SLUG),
        rawurlencode(LERNMARK_INSTITUTION_ID)
    );
    $cacheFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR
        . 'lernmark-programmes-' . hash('sha256', $url) . '.json';

    if (is_file($cacheFile) && filemtime($cacheFile) > time() - LERNMARK_CACHE_SECONDS) {
        $cached = json_decode((string) file_get_contents($cacheFile), true);
        if (is_array($cached)) {
            return $cached;
        }
    }

    $curl = curl_init($url);
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_HTTPHEADER => ['Accept: application/json'],
    ]);
    $raw = curl_exec($curl);
    $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    $curlError = curl_error($curl);
    curl_close($curl);

    if ($raw === false || $status !== 200) {
        throw new RuntimeException($curlError !== '' ? $curlError : "Lernmark returned HTTP {$status}");
    }

    $programmes = json_decode($raw, true, 512, JSON_THROW_ON_ERROR)['data']['programmes'] ?? null;
    if (!is_array($programmes)) {
        throw new RuntimeException('Lernmark returned an unexpected response.');
    }

    file_put_contents($cacheFile, json_encode($programmes, JSON_THROW_ON_ERROR), LOCK_EX);
    return $programmes;
}

function h(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function render_lernmark_programmes(): string
{
    try {
        $programmes = get_lernmark_programmes();
    } catch (Throwable) {
        return '<p role="status">Programme information is temporarily unavailable.</p>';
    }

    if ($programmes === []) {
        return '<p role="status">No programmes are currently listed.</p>';
    }

    $html = '<ul aria-label="Programmes">';
    foreach ($programmes as $programme) {
        $details = array_filter([$programme['award'] ?? null, $programme['duration'] ?? null]);

        $html .= '<li><article>';
        $html .= '<h3>' . h($programme['title'] ?? '') . '</h3>';
        if ($details !== []) {
            $html .= '<p>' . implode(' · ', array_map('h', $details)) . '</p>';
        }
        if (!empty($programme['summary'])) {
            $html .= '<p>' . h($programme['summary']) . '</p>';
        }
        $html .= !empty($programme['application_available'])
            ? '<p><a href="' . h(lernmark_apply_url()) . '">Apply for ' . h($programme['title'] ?? '') . '</a></p>'
            : '<p>Applications are not currently open.</p>';
        $html .= '</article></li>';
    }

    return $html . '</ul>';
}

echo render_lernmark_programmes();

The output matches the Node.js sample.

WordPress (shortcode and transients)

First, add your values to wp-config.php, above the line that says to stop editing. Keeping them out of your theme means a theme update cannot lose them.

PHP
define('LERNMARK_SCHOOL_SLUG', 'your-school');
define('LERNMARK_INSTITUTION_ID', 'YOUR_INSTITUTION_ID');

Then save the following as wp-content/plugins/lernmark-programmes/lernmark-programmes.php, activate Lernmark Programmes under Plugins, and place [lernmark_programmes] in any page or post.

PHP
<?php
/**
 * Plugin Name: Lernmark Programmes
 * Description: Adds the [lernmark_programmes] shortcode, which lists your publicly listed Lernmark programmes.
 */

if (!defined('ABSPATH')) {
    exit;
}

function lernmark_fetch_programmes()
{
    $url = add_query_arg(
        ['per_page' => 100, 'sort' => 'name'],
        'https://api.lernmark.app' . sprintf(
            '/api/v1/public/site/%s/institutions/%s/programmes',
            rawurlencode(LERNMARK_SCHOOL_SLUG),
            rawurlencode(LERNMARK_INSTITUTION_ID)
        )
    );
    $cacheKey = 'lernmark_programmes_' . md5($url);

    $cached = get_transient($cacheKey);
    if (is_array($cached)) {
        return $cached;
    }

    $response = wp_remote_get($url, [
        'timeout' => 5,
        'headers' => ['Accept' => 'application/json'],
    ]);
    if (is_wp_error($response)) {
        return $response;
    }

    $status = wp_remote_retrieve_response_code($response);
    if ($status !== 200) {
        return new WP_Error('lernmark_http', "Lernmark returned HTTP {$status}");
    }

    $programmes = json_decode(wp_remote_retrieve_body($response), true)['data']['programmes'] ?? null;
    if (!is_array($programmes)) {
        return new WP_Error('lernmark_json', 'Lernmark returned an unexpected response.');
    }

    set_transient($cacheKey, $programmes, 10 * MINUTE_IN_SECONDS);
    return $programmes;
}

function lernmark_programmes_shortcode(): string
{
    $programmes = lernmark_fetch_programmes();
    if (is_wp_error($programmes)) {
        return '<p role="status">Programme information is temporarily unavailable.</p>';
    }
    if ($programmes === []) {
        return '<p role="status">No programmes are currently listed.</p>';
    }

    $applyUrl = 'https://www.lernmark.app/apply/' . rawurlencode(LERNMARK_SCHOOL_SLUG);

    $html = '<ul class="lernmark-programmes" aria-label="Programmes">';
    foreach ($programmes as $programme) {
        $details = array_filter([$programme['award'] ?? null, $programme['duration'] ?? null]);

        $html .= '<li><article>';
        $html .= '<h3>' . esc_html($programme['title'] ?? '') . '</h3>';
        if ($details !== []) {
            $html .= '<p>' . implode(' · ', array_map('esc_html', $details)) . '</p>';
        }
        if (!empty($programme['summary'])) {
            $html .= '<p>' . esc_html($programme['summary']) . '</p>';
        }
        $html .= !empty($programme['application_available'])
            ? '<p><a href="' . esc_url($applyUrl) . '">Apply for ' . esc_html($programme['title'] ?? '') . '</a></p>'
            : '<p>Applications are not currently open.</p>';
        $html .= '</article></li>';
    }

    return $html . '</ul>';
}

add_shortcode('lernmark_programmes', 'lernmark_programmes_shortcode');

The output matches the Node.js sample, with a lernmark-programmes class on the list for your theme's styles.

Next

Read Apply links before you show the Apply link on your live site.