Skip to main content

Featured Image For Courses

Currently, we already have thumbnail & featured image for Space.

But for course, we only have thumbnail option, no featured image option here.

I think we can also have Featured Image option also for Course, that will be displayed at the top of single course page, just like the space banner displayed at the top.

That will make the single course page better.

Thankyou!

We are already on the process, the work is in the initial phase.

Agus Mu

Tawsif Ahmed RiyadΒ perfect and thankyou

Morgan McArthur

Tawsif Ahmed RiyadΒ brilliant! ... finally! ... thanks for the update!

Jacob Windham

I build a snippet to use until it’s official. You can see it used here:

https://metanesta.com/course/from-sweet-tea-to-the-big-apple-a-southerners-first-timer-guide-to-nyc-177/lessons

/**
 * Course Cover Hero
 *
 * Injects the course cover_photo as a full-bleed hero banner above
 * the course intro β€” identical in style to FC's space cover images.
 *
 * Route:  /course/{slug}/lessons
 * Target: .fcom_course_intro  (prepends the hero above it)
 *
 * KILL SWITCH: set MN_FLAG_COURSE_COVER_HERO to false in MN_Config
 * when FluentCommunity ships this natively. Zero residue β€” snippet
 * stays inactive, no DOM changes.
 *
 * Depends on: MN_FLAG_COURSE_COVER_HERO constant (MN_Config)
 */

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

// ── Kill switch β€” flip to false in MN_Config when FC ships natively ──
if ( defined( 'MN_FLAG_COURSE_COVER_HERO' ) && ! MN_FLAG_COURSE_COVER_HERO ) {
    return;
}

// ── AJAX: return cover_photo URL for a course slug ────────────────────
add_action( 'wp_ajax_mn_get_course_cover', function () {
    // No nonce needed β€” public read, no state change
    global $wpdb;

    $slug = sanitize_text_field( $_POST['slug'] ?? '' );
    if ( ! $slug ) wp_send_json_error( 'Missing slug.' );

    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT cover_photo FROM {$wpdb->prefix}fcom_spaces
         WHERE slug = %s AND type = 'course' LIMIT 1",
        $slug
    ) );

    if ( ! $row || empty( $row->cover_photo ) ) {
        wp_send_json_error( 'No cover.' );
    }

    wp_send_json_success( [ 'cover' => esc_url( $row->cover_photo ) ] );
} );

add_action( 'wp_ajax_nopriv_mn_get_course_cover', function () {
    global $wpdb;
    $slug = sanitize_text_field( $_POST['slug'] ?? '' );
    if ( ! $slug ) wp_send_json_error( 'Missing slug.' );

    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT cover_photo FROM {$wpdb->prefix}fcom_spaces
         WHERE slug = %s AND type = 'course' LIMIT 1",
        $slug
    ) );

    if ( ! $row || empty( $row->cover_photo ) ) {
        wp_send_json_error( 'No cover.' );
    }

    wp_send_json_success( [ 'cover' => esc_url( $row->cover_photo ) ] );
} );


// ── Frontend ──────────────────────────────────────────────────────────
add_action( 'fluent_community/portal_footer', function () {
    $ajax = admin_url( 'admin-ajax.php' );
    ?>
    <style>
    /* ── Hero wrapper β€” mirrors .object_header / .object_cover ── */
    #mn-course-hero {
        position: relative;
        width: 100%;
        /* 16:5 ratio matches FC space banners on desktop */
        aspect-ratio: 16 / 5;
        overflow: hidden;
        background: var(--fcom-secondary-bg, #f0f0f1);
        /* Sits flush above .fcom_course_intro which has its own padding */
        margin-bottom: 0;
    }

    #mn-course-hero img {
        display: block;
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: center center;
    }

    /* Subtle bottom fade so the hero blends into the page bg */
    #mn-course-hero::after {
        content: '';
        position: absolute;
        inset: auto 0 0 0;
        height: 40%;
        background: linear-gradient(
            to bottom,
            transparent 0%,
            var(--fcom-primary-bg, #fff) 100%
        );
        pointer-events: none;
    }

    /* Skeleton shimmer while loading */
    #mn-course-hero.mn-hero-loading {
        background: linear-gradient(
            90deg,
            var(--fcom-secondary-bg, #eee) 25%,
            var(--fcom-active-bg,    #e0e0e0) 50%,
            var(--fcom-secondary-bg, #eee) 75%
        );
        background-size: 200% 100%;
        animation: mn-hero-shimmer 1.4s infinite;
    }

    @keyframes mn-hero-shimmer {
        0%   { background-position: 200% 0; }
        100% { background-position: -200% 0; }
    }

    /* Mobile β€” taller ratio on narrow screens, matches FC space cover behaviour */
    <a data-user_name="media" class="fcom_mention fcom_route" href="https://community.wpmanageninja.com/portal/u/media/">Media Rocketeer</a> (max-width: 640px) {
        #mn-course-hero {
            aspect-ratio: 16 / 7;
        }
        /* FC removes the gradient fade on mobile since it's shorter */
        #mn-course-hero::after {
            height: 30%;
        }
    }
    </style>

    <script>
    (function () {
        var AJAX_URL = '<?php echo esc_js( $ajax ); ?>';

        var injected     = false;
        var lastPath     = location.pathname;
        var lastSlug     = '';

        // ── Route guard ───────────────────────────────────────────
        function getCourseSlug() {
            // Matches /course/{slug}/lessons  AND  /course/{slug}/lessons/
            var m = location.pathname.match( /\/course\/([^/?#\/]+)\/lessons/ );
            return m ? m[1] : null;
        }

        // ── Build hero element ────────────────────────────────────
        function buildHero() {
            var el = document.createElement( 'div' );
            el.id        = 'mn-course-hero';
            el.className = 'mn-hero-loading';
            return el;
        }

        // ── Find the insertion anchor ─────────────────────────────
        // FC renders .fcom_course_intro inside a content wrapper.
        // We prepend the hero as the first sibling before it.
        function getAnchor() {
            return document.querySelector( '.fcom_course_intro' )
                || document.querySelector( '.fcom_content.fcom_course_content' )
                || document.querySelector( '.fcom_course_wrap' );
        }

        // ── Fetch cover and hydrate ───────────────────────────────
        function fetchAndHydrate( slug, heroEl ) {
            var body = new URLSearchParams( {
                action : 'mn_get_course_cover',
                slug   : slug,
            } );

            fetch( AJAX_URL, {
                method      : 'POST',
                credentials : 'same-origin',
                body        : body,
            } )
            .then( function (r) { return r.json(); } )
            .then( function (res) {
                heroEl.classList.remove( 'mn-hero-loading' );

                if ( ! res.success ) {
                    // No cover photo β€” remove the placeholder entirely
                    heroEl.remove();
                    return;
                }

                var img    = document.createElement( 'img' );
                img.src    = res.data.cover;
                img.alt    = '';
                img.loading = 'lazy';

                // If image fails to load, remove the hero rather than showing a broken slot
                img.onerror = function () { heroEl.remove(); };

                heroEl.appendChild( img );
            } )
            .catch( function () {
                heroEl.remove();
            } );
        }

        // ── Inject ────────────────────────────────────────────────
        function tryInject() {
            if ( document.getElementById( 'mn-course-hero' ) ) {
                injected = true;
                return;
            }

            var slug = getCourseSlug();
            if ( ! slug ) return;

            var anchor = getAnchor();
            if ( ! anchor ) return;

            var hero = buildHero();

            // Insert immediately before .fcom_course_intro (or its parent)
            anchor.parentNode.insertBefore( hero, anchor );

            fetchAndHydrate( slug, hero );
            lastSlug = slug;
            injected = true;
        }

        // ── SPA-aware interval loop ───────────────────────────────
        setInterval( function () {
            var currentPath = location.pathname;

            if ( currentPath !== lastPath ) {
                lastPath = currentPath;
                injected = false;
                lastSlug = '';
                var old = document.getElementById( 'mn-course-hero' );
                if ( old ) old.remove();
            }

            if ( ! injected && getCourseSlug() ) {
                tryInject();
            }
        }, 800 );

    })();
    </script>
    <?php
} );

Agus Mu

Jacob WindhamΒ thanks a lot!!!

Christian Frank

Jacob WindhamΒ This is cool. I'm going to try it out. :)