Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common-mainpage.js

MediaWiki interface page

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.

/**

* BattlestarWiki — Main Page JavaScript
* Append to MediaWiki:Common.js
*
* Handles:
*   1. Hero slideshow auto-rotation + article image fetching
*   2. Featured Article of the Day — daily deterministic pick,
*      excluding Category:Stub_Pages, with extract + thumbnail
*   3. Media wiki file count (from media.battlestarwiki.org)
*   4. Recent changes tab switching (EN / DE / Media wikis)
*   5. Red-link card filtering
*
* All logic is scoped to .bsw-main-page; safe to load globally.
*/

( function () {

   'use strict';
   var API       = 'https://en.battlestarwiki.org/w/api.php';
   var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php';
   var DE_API    = 'https://de.battlestarwiki.org/w/api.php';
   /* ── Shared utilities ─────────────────────────────────────────── */
   function dailySeed( dateStr ) {
       var d;
       if ( dateStr ) {
           var p = dateStr.split( '-' );
           d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
       } else {
           var now = new Date();
           d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
       }
       return Math.floor( d / 86400000 );
   }
   function esc( s ) {
       return String( s ||  )
           .replace( /&/g, '&' )
           .replace( /</g, '<' )
           .replace( />/g, '>' )
           .replace( /"/g, '"' );
   }
   function apiGetFrom( baseUrl, params ) {
       params.format = 'json';
       params.origin = '*';
       var qs = Object.keys( params )
           .map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
           .join( '&' );
       return fetch( baseUrl + '?' + qs, { headers: { Accept: 'application/json' } } )
           .then( function ( r ) {
               if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
               return r.json();
           } );
   }
   function apiGet( params ) {
       return apiGetFrom( API, params );
   }
   /* ════════════════════════════════════════════════════════════════
      1. HERO SLIDESHOW
      Dynamically populated from the wiki's most-viewed articles.
      Uses a daily seed so all visitors see the same 3 slides each day.
      Falls back to the static Lua-rendered slides if the API fails.
      ════════════════════════════════════════════════════════════════ */
   /* Series classification for badge color + label */
   var SERIES_CATS = [
       { pat: /\(RDM\)|\(TRS\)|Re-imagined|Reimagined/i,                    label: 'Re-imagined Series', color: 'var(--color-primary)' },
       { pat: /\(TOS\)|Original Series|\bTOS\b/i,                            label: 'Original Series',    color: '#ef9f27' },
       { pat: /\(1980\)|Galactica.?1980|Category:1980\b/i,                   label: 'Galactica 1980',     color: '#97c459' },
       { pat: /\(Caprica\)|Caprica \(series\)|Category:Caprica\b/i,          label: 'Caprica',            color: '#d4537e' },
       { pat: /\(BAC\)|Blood and Chrome|Blood.*Chrome|\bBAC\b/i,             label: 'Blood & Chrome',     color: '#e24b4a' }
   ];
   function classifyArticle( title, cats, extract ) {
       /* Check category names first */
       if ( cats ) {
           for ( var i = 0; i < SERIES_CATS.length; i++ ) {
               for ( var j = 0; j < cats.length; j++ ) {
                   if ( SERIES_CATS[i].pat.test( cats[j].title ) ) return SERIES_CATS[i];
               }
           }
       }
       /* Fall back to title matching */
       for ( var k = 0; k < SERIES_CATS.length; k++ ) {
           if ( SERIES_CATS[k].pat.test( title ) ) return SERIES_CATS[k];
       }
       /* Final fallback: scan extract for series keywords */
       if ( extract ) {
           var text = extract.replace( /<[^>]+>/g,  );
           for ( var m = 0; m < SERIES_CATS.length; m++ ) {
               if ( SERIES_CATS[m].pat.test( text ) ) return SERIES_CATS[m];
           }
       }
       return { label: 'Battlestar Wiki', color: 'var(--color-primary)' };
   }
   function buildSlideHTML( article, series, imgUrl ) {
       var title   = article.title;
       var url     = '/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
       var desc    = ( article.extract ||  ).replace( /<[^>]+>/g,  ).slice( 0, 160 );
       if ( desc.length === 160 ) desc = desc.replace( /\s\S+$/,  ) + '…';
       var badgeStyle = series.color ? ' style="background:' + series.color + '"' : ;

return '

' + '
' + '
' + '
' + '
' +
           '' +
           '' + esc( series.label ) + '' +
'
' + '
<a href="' + esc( url ) + '">' + esc( title ) + '</a>
' + '
' + esc( desc ) + '
' + '
' + '

';

   }
   function buildDotsAndNav( count ) {

var dots = '

';
       for ( var i = 0; i < count; i++ ) {
           var cls = 'bsw-hero-dot' + ( i === 0 ? ' bsw-active' :  );
           dots += '';
       }
dots += '

'; var nav = '

' +
           '' +
           '' +
'

';

       return dots + nav;
   }
   function initHeroSlideshow( hero, slides ) {
       var dots  = hero.querySelectorAll( '.bsw-hero-dot' );
       var current = 0;
       var total   = slides.length;
       var timer;
       function goTo( n ) {
           slides[ current ].classList.remove( 'bsw-active' );
           dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
           current = ( n + total ) % total;
           slides[ current ].classList.add( 'bsw-active' );
           dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
           resetTimer();
       }
       function resetTimer() {
           clearInterval( timer );
           timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
       }
       window.bswGoSlide   = function ( n ) { goTo( n ); };
       window.bswPrevSlide = function () { goTo( current - 1 ); };
       window.bswNextSlide = function () { goTo( current + 1 ); };
       dots.forEach( function ( dot, i ) {
           dot.addEventListener( 'click', function () { goTo( i ); } );
       } );
       var prevBtn = document.getElementById( 'bsw-hero-prev' );
       var nextBtn = document.getElementById( 'bsw-hero-next' );
       if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
       if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );
       hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
       hero.addEventListener( 'mouseleave', resetTimer );
       resetTimer();
   }
   function fetchSlideImages( hero, slides ) {
       var titles = Array.from( slides ).map( function ( s ) { return s.dataset.article; } ).filter( Boolean );
       if ( !titles.length ) return;
       apiGet( {
           action:      'query',
           titles:      titles.join( '|' ),
           prop:        'pageimages',
           piprop:      'thumbnail|original',
           pithumbsize: '1200',
           redirects:   '1'
       } ).then( function ( data ) {
           var pages = data.query.pages;
           var imageMap = {};
           var normalized = {};
           if ( data.query.normalized ) {
               data.query.normalized.forEach( function ( n ) { normalized[ n.to ] = n.from; } );
           }
           var redirects = {};
           if ( data.query.redirects ) {
               data.query.redirects.forEach( function ( r ) { redirects[ r.to ] = r.from; } );
           }
           Object.values( pages ).forEach( function ( page ) {
               if ( !page.title ) return;
               var img = ( page.original && page.original.source ) ||
                         ( page.thumbnail && page.thumbnail.source ) || ;
               if ( !img ) return;
               imageMap[ page.title ] = img;
               if ( redirects[ page.title ] ) imageMap[ redirects[ page.title ] ] = img;
               if ( normalized[ page.title ] ) imageMap[ normalized[ page.title ] ] = img;
           } );
           slides.forEach( function ( slide ) {
               var imgUrl = imageMap[ slide.dataset.article ];
               if ( !imgUrl ) return;
               var bg = slide.querySelector( '.bsw-slide-bg' );
               if ( !bg ) return;
               var bgImg = document.createElement( 'img' );
               bgImg.src = imgUrl;
               bgImg.alt = ;
               bgImg.className = 'bsw-slide-bg-blur';
               bgImg.setAttribute( 'aria-hidden', 'true' );
               var fgImg = document.createElement( 'img' );
               fgImg.src = imgUrl;
               fgImg.alt = ;
               fgImg.className = 'bsw-slide-bg-img';
               bg.appendChild( bgImg );
               bg.appendChild( fgImg );
           } );
       } ).catch( function () {} );
   }
   function initHero() {
       var hero = document.querySelector( '.bsw-hero' );
       if ( !hero ) return;
       var seed = dailySeed();
       var SLIDE_COUNT = 3;
       var FETCH_BATCH = 20; /* MW caps rnlimit at 20 for non-bots */
       /* Fetch two batches of random articles for a larger candidate pool */
       Promise.all( [
           apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } ),
           apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } )
       ] ).then( function ( results ) {
           var combined = results[0].query.random.concat( results[1].query.random );
           /* Deduplicate and filter */
           var seen = {};
           var items = combined.filter( function ( p ) {
               if ( seen[ p.title ] ) return false;
               seen[ p.title ] = true;
               return p.title !== 'Main Page' &&
                      p.title !== 'Main Page/New' &&
                      p.title.indexOf( '/' ) === -1;
           } );
           if ( !items.length ) throw new Error( 'No articles' );
           /* Cap at 20 titles to stay within MW API limits */
           items = items.slice( 0, 20 );
           /* Fetch extracts + images + categories for all candidates in one call */
           return apiGet( {
               action:      'query',
               titles:      items.map( function ( p ) { return p.title; } ).join( '|' ),
               prop:        'extracts|pageimages|categories|pageprops',
               exintro:     '1',
               exchars:     '200',
               exsectionformat: 'plain',
               piprop:      'thumbnail|original',
               pithumbsize: '1200',
               cllimit:     '500',
               ppprop:      'disambiguation',
               redirects:   '1'
           } );
       } ).then( function ( data ) {
           var pages = Object.values( data.query.pages || {} );
           /* Keep only articles that have an image and aren't disambiguation pages */
           var withImages = pages.filter( function ( p ) {
               if ( !p.thumbnail && !p.original ) return false;
               var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
               var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
                   cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
               var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
               return !isDisamb && !isStub;
           } );
           /* Fall back to all non-disambiguation pages if not enough have images */
           var pool = withImages.length >= SLIDE_COUNT ? withImages : pages.filter( function ( p ) {
               var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
               var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
                   cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
               var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
               return !isDisamb && !isStub;
           } );
           if ( pool.length < SLIDE_COUNT ) throw new Error( 'Not enough articles' );
           /* Pick SLIDE_COUNT articles using daily seed with spacing */
           var picks = [];
           for ( var i = 0; i < SLIDE_COUNT; i++ ) {
               picks.push( pool[ ( seed + i * 7 ) % pool.length ] );
           }
           /* Build slide HTML */
           var slideHTML = picks.map( function ( article, i ) {
               var cats   = article.categories || [];
               var series = classifyArticle( article.title, cats, article.extract );
               var html   = buildSlideHTML( article, series,  );
               return i === 0 ? html : html.replace( 'bsw-slide bsw-active', 'bsw-slide' );
           } ).join(  );
           hero.innerHTML = slideHTML + buildDotsAndNav( picks.length );
           var newSlides = hero.querySelectorAll( '.bsw-slide' );
           initHeroSlideshow( hero, newSlides );
           /* Inject images directly from the data we already fetched */
           picks.forEach( function ( article, i ) {
               var imgUrl = ( article.original && article.original.source ) ||
                            ( article.thumbnail && article.thumbnail.source ) || ;
               if ( !imgUrl ) return;
               var bg = newSlides[ i ] && newSlides[ i ].querySelector( '.bsw-slide-bg' );
               if ( !bg ) return;
               var bgImg = document.createElement( 'img' );
               bgImg.src = imgUrl;
               bgImg.alt = ;
               bgImg.className = 'bsw-slide-bg-blur';
               bgImg.setAttribute( 'aria-hidden', 'true' );
               var fgImg = document.createElement( 'img' );
               fgImg.src = imgUrl;
               fgImg.alt = ;
               fgImg.className = 'bsw-slide-bg-img';
               bg.appendChild( bgImg );
               bg.appendChild( fgImg );
           } );
       } ).catch( function ( err ) {
           /* Fallback: use static Lua-rendered slides */
           var staticSlides = hero.querySelectorAll( '.bsw-slide' );
           if ( staticSlides.length ) {
               /* Ensure first slide is active */
               staticSlides.forEach( function( s ) { s.classList.remove( 'bsw-active' ); } );
               staticSlides[0].classList.add( 'bsw-active' );
               initHeroSlideshow( hero, staticSlides );
               fetchSlideImages( hero, staticSlides );
           }
       } );
   }
   /* ════════════════════════════════════════════════════════════════
      2. FEATURED ARTICLE OF THE DAY
      Daily deterministic pick from all mainspace articles,
      excluding Category:Stub_Pages.
      ════════════════════════════════════════════════════════════════ */
   function initFeatured() {
       var container = document.getElementById( 'bsw-featured-inner' );
       if ( !container ) return;
       var seed = dailySeed();
       /* We fetch batches of 20 random articles, check for stubs,
          and pick the first clean one. Two batches covers ~99.99%
          of cases given only ~9% of articles are stubs. */
       fetchCandidateBatch( seed )
           .then( function ( pages ) {
               var clean = pages.filter( function ( p ) {
                   /* If categories array is present and contains Stub_Pages,
                      exclude. Absence of categories key means not a stub. */
                   if ( !p.categories ) return true;
                   return !p.categories.some( function ( c ) {
                       return c.title === 'Category:Stub_Pages';
                   } );
               } );
               if ( !clean.length ) {
                   /* Extremely unlikely — fall back to second batch */
                   return fetchCandidateBatch( seed + 1 );
               }
               return clean;
           } )
           .then( function ( pages ) {
               /* Pick deterministically from the clean pool */
               var pick = pages[ seed % pages.length ];
               renderFeatured( container, pick );
           } )
           .catch( function ( e ) {
               container.innerHTML =

'

Could not load featured article. ' + esc( e.message ) + '

';

           } );
   }
   /**
    * Fetch 20 random mainspace articles with extract, thumbnail,
    * and stub-category membership check in a single API call.
    * Uses rnstart derived from seed for determinism.
    */
   function fetchCandidateBatch( seed ) {
       /* Step 1: get 20 random page IDs */
       return apiGet( {
           action:      'query',
           list:        'random',
           rnnamespace: '0',
           rnlimit:     '20'
       } ).then( function ( data ) {
           var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );
           /* Step 2: batch fetch extract + thumbnail + stub check */
           return apiGet( {
               action:        'query',
               pageids:       ids,
               prop:          'extracts|pageimages|categories',
               exintro:       '1',
               exchars:       '600',
               exsectionformat: 'plain',
               piprop:        'thumbnail',
               pithumbsize:   '320',
               clcategories:  'Category:Stub_Pages',
               cllimit:       '1'
           } );
       } ).then( function ( data ) {
           return Object.values( data.query.pages );
       } );
   }
   function renderFeatured( container, page ) {
       var title   = page.title || ;
       var url     = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
       var extract = page.extract || ;
       var thumb   = page.thumbnail ? page.thumbnail.source : ;
       var thumbHtml;
       if ( thumb ) {
           thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '" width="90" height="110">';
       } else {
           thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" width="90" height="110">';
       }
       container.innerHTML =
           thumbHtml +

'

' + '
' +
                   '<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
'
' + '
' + extract + '
' + '
' +
                   '<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
'
' + '

';

       /* Update the "Read more" span in the card header to link to the article */
       var hdrLink = document.getElementById( 'bsw-featured-link' );
       if ( hdrLink ) {
           /* Replace the span with a proper anchor now that we have a URL */
           var a = document.createElement( 'a' );
           a.href = url;
           a.textContent = 'Read more \u2192';
           hdrLink.parentNode.replaceChild( a, hdrLink );
       }
   }
   /**
    * Fetch file count from media.battlestarwiki.org and populate
    * the #bsw-stat-files span in the statistics card.
    */
   function initMediaFileCount() {
       var el = document.getElementById( 'bsw-stat-files' );
       if ( !el ) return;
       apiGetFrom( MEDIA_API, {
           action: 'query',
           meta:   'siteinfo',
           siprop: 'statistics'
       } ).then( function ( data ) {
           var files = data.query.statistics.images || 0;
           el.textContent = files.toLocaleString();
       } ).catch( function () {
           el.textContent = '—';
       } );
   }
   /**
    * Recent changes tab switching.
    * Fetches live recent changes from the selected wiki's API
    * and replaces the #bsw-rc-content div content.
    */
   var rcCurrentWiki = 'all';
   var rcCache = {};
   function loadRecentChanges( wiki ) {
       var wrap = document.getElementById( 'bsw-rc-content' );
       if ( !wrap ) return;
       if ( rcCache[ wiki ] ) {
           wrap.innerHTML = rcCache[ wiki ];
           return;
       }
       /* 'all' shows the cached Special:RecentChanges transclude */
       if ( wiki === 'all' ) {
           if ( rcCache.all ) {
               wrap.innerHTML = rcCache.all;
           }
           return;
       }

wrap.innerHTML = '

Loading\u2026

';

       var apiMap = { en: API, de: DE_API, media: MEDIA_API };
       var base = apiMap[ wiki ];
       if ( !base ) return;
       var nsMap = {
           en:    '0',
           de:    '0',
           media: '0|6'  /* articles + files */
       };
       apiGetFrom( base, {
           action:      'query',
           list:        'recentchanges',
           rcprop:      'title|timestamp|user|sizes',
           rcnamespace: nsMap[ wiki ] || '0',
           rclimit:     '10',
           rctype:      'edit|new'
       } ).then( function ( data ) {
           var changes = ( data.query.recentchanges || [] ).filter( function ( rc ) {
               /* Filter out main page and subpages */
               return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1;
           } );
           changes = changes.filter( function ( rc ) {
               return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1;
           } );
           if ( !changes.length ) {

wrap.innerHTML = '

No recent changes.

';

               return;
           }
           var baseUrl = {
               en:    'https://en.battlestarwiki.org/wiki/',
               de:    'https://de.battlestarwiki.org/wiki/',
               media: 'https://media.battlestarwiki.org/wiki/'
           }[ wiki ];

var html = '

    ' + changes.map( function ( rc ) { var title = rc.title; var url = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) ); var ts = new Date( rc.timestamp ); var ago = timeAgo( ts ); return '
  • ' + '<a href="' + esc( url ) + '" target="_blank">' + esc( title ) + '</a>' + '' + esc( ago ) + '' + '
  • '; } ).join( ) + '

';

           rcCache[ wiki ] = html;
           wrap.innerHTML = html;
       } ).catch( function () {

wrap.innerHTML = '

Could not load recent changes.

';

       } );
   }
   function timeAgo( date ) {
       var diff = Math.floor( ( Date.now() - date.getTime() ) / 1000 );
       if ( diff < 60 )   return diff + 's ago';
       if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago';
       var hrs  = Math.floor( diff / 3600 );
       if ( diff < 86400 ) return hrs + ( hrs === 1 ? ' hr ago' : ' hrs ago' );
       var days = Math.floor( diff / 86400 );
       return days + ( days === 1 ? ' day ago' : ' days ago' );
   }
   /**
    * Newest article — pulls from Special:NewPages via API,
    * skips subpages, redirects, and disambiguation pages.
    */
   function initNewestArticle() {
       var inner = document.getElementById( 'bsw-newest-inner' );
       if ( !inner ) return;
       apiGet( {
           action:      'query',
           list:        'logevents',
           letype:      'create',
           lenamespace: '0',
           lelimit:     '20',
           leprop:      'title|timestamp'
       } ).then( function ( data ) {
           var entries = ( data.query.logevents || [] ).filter( function ( e ) {
               return e.title.indexOf( '/' ) === -1 && e.title !== 'Main Page';
           } );
           if ( !entries.length ) throw new Error( 'No entries' );
           var entry = entries[0];
           var title = entry.title;
           var url   = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
           var ts    = new Date( entry.timestamp );
           var date  = ts.toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
           return apiGet( {
               action:  'query',
               titles:  title,
               prop:    'extracts|pageimages|pageprops',
               exintro: '1',
               exchars: '500',
               exsectionformat: 'plain',
               piprop:  'thumbnail',
               pithumbsize: '200',
               ppprop:  'disambiguation'
           } ).then( function ( d ) {
               var page = Object.values( d.query.pages || {} )[0] || {};
               /* Skip disambiguation pages, try next */
               if ( page.pageprops && page.pageprops.disambiguation !== undefined ) {
                   var next = entries[1];
                   if ( !next ) throw new Error( 'No suitable page' );
                   title = next.title;
                   url   = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
                   date  = new Date( next.timestamp ).toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
                   return apiGet( {
                       action: 'query', titles: title,
                       prop: 'extracts|pageimages', exintro: '1', exchars: '300',
                       exsectionformat: 'plain', piprop: 'thumbnail', pithumbsize: '200'
                   } ).then( function ( d2 ) {
                       return { page: Object.values( d2.query.pages || {} )[0] || {}, title: title, url: url, date: date };
                   } );
               }
               return { page: page, title: title, url: url, date: date };
           } );
       } ).then( function ( result ) {
           var page    = result.page;
           var title   = result.title || page.title || ;
           var url     = result.url || ( 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( ( page.title ||  ).replace( / /g, '_' ) ) );
           var thumb   = page.thumbnail ? page.thumbnail.source : ;
           var extract = page.extract || ;
           var thumbHtml = thumb
               ? '<img src="' + esc( thumb ) + '" alt="' + esc( title ) + '" class="bsw-fa-thumb" width="90" height="110">'
               : '<img src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" class="bsw-fa-thumb" width="90" height="110">';
           /* Clean up extract — truncate at sentence boundary */
           var cleanExtract = extract.replace( /\s*\.\.\.\s*$/,  );
           /* Try to truncate at last sentence end within ~400 chars */
           var stripped = cleanExtract.replace( /<[^>]+>/g,  );
           if ( stripped.length > 400 ) {
               var lastPeriod = cleanExtract.lastIndexOf( '.', 500 );
               if ( lastPeriod > 200 ) {
                   cleanExtract = cleanExtract.slice( 0, lastPeriod + 1 );
               }
           }
           inner.innerHTML =
               thumbHtml +

'

' + '
<a href="' + esc( url ) + '">' + esc( title ) + '</a>
' + '
Created ' + esc( result.date ) + '
' + '
' + cleanExtract + '
' + '
' +
               '<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
'
' + '

';

       } ).catch( function ( err ) {

inner.innerHTML = '

Could not load newest article.

';

       } );
   }
   /* ── Entry point ──────────────────────────────────────────────── */
   /**
    * Hide any .bsw-card whose .bsw-card-body contains nothing but
    * a red link (missing page transclusion). This catches subpages
    * like BattlestarWiki:Did_you_know, OTD/Month_Day etc. that
    * haven't been created yet.
    */
   function filterRedlinkCards() {
       document.querySelectorAll( '.bsw-main-page .bsw-card' ).forEach( function ( card ) {
           /* Skip cards that are JS-populated — they start with a loading spinner
              and will be filled in asynchronously */
           if ( card.querySelector( '.bsw-loading, .bsw-spinner' ) ) return;
           var body = card.querySelector( '.bsw-card-body' );
           if ( !body ) return;
           /* Remove non-content elements before checking — date labels,
              loading spinners etc. shouldn't count as "content" */
           var clone = body.cloneNode( true );
           /* Remove known non-content wrappers */
           clone.querySelectorAll( '.bsw-otd-date, .bsw-spinner, .bsw-loading' )
                .forEach( function ( el ) { el.remove(); } );
           var text     = clone.textContent.trim();
           var links    = clone.querySelectorAll( 'a' );
           var redLinks = clone.querySelectorAll( 'a.new' );
           /* Hide if nothing left after stripping non-content */
           if ( text ===  ) {
               card.style.display = 'none';
               return;
           }
           /* Hide if the only remaining content is red links */
           if ( redLinks.length > 0 && redLinks.length === links.length ) {
               clone.querySelectorAll( 'a' ).forEach( function ( a ) { a.remove(); } );
               if ( clone.textContent.trim() ===  ) {
                   card.style.display = 'none';
               }
           }
       } );
   }
   /* bswSetTab — exposed globally as fallback for inline onclick */
   window.bswSetTab = function ( el, wiki ) {
       document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
           t.classList.remove( 'bsw-active' );
       } );
       el.classList.add( 'bsw-active' );
       rcCurrentWiki = wiki;
       loadRecentChanges( wiki );
   };
   function wireRcTabs() {
       var tabMap = { 'All': 'all', 'EN': 'en', 'DE': 'de', 'Media': 'media' };
       document.querySelectorAll( '.bsw-wtab' ).forEach( function ( tab ) {
           var wiki = tabMap[ tab.textContent.trim() ];
           if ( !wiki ) return;
           tab.addEventListener( 'click', function () {
               document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
                   t.classList.remove( 'bsw-active' );
               } );
               tab.classList.add( 'bsw-active' );
               rcCurrentWiki = wiki;
               loadRecentChanges( wiki );
           } );
       } );
   }
   mw.hook( 'wikipage.content' ).add( function () {
       if ( !document.querySelector( '.bsw-main-page' ) ) return;
       initHero();
       initFeatured();
       initMediaFileCount();
       initNewestArticle();
       filterRedlinkCards();
       wireRcTabs();
       /* Cache the initial 'all' RC content so tab switching can restore it */
       var rcWrap = document.getElementById( 'bsw-rc-content' );
       if ( rcWrap ) rcCache.all = rcWrap.innerHTML;
   } );

}() );