Hi Karl,
yes, your understanding is basically correct.
The JavaScript checks the relevant gallery/category pages in the background and then reads the
value from the individual gallery pages. If the timestamp is within the last 30 days, that entry is considered “NEW”. Based on that result, the script adds the NEW badge to the individual gallery entry, the gallery category and also the main Gallery menu item.
To keep the number of requests down, the result is cached locally for one hour, so it does not have to re-check everything on every page load.
Regarding the sorting issue, I was able to clarify that in the meantime. The problem was simply that I had not entered the optional date for some gallery folders. After adding the missing dates, the folder order in the gallery is correct again.
So the remaining issue is really only the NEW logic: if I update an older gallery entry, its
changes and my script then treats it as new again for 30 days, even though the entry itself may have been published much earlier.
Ideally, I would therefore prefer to base the NEW badge on the original publication/upload date rather than the last modified date.
Usually, this works even if you don't update older posts.
Maybe we could explore one of your ideas using a proper PHP solution?
Below are the current complete JavaScript and CSS versions I’m using on my X3 site.
The code has grown a bit because it now handles several related functions, not just the NEW badges. The relevant part for the NEW functionality is mainly the article and gallery date checking.
The current logic is:
- Content is considered NEW for 30 days.
- Blog articles within that period receive a NEW badge in the article dropdown.
- If at least one recent article exists, the main Articles menu item also receives a NEW badge.
- The same logic is applied to the gallery.
- The JavaScript first loads the gallery overview to determine the available gallery categories.
- It then checks the entries inside those categories and loads the relevant gallery detail pages.
- On each gallery detail page, it currently reads:
<meta property="og:updated_time" ...>
- The value is converted into a date and compared with the 30-day limit.
- If one or more recent entries are found, the corresponding Gallery menu item, gallery category and individual gallery entries receive a NEW badge.
- Multiple gallery entries can therefore be marked NEW at the same time.
To reduce the number of requests, the gallery result is cached in
. The cached result is used immediately on subsequent page loads and the complete gallery check is only repeated after one hour.
Gallery categories are checked in parallel. Within each category, the entries are checked from newest to oldest and the script stops as soon as it reaches the first entry outside the 30-day window. This avoids unnecessarily loading all older gallery pages.
There are also a few unrelated functions in the same JavaScript, such as the
/
homepage overlays and some modifications to the article dropdown, so the NEW functionality is only one part of the complete script.
The main weakness, as discussed, is currently the use of
: editing an older gallery entry changes this timestamp and can therefore make an old entry appear NEW again.
Javascript
function x3_load() {
/* =========================================================
GLOBAL SETTINGS
========================================================= */
const NEW_DAYS = 30;
const NEW_MAX_AGE = NEW_DAYS * 24 * 60 * 60 * 1000;
const GALLERY_CACHE_REFRESH_AGE = 60 * 60 * 1000; // 1 hour
const GALLERY_CACHE_DISPLAY_AGE = 24 * 60 * 60 * 1000; // 24 hours
const GALLERY_CACHE_KEY = 'linsenschuss-gallery-new-state-v2';
/* =========================================================
HELPER FUNCTIONS
========================================================= */
function normalizePath(href) {
try {
let path = new URL(href, window.location.origin).pathname;
if (!path.endsWith('/')) {
path += '/';
}
return path.toLowerCase();
} catch (e) {
return '';
}
}
function getOwnText(element) {
return Array.from(element.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE)
.map((node) => node.nodeValue)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function findMainMenuLink(label) {
const target = label.toUpperCase();
const menuLinks = document.querySelectorAll('.menu > li > a');
for (const menuLink of menuLinks) {
if (getOwnText(menuLink).toUpperCase() === target) {
return menuLink;
}
}
return null;
}
function setMainMenuBadge(label, showBadge) {
const menuLink = findMainMenuLink(label);
if (!menuLink) {
return false;
}
const existingBadge = menuLink.querySelector('.main-menu-new-badge');
if (showBadge) {
if (!existingBadge) {
const badge = document.createElement('span');
badge.className = 'main-menu-new-badge';
badge.textContent = 'NEU';
menuLink.appendChild(badge);
}
} else if (existingBadge) {
existingBadge.remove();
}
return true;
}
function isRecentDate(date) {
if (!date || Number.isNaN(date.getTime())) {
return false;
}
const age = Date.now() - date.getTime();
return age >= 0 && age <= NEW_MAX_AGE;
}
function styleGalleryBadge(badge) {
badge.style.cssText = `
display: inline-block !important;
margin-left: 7px !important;
padding: 1px 4px !important;
background: rgba(70, 70, 70, 0.9) !important;
color: #ffffff !important;
font-size: 10px !important;
font-weight: 500 !important;
line-height: 1 !important;
letter-spacing: 0.03em !important;
border-radius: 2px !important;
white-space: nowrap !important;
vertical-align: 2px !important;
`;
}
function createGalleryBadge(className) {
const badge = document.createElement('span');
badge.className = className;
badge.textContent = 'NEU';
styleGalleryBadge(badge);
return badge;
}
function isGalleryOverviewPath(path) {
return path === '/galerie/';
}
function isGalleryCategoryPath(path) {
return /^\/galerie\/[^/]+\/$/i.test(path) && path !== '/galerie/';
}
/* =========================================================
ARTICLE LIST HELP TEXT
========================================================= */
document.querySelectorAll('.list-inner a:first-child').forEach((heading) => {
if (!heading.parentElement.querySelector('.blog-list-help-text')) {
heading.insertAdjacentHTML(
'afterend',
'<span class="blog-list-help-text"></span>'
);
}
});
/* =========================================================
HOMEPAGE: [mehr] AND [artikel] MARKERS
========================================================= */
const markerWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT
);
const markerNodes = [];
let markerNode;
while (markerNode = markerWalker.nextNode()) {
if (
markerNode.nodeValue.includes('[mehr]') ||
markerNode.nodeValue.includes('[artikel]')
) {
markerNodes.push(markerNode);
}
}
markerNodes.forEach((textNode) => {
const isArticle = textNode.nodeValue.includes('[artikel]');
const isMore = textNode.nodeValue.includes('[mehr]');
const link = textNode.parentElement.closest('a');
textNode.nodeValue = textNode.nodeValue.replace(
/\s*\[(mehr|artikel)\]\s*/g,
''
);
if (!link) return;
const img = link.querySelector('img');
if (!img) return;
/* ARTICLE */
if (isArticle) {
if (link.querySelector('.article-overlay')) return;
link.classList.add(
'has-more-overlay',
'has-article-overlay'
);
img.insertAdjacentHTML(
'afterend',
'<span class="more-overlay article-overlay">Artikel lesen →</span>' +
'<span class="article-badge">ARTIKEL</span>'
);
return;
}
/* NORMAL POST */
if (isMore) {
if (link.querySelector('.more-overlay')) return;
link.classList.add('has-more-overlay');
img.insertAdjacentHTML(
'afterend',
'<span class="more-overlay">Beitrag ansehen →</span>'
);
}
});
/* =========================================================
ARTICLES
========================================================= */
const articleDates = new Map();
const loadedArticleCategories = new Set();
const loadingArticleCategories = new Set();
/* ---------------------------------------------------------
LOAD DATES FROM AN ARTICLE CATEGORY
--------------------------------------------------------- */
async function loadArticleCategoryDates(categoryUrl) {
const categoryPath = normalizePath(categoryUrl);
if (!categoryPath) return;
if (
loadedArticleCategories.has(categoryPath) ||
loadingArticleCategories.has(categoryPath)
) {
return;
}
loadingArticleCategories.add(categoryPath);
try {
const response = await fetch(
categoryUrl +
(categoryUrl.includes('?') ? '&' : '?') +
'datecheck=' + Date.now(),
{
cache: 'no-store'
}
);
if (!response.ok) return;
const html = await response.text();
const doc = new DOMParser().parseFromString(
html,
'text/html'
);
const datePattern =
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2}|Z)\b/;
const dateWalker = doc.createTreeWalker(
doc.body,
NodeFilter.SHOW_TEXT
);
let dateNode;
while (dateNode = dateWalker.nextNode()) {
const match = dateNode.nodeValue.match(datePattern);
if (!match) continue;
const parent = dateNode.parentElement;
const articleLink =
parent.closest('a') ||
parent.closest('li')?.querySelector('a[href]');
if (!articleLink) continue;
const path = normalizePath(
articleLink.getAttribute('href')
);
if (!path) continue;
articleDates.set(
path,
new Date(match[0])
);
}
loadedArticleCategories.add(categoryPath);
} catch (e) {
console.warn(
'Article dates could not be loaded:',
categoryUrl
);
} finally {
loadingArticleCategories.delete(categoryPath);
}
}
/* ---------------------------------------------------------
CHECK IF AT LEAST ONE ARTICLE IS NEW
--------------------------------------------------------- */
function hasRecentArticle() {
for (const published of articleDates.values()) {
if (isRecentDate(published)) {
return true;
}
}
return false;
}
/* ---------------------------------------------------------
ADD NEW BADGES TO INDIVIDUAL ARTICLES
--------------------------------------------------------- */
function updateArticleBadges() {
document.querySelectorAll(
'.mega-list-deep .list-inner > li > div'
).forEach((group) => {
const links = Array.from(group.children).filter(
(element) => element.tagName === 'A'
);
if (links.length < 2) return;
links.slice(1).forEach((article) => {
const path = normalizePath(
article.getAttribute('href')
);
const published = articleDates.get(path);
const existingBadge =
article.querySelector('.menu-new-inline');
const isNew =
published
? isRecentDate(published)
: false;
if (isNew) {
if (!existingBadge) {
const badge = document.createElement('span');
badge.className = 'menu-new-inline';
badge.textContent = 'NEU';
article.prepend(badge);
}
} else if (existingBadge) {
existingBadge.remove();
}
});
});
}
/* ---------------------------------------------------------
"ALLE ARTIKEL ANZEIGEN →"
--------------------------------------------------------- */
function updateAllArticleLinks() {
document.querySelectorAll(
'.mega-list-deep .list-inner > li > div'
).forEach((group) => {
const categoryLink =
Array.from(group.children).find(
(element) => element.tagName === 'A'
);
if (!categoryLink?.href) return;
if (group.querySelector('.all-articles-wrap')) return;
const wrapper = document.createElement('div');
wrapper.className = 'all-articles-wrap';
const allLink = document.createElement('a');
allLink.className = 'all-articles-link';
allLink.href = categoryLink.href;
allLink.textContent = 'Alle Artikel anzeigen →';
wrapper.appendChild(allLink);
group.appendChild(wrapper);
});
}
/* ---------------------------------------------------------
ARTICLES NEW BADGE IN MAIN MENU
--------------------------------------------------------- */
function updateMainArticleBadge() {
setMainMenuBadge(
'ARTIKEL',
hasRecentArticle()
);
}
async function preloadArticleDates() {
const categories = [
'/blog/Fotografie/',
'/blog/Astrofotografie/'
];
await Promise.all(
categories.map(
(url) => loadArticleCategoryDates(url)
)
);
updateMainArticleBadge();
}
/* =========================================================
GALLERY
========================================================= */
const galleryCategoryStates = new Map();
const galleryCategoryTitles = new Map();
const galleryRecentEntries = new Map();
let galleryCheckPromise = null;
/* =========================================================
GALLERY CACHE
========================================================= */
function applyGalleryCacheData(cache) {
if (
cache.states &&
typeof cache.states === 'object'
) {
Object.entries(cache.states).forEach(
([path, isNew]) => {
galleryCategoryStates.set(
path,
Boolean(isNew)
);
}
);
}
if (
cache.titles &&
typeof cache.titles === 'object'
) {
Object.entries(cache.titles).forEach(
([path, title]) => {
if (typeof title === 'string') {
galleryCategoryTitles.set(
path,
title
);
}
}
);
}
if (
cache.recentEntries &&
typeof cache.recentEntries === 'object'
) {
Object.entries(cache.recentEntries).forEach(
([categoryPath, entryPaths]) => {
if (!Array.isArray(entryPaths)) return;
galleryRecentEntries.set(
categoryPath,
new Set(
entryPaths.filter(
(entryPath) => typeof entryPath === 'string'
)
)
);
}
);
}
}
function readGalleryCache(key) {
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
const cache = JSON.parse(raw);
if (
!cache ||
typeof cache.checkedAt !== 'number'
) {
return null;
}
return cache;
} catch (e) {
return null;
}
}
function loadGalleryCache() {
const cache = readGalleryCache(GALLERY_CACHE_KEY);
if (!cache) {
return {
available: false,
fresh: false
};
}
const cacheAge = Date.now() - cache.checkedAt;
if (
cacheAge < 0 ||
cacheAge > GALLERY_CACHE_DISPLAY_AGE
) {
return {
available: false,
fresh: false
};
}
applyGalleryCacheData(cache);
return {
available: true,
fresh: cacheAge <= GALLERY_CACHE_REFRESH_AGE
};
}
function saveGalleryCache() {
try {
const states = {};
const titles = {};
const recentEntries = {};
galleryCategoryStates.forEach((isNew, path) => {
states[path] = isNew;
});
galleryCategoryTitles.forEach((title, path) => {
titles[path] = title;
});
galleryRecentEntries.forEach((entryPaths, categoryPath) => {
recentEntries[categoryPath] = Array.from(entryPaths);
});
localStorage.setItem(
GALLERY_CACHE_KEY,
JSON.stringify({
checkedAt: Date.now(),
states: states,
titles: titles,
recentEntries: recentEntries
})
);
} catch (e) {
console.warn(
'Gallery cache could not be saved.'
);
}
}
/* =========================================================
GALLERY DATE HANDLING
========================================================= */
function parseGalleryUpdatedTime(value) {
if (!value) return null;
const trimmed = value.trim();
/*
* X3 currently returns og:updated_time
* as a Unix timestamp in seconds.
*/
if (/^\d+$/.test(trimmed)) {
const unixSeconds = Number(trimmed);
if (!Number.isFinite(unixSeconds)) {
return null;
}
return new Date(
unixSeconds * 1000
);
}
/*
* Fallback in case X3 returns
* a normal date format in the future.
*/
const parsed = new Date(trimmed);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return parsed;
}
function findGalleryEntries(
doc,
categoryPath
) {
const links = Array.from(
doc.querySelectorAll(
'main a[href], #content a[href]'
)
);
const entries = new Map();
for (const link of links) {
const href = link.getAttribute('href');
const path = normalizePath(href);
if (!path) continue;
if (!path.startsWith(categoryPath)) continue;
if (path === categoryPath) continue;
const remainingPath =
path
.slice(categoryPath.length)
.replace(/\/$/, '');
/*
* Only direct gallery entries/subfolders
* below the category are accepted.
*/
if (
!remainingPath ||
remainingPath.includes('/')
) {
continue;
}
if (!entries.has(path)) {
entries.set(
path,
{
path: path,
url: new URL(
href,
window.location.origin
).href
}
);
}
}
return Array.from(entries.values());
}
/* =========================================================
GALLERY NEW BADGE IN MAIN MENU
========================================================= */
function hasRecentGallery() {
return Array.from(
galleryCategoryStates.values()
).some(
(isNew) => isNew
);
}
function updateMainGalleryBadge() {
return setMainMenuBadge(
'GALERIE',
hasRecentGallery()
);
}
/*
* X3 builds parts of the main menu dynamically.
*
* If the gallery cache is already available,
* this function inserts the NEW badge as soon
* as the Gallery menu item exists in the DOM.
*/
function ensureMainGalleryBadgeWhenReady(
attempt = 0
) {
if (updateMainGalleryBadge()) {
return;
}
if (attempt >= 120) {
return;
}
requestAnimationFrame(
() => {
ensureMainGalleryBadgeWhenReady(
attempt + 1
);
}
);
}
/* =========================================================
NEW BADGES FOR CATEGORIES ON /galerie/
========================================================= */
function updateGalleryCategoryBadges() {
const currentPath = normalizePath(
window.location.href
);
if (!isGalleryOverviewPath(currentPath)) {
return;
}
const headings = document.querySelectorAll(
'main h1, main h2, main h3, main h4, main h5, main h6,' +
'#content h1, #content h2, #content h3, #content h4, #content h5, #content h6'
);
galleryCategoryStates.forEach(
(isNew, categoryPath) => {
const title = galleryCategoryTitles.get(
categoryPath
);
if (!title) return;
for (const heading of headings) {
const headingText = getOwnText(heading);
if (headingText !== title) continue;
const existingBadge =
heading.querySelector(
'.gallery-category-new-badge'
);
if (isNew) {
if (!existingBadge) {
heading.appendChild(
createGalleryBadge(
'gallery-category-new-badge'
)
);
}
} else if (existingBadge) {
existingBadge.remove();
}
break;
}
}
);
}
/* =========================================================
NEW BADGES FOR INDIVIDUAL GALLERY ENTRIES
========================================================= */
function findGalleryEntryBadgeTarget(
entryPath
) {
const links = Array.from(
document.querySelectorAll(
'main a[href], #content a[href]'
)
).filter(
(link) =>
normalizePath(
link.getAttribute('href')
) === entryPath
);
if (!links.length) return null;
for (const link of links) {
const headingInside = link.querySelector(
'h1, h2, h3, h4, h5, h6'
);
if (headingInside) {
return headingInside;
}
const headingParent = link.closest(
'h1, h2, h3, h4, h5, h6'
);
if (headingParent) {
return headingParent;
}
}
const textLinkWithoutImage = links.find(
(link) =>
link.textContent.trim() &&
!link.querySelector('img')
);
if (textLinkWithoutImage) {
return textLinkWithoutImage;
}
const textLink = links.find(
(link) => link.textContent.trim()
);
if (textLink) {
return textLink;
}
for (const link of links) {
const container = link.closest(
'article, figure, li, .card, .item, [class*="card"], [class*="item"]'
);
const heading = container?.querySelector(
'h1, h2, h3, h4, h5, h6'
);
if (heading) {
return heading;
}
}
return null;
}
function updateGalleryEntryBadges() {
const currentPath = normalizePath(
window.location.href
);
if (!isGalleryCategoryPath(currentPath)) {
return;
}
document.querySelectorAll(
'.gallery-entry-new-badge'
).forEach(
(badge) => badge.remove()
);
const recentEntries =
galleryRecentEntries.get(
currentPath
);
if (
!recentEntries ||
recentEntries.size === 0
) {
return;
}
recentEntries.forEach(
(entryPath) => {
const target =
findGalleryEntryBadgeTarget(
entryPath
);
if (!target) return;
target.appendChild(
createGalleryBadge(
'gallery-entry-new-badge'
)
);
}
);
}
function updateAllGalleryBadges() {
updateMainGalleryBadge();
updateGalleryCategoryBadges();
updateGalleryEntryBadges();
}
/* =========================================================
CHECK ONE GALLERY CATEGORY
========================================================= */
async function checkSingleGalleryCategory(
categoryPath,
category
) {
if (category.title) {
galleryCategoryTitles.set(
categoryPath,
category.title
);
}
try {
const categoryResponse = await fetch(
category.url +
(category.url.includes('?') ? '&' : '?') +
'gallerycategory=' +
Date.now(),
{
cache: 'no-store'
}
);
if (!categoryResponse.ok) {
return {
categoryPath: categoryPath,
success: false,
recentEntries: []
};
}
const categoryHtml =
await categoryResponse.text();
const categoryDoc =
new DOMParser().parseFromString(
categoryHtml,
'text/html'
);
const entries = findGalleryEntries(
categoryDoc,
categoryPath
);
const recentEntries = [];
/*
* Gallery entries are returned by X3
* in the configured gallery order.
*
* The script assumes that the newest
* entries are listed first.
*
* As soon as the first entry older than
* 30 days is reached, the loop stops.
*/
for (const entry of entries) {
const detailResponse = await fetch(
entry.url +
(entry.url.includes('?') ? '&' : '?') +
'gallerydate=' +
Date.now(),
{
cache: 'no-store'
}
);
if (!detailResponse.ok) {
return {
categoryPath: categoryPath,
success: false,
recentEntries: recentEntries
};
}
const detailHtml =
await detailResponse.text();
const detailDoc =
new DOMParser().parseFromString(
detailHtml,
'text/html'
);
const updatedValue =
detailDoc
.querySelector(
'meta[property="og:updated_time"]'
)
?.getAttribute(
'content'
);
const updatedDate =
parseGalleryUpdatedTime(
updatedValue
);
if (!updatedDate) {
return {
categoryPath: categoryPath,
success: false,
recentEntries: recentEntries
};
}
if (isRecentDate(updatedDate)) {
recentEntries.push(
entry.path
);
continue;
}
break;
}
return {
categoryPath: categoryPath,
success: true,
recentEntries: recentEntries
};
} catch (e) {
console.warn(
'Gallery category could not be checked:',
category.url
);
return {
categoryPath: categoryPath,
success: false,
recentEntries: []
};
}
}
/* =========================================================
CHECK ALL GALLERY CATEGORIES
========================================================= */
async function checkGalleryCategories() {
if (galleryCheckPromise) {
return galleryCheckPromise;
}
galleryCheckPromise =
(async () => {
try {
const galleryResponse = await fetch(
'/galerie/?gallerycheck=' +
Date.now(),
{
cache: 'no-store'
}
);
if (!galleryResponse.ok) return;
const galleryHtml =
await galleryResponse.text();
const galleryDoc =
new DOMParser().parseFromString(
galleryHtml,
'text/html'
);
const categoryLinks = Array.from(
galleryDoc.querySelectorAll(
'main a[href], #content a[href]'
)
);
const categories = new Map();
categoryLinks.forEach(
(link) => {
const path = normalizePath(
link.getAttribute('href')
);
if (!path) return;
if (!isGalleryCategoryPath(path)) {
return;
}
const title =
link.textContent
.trim()
.replace(
/\s+/g,
' '
);
if (!categories.has(path)) {
categories.set(
path,
{
url: new URL(
link.getAttribute('href'),
window.location.origin
).href,
title: title
}
);
} else if (title) {
const existing =
categories.get(path);
if (!existing.title) {
existing.title = title;
}
}
}
);
/*
* Gallery categories are checked in parallel.
*/
const results = await Promise.all(
Array.from(
categories.entries()
).map(
([categoryPath, category]) =>
checkSingleGalleryCategory(
categoryPath,
category
)
)
);
let successfulChecks = 0;
let usableResults = 0;
results.forEach(
(result) => {
const hasUsableData =
result.success ||
result.recentEntries.length > 0;
if (!hasUsableData) {
return;
}
const recentSet =
new Set(
result.recentEntries
);
galleryRecentEntries.set(
result.categoryPath,
recentSet
);
galleryCategoryStates.set(
result.categoryPath,
recentSet.size > 0
);
usableResults += 1;
if (result.success) {
successfulChecks += 1;
}
}
);
updateAllGalleryBadges();
/*
* Save the cache as soon as at least one
* usable gallery result is available.
*
* This allows the Gallery NEW badge to be
* displayed immediately on subsequent page loads.
*/
if (usableResults > 0) {
saveGalleryCache();
}
if (
successfulChecks < categories.size
) {
console.info(
'Gallery check partially completed:',
successfulChecks +
' of ' +
categories.size +
' categories fully checked.'
);
}
} catch (e) {
console.warn(
'Gallery could not be checked for new content.'
);
} finally {
galleryCheckPromise = null;
}
})();
return galleryCheckPromise;
}
/* =========================================================
SYNCHRONIZE ARTICLE DROPDOWN
========================================================= */
async function syncArticleMenu() {
const groups = document.querySelectorAll(
'.mega-list-deep .list-inner > li > div'
);
if (!groups.length) {
updateMainArticleBadge();
return;
}
updateAllArticleLinks();
const categoryUrls = new Set();
groups.forEach(
(group) => {
const categoryLink =
Array.from(group.children).find(
(element) => element.tagName === 'A'
);
if (categoryLink?.href) {
categoryUrls.add(
categoryLink.href
);
}
}
);
await Promise.all(
Array.from(categoryUrls).map(
(url) =>
loadArticleCategoryDates(
url
)
)
);
updateArticleBadges();
updateMainArticleBadge();
}
/* =========================================================
INITIALIZATION
========================================================= */
const galleryCache =
loadGalleryCache();
/*
* Immediately use an existing gallery cache.
*/
if (galleryCache.available) {
updateGalleryCategoryBadges();
updateGalleryEntryBadges();
ensureMainGalleryBadgeWhenReady();
}
/*
* Check articles live.
*/
preloadArticleDates();
/*
* Only perform a full gallery check if the
* cache is older than one hour or if no
* gallery cache exists yet.
*/
if (!galleryCache.fresh) {
checkGalleryCategories();
}
syncArticleMenu();
/* =========================================================
WATCH DYNAMIC X3 CONTENT
========================================================= */
if (window.x3ArticleMenuObserver) {
window.x3ArticleMenuObserver.disconnect();
}
let articleSyncScheduled = false;
let gallerySyncScheduled = false;
let mainMenuSyncScheduled = false;
function scheduleArticleSync() {
if (articleSyncScheduled) return;
articleSyncScheduled = true;
requestAnimationFrame(
() => {
articleSyncScheduled = false;
syncArticleMenu();
}
);
}
function scheduleGallerySync() {
if (gallerySyncScheduled) return;
gallerySyncScheduled = true;
requestAnimationFrame(
() => {
gallerySyncScheduled = false;
updateGalleryCategoryBadges();
updateGalleryEntryBadges();
}
);
}
function scheduleMainMenuSync() {
if (mainMenuSyncScheduled) return;
mainMenuSyncScheduled = true;
requestAnimationFrame(
() => {
mainMenuSyncScheduled = false;
updateMainGalleryBadge();
if (articleDates.size > 0) {
updateMainArticleBadge();
}
}
);
}
/*
* Detect elements inserted by this script itself
* so that they do not unnecessarily trigger
* another synchronization cycle.
*/
function isOwnInjectedElement(node) {
if (!(node instanceof Element)) {
return false;
}
return node.matches(
'.blog-list-help-text,' +
'.menu-new-inline,' +
'.main-menu-new-badge,' +
'.all-articles-wrap,' +
'.gallery-category-new-badge,' +
'.gallery-entry-new-badge,' +
'.more-overlay,' +
'.article-badge'
);
}
function mutationTouchesMainMenu(
mutation
) {
const target = mutation.target;
if (
target instanceof Element &&
(
target.matches('.menu') ||
target.closest('.menu')
)
) {
return true;
}
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (isOwnInjectedElement(node)) continue;
if (
node.matches(
'.menu, .menu > li, .menu > li > a'
) ||
node.querySelector('.menu')
) {
return true;
}
}
return false;
}
function mutationTouchesArticleMenu(
mutation
) {
const target = mutation.target;
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
if (isOwnInjectedElement(node)) continue;
if (
node.matches('.mega-list-deep') ||
node.querySelector('.mega-list-deep')
) {
return true;
}
}
if (
target instanceof Element &&
target.closest('.mega-list-deep')
) {
return true;
}
}
return false;
}
function mutationTouchesGalleryContent(
mutation
) {
const currentPath = normalizePath(
window.location.href
);
if (
!isGalleryOverviewPath(currentPath) &&
!isGalleryCategoryPath(currentPath)
) {
return false;
}
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
if (isOwnInjectedElement(node)) continue;
return true;
}
if (
node.nodeType === Node.TEXT_NODE
) {
return true;
}
}
return false;
}
window.x3ArticleMenuObserver =
new MutationObserver(
(mutations) => {
let needsMainMenuSync = false;
let needsArticleSync = false;
let needsGallerySync = false;
for (const mutation of mutations) {
if (
!needsMainMenuSync &&
mutationTouchesMainMenu(
mutation
)
) {
needsMainMenuSync = true;
}
if (
!needsArticleSync &&
mutationTouchesArticleMenu(
mutation
)
) {
needsArticleSync = true;
}
if (
!needsGallerySync &&
mutationTouchesGalleryContent(
mutation
)
) {
needsGallerySync = true;
}
if (
needsMainMenuSync &&
needsArticleSync &&
needsGallerySync
) {
break;
}
}
if (needsMainMenuSync) {
scheduleMainMenuSync();
}
if (needsArticleSync) {
scheduleArticleSync();
}
if (needsGallerySync) {
scheduleGallerySync();
}
}
);
window.x3ArticleMenuObserver.observe(
document.body,
{
childList: true,
subtree: true
}
);
}
CSS:
/* =========================================================
MAIN MENU
========================================================= */
/* Hover effect */
body[class*="topbar"] .menu > li > a:hover {
color: white !important;
}
/* Highlight active menu item */
body[class*="topbar"] .menu > li > a.active {
background: #222 !important;
color: white;
font-weight: bolder;
}
/* =========================================================
GENERAL DISPLAY
========================================================= */
/* Image captions styled like the blog */
.images figcaption {
background: #2a2a2a;
font-weight: 400;
}
/* Make before/after comparison slider wider */
@media (min-width: 1024px) {
.comparison-slider-wrapper {
margin: 0 -100px;
}
}
/* Remove line limitation in popup captions */
.popup-caption-description {
display: block !important;
}
/* Card title spacing */
.card .title {
margin-bottom: 1rem;
}
/* Mobile logo position */
@media screen and (max-width: 639px) {
.logo-wrapper {
text-align: left;
margin-left: 15px;
}
.logo > img {
max-width: 80vw;
}
}
/* =========================================================
ARTICLE DROPDOWN
========================================================= */
/* Help text above article lists */
.blog-list-help-text:after {
content: 'Die neusten Artikel';
display: block;
margin: 0 0 .5em 1.9em;
opacity: 1;
font-weight: 400;
}
/* Show exactly 7 articles per category in the dropdown */
.mega-list-deep .list-inner > li > div > a:nth-of-type(n+9) {
display: none !important;
}
/* Article links as positioning reference for bullet points */
.mega-list-deep .list-inner > li > div > a:not(:first-of-type) {
position: relative;
}
/* Bullet point before every article */
.mega-list-deep .list-inner > li > div > a:not(:first-of-type)::before {
content: '•' !important;
display: block !important;
position: absolute !important;
left: 2px !important;
top: 12px !important;
font-size: 1em !important;
line-height: 1 !important;
}
/* NEW badge inline with article title */
body[class*="topbar"] .menu .mega-list-deep .menu-new-inline {
display: inline-block !important;
margin: 0 6px 0 0 !important;
padding: 1px 4px !important;
background: rgba(70, 70, 70, 0.9) !important;
color: #ffffff !important;
font-size: 10px !important;
font-weight: 500 !important;
line-height: 1 !important;
letter-spacing: 0.03em !important;
border-radius: 2px !important;
white-space: nowrap !important;
vertical-align: 1px !important;
}
/* =========================================================
NEW BADGE IN MAIN MENU
========================================================= */
/* NEW badge for ARTICLES and GALLERY */
body[class*="topbar"] .menu > li > a .main-menu-new-badge {
display: inline-block !important;
margin-left: 6px !important;
padding: 1px 4px !important;
background: rgba(70, 70, 70, 0.9) !important;
color: #ffffff !important;
font-size: 9px !important;
font-weight: 500 !important;
line-height: 1 !important;
letter-spacing: 0.03em !important;
border-radius: 2px !important;
white-space: nowrap !important;
vertical-align: 1px !important;
pointer-events: none;
}
/* =========================================================
MOBILE: HIDE NEW BADGES
========================================================= */
@media screen and (max-width: 639px) {
.main-menu-new-badge,
.menu-new-inline {
display: none !important;
}
}
/* =========================================================
"SHOW ALL ARTICLES"
========================================================= */
/* Container below the article list */
.mega-list-deep .all-articles-wrap {
margin-top: 10px !important;
padding-left: 1.75em !important;
}
/* Link */
body[class*="topbar"] .menu .mega-list-deep .all-articles-link {
display: inline-block !important;
margin: 0 !important;
padding: 3px 0 !important;
color: rgba(190, 190, 190, 0.75) !important;
font-size: 0.85rem !important;
font-weight: 400 !important;
line-height: 120% !important;
transition: color 0.2s ease;
}
/* No bullet point before "Show all articles" */
body[class*="topbar"] .menu .mega-list-deep .all-articles-link::before {
content: none !important;
display: none !important;
}
/* Hover effect */
body[class*="topbar"] .menu .mega-list-deep .all-articles-link:hover {
color: #ffffff !important;
}
/* =========================================================
ARTICLE DROPDOWN PREVIEW
========================================================= */
/* Reduce X3 preview image outline to 1 px */
.mega-list-deep .preview .image-container {
outline: 1px solid #1b1b1b !important;
}
/* Prevent preview content from overflowing */
.mega-list-deep .preview {
overflow: hidden !important;
}
/* Smaller preview title, limited to 2 lines */
.mega-list-deep .preview h2 {
white-space: normal !important;
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden !important;
text-overflow: ellipsis !important;
overflow-wrap: break-word;
font-size: 1.10rem !important;
line-height: 1.25 !important;
}
/* Smaller preview description, limited to 3 lines */
.mega-list-deep .preview p,
.mega-list-deep .preview .description {
white-space: normal !important;
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden !important;
text-overflow: ellipsis !important;
overflow-wrap: break-word;
font-size: 0.85rem !important;
line-height: 1.4 !important;
}
/* =========================================================
HOMEPAGE: POST AND ARTICLE OVERLAYS
========================================================= */
/* Image link as positioning reference for overlay */
.has-more-overlay {
position: relative;
display: block;
}
/* "View post" / "Read article" overlay on image */
.more-overlay {
position: absolute;
right: 16px;
bottom: 16px;
padding: 8px 14px;
background: rgba(0, 0, 0, 0.72);
color: #ffffff;
font-size: 15px;
line-height: 1.2;
border-radius: 3px;
z-index: 1;
pointer-events: none;
transition: background 0.2s ease;
}
/* Lighten overlay on hover */
.has-more-overlay:hover .more-overlay {
background: rgba(70, 70, 70, 0.9);
}
/* =========================================================
HOMEPAGE: ARTICLE BADGE
========================================================= */
/* ARTICLE label on homepage image */
.article-badge {
position: absolute;
left: 16px;
top: 16px;
padding: 5px 9px;
background: rgba(0, 0, 0, 0.50);
color: rgba(255, 255, 255, 0.88);
font-size: 11px;
font-weight: 500;
line-height: 1.2;
letter-spacing: 0.06em;
border-radius: 3px;
z-index: 1;
pointer-events: none;
transition: background 0.2s ease;
}
/* Lighten ARTICLE badge on hover */
.has-article-overlay:hover .article-badge {
background: rgba(70, 70, 70, 0.9);
}