Smart Card Lists – warum ich dieses Plugin gebaut habe (und was es alles kann)
Als Webagentur baue ich regelmäßig WordPress-Seiten, auf denen Inhalte nicht einfach “als Blog” funktionieren, sondern als übersichtliche Karten, filterbar, performant, und oft CPT-/JetEngine-basiert (Projekte, Referenzen, Veranstaltungen, Partner, …). Und immer wieder kam derselbe Punkt:
- Ich möchte eine einzige, wiederverwendbare Karten-Komponente
- die auf Posts oder Taxonomy-Terms laufen kann,
- Filter kann (ohne schweres JS-Framework),
- Events sauber als “upcoming / past” listet (mit JetEngine/ACF-Datum),
- archiv-aware ist (Kategorie-/Tax-Archive sollen “einfach so” funktionieren),
- und bei großen Datenmengen Lazy Load beherrscht – ohne gleich ein Monster-Plugin zu installieren.
Genau daraus ist Smart Card Lists entstanden.
Die Grundidee
Smart Card Lists ist ein Shortcode-basiertes “Listing-System”, das überall eingebettet werden kann:
- als Grid (Cards),
- optional mit Filter-UI (Checkbox-Gruppen pro Taxonomie),
- optional als Slider,
- optional als Lazy-Loading-List,
- und mit einem Event-Datum, das aus Meta-Feldern (JetEngine/ACF) gezogen wird.
Alles ist so gedacht, dass ich als Entwickler eine solide Basis habe, aber im Alltag einfach nur Shortcodes einsetzen muss.
Installation & Settings
Das Plugin bringt eine eigene Einstellungsseite mit:
WordPress → Einstellungen → Smart Card Lists
Dort kann ich globale Defaults setzen, die ich dann pro Shortcode überschreiben kann:
event_meta_key(Standard:date_of_event)event_prefix(Standard:Event:)upcoming_post_types(Standard:event)thumb_ratio(Standard:3/2)thumb_fit(Standard:cover)
Technisch hole ich mir diese Defaults über:
function scl_get_options() {
$defaults = [
'event_meta_key' => 'date_of_event',
'event_prefix' => 'Event: ',
'upcoming_post_types' => 'event',
'thumb_ratio' => '3/2',
'thumb_fit' => 'cover',
];
$stored = get_option('smart_card_lists_options', []);
return wp_parse_args(is_array($stored) ? $stored : [], $defaults);
}
Wichtig: Shortcode-Attribute schlagen immer die Defaults. Das macht das System flexibel.
Was kann das Plugin? (Features im Überblick)
1) smart_list – der Haupt-Shortcode
Der zentrale Shortcode ist:
[smart_list]
Damit kann ich:
- Posts aus beliebigen Post Types listen (
post_types="post,projekt,event") - Kategorien ein-/ausschließen
- Taxonomy-Filter (serverseitig) setzen
- Taxonomy-Filter (clientseitig) als UI anzeigen
- Archive übernehmen (oder auto-detect)
- Excerpt / Autor / Datum steuern
- Event-Datum aus Meta ziehen
- Lazy Load aktivieren
- Slider aktivieren
- Cards komplett klickbar machen
2) mode="terms" – Taxonomie-Terms als Cards
Ich kann statt Posts auch Terms als Cards darstellen:
[smart_list mode="terms" tax="branche" cols_desktop="4"]
Das ist super für “Kategorien-Übersichten” oder “Branchen-Kacheln”.
3) Event-Logik: upcoming / past / all
Über event_scope kann ich Events trennen:
[smart_list post_types="event" event_scope="upcoming" prefer_event_date="true"]
oder:
[smart_list post_types="event" event_scope="past" order="DESC"]
Die Datumsquelle kommt aus event_date_meta_key (komma-separierte Fallback-Liste möglich).
4) upcoming_events – der Komfort-Shortcode
Für den Alltag wollte ich etwas, das “einfach geht”:
[upcoming_events]
Optional mit “Alle Events”-Link:
[upcoming_events limit="5" all_url="/events" all_label="Alle Events anzeigen"]
Intern generiert der Shortcode automatisch einen passenden smart_list … mit:
event_scope="upcoming"- 1-Spalten-Layout
card_link="true"- keine Filter
- Excerpt aus
So sieht der Wrapper im Code aus:
$sc = sprintf(
'[smart_list post_types="%s" per_page="%d" event_scope="upcoming" event_date_meta_key="%s" prefer_event_date="true" ...]',
esc_attr($atts['post_types']),
intval($atts['limit']),
esc_attr($atts['meta_key'])
);
$out = '<div class="'.esc_attr($atts['wrapper_class']).'">';
$out .= do_shortcode($sc);
...
$out .= '</div>';
Die wichtigsten Shortcode-Beispiele (Copy & Paste)
Standard-Grid (Posts)
[smart_list post_types="post,projekt" per_page="12" cols_mobile="1" cols_tablet="2" cols_desktop="3"]
Manuell kuratierte Liste (IDs) + Reihenfolge behalten
[smart_list include_ids="12,7,45" ids_order="keep" per_page="3"]
Related Posts: gleiche Kategorie + aktuelles Exemplar ausblenden
[smart_list same_category="true" exclude_current="true" per_page="3"]
Taxonomie serverseitig filtern + Filter-UI anzeigen
[smart_list
post_types="projekt"
tax="branche:agentur,industrie;technologie:wordpress,react"
tax_filters="branche,technologie"
show_filters="true"
per_page="24"
]
Lazy Load bei großen Listen
[smart_list per_page="240" lazy="scroll" initial="12" batch="12"]
Slider statt Grid
[smart_list post_types="projekt" per_page="12" cols_desktop="4" slider="true" slider_step="auto"]
Oder immer nur “eine Karte weiterschieben”:
[smart_list post_types="projekt" per_page="12" cols_desktop="4" slider="true" slider_step="1"]
Terms als Cards
[smart_list mode="terms" tax="branche" hide_empty="true" cols_desktop="4" meta_bild="tax_bild"]
Deep Dive: Was passiert “an wichtigen Stellen”?
A) Event-Datum robust parsen (JetEngine/ACF freundlich)
Eines der Kernprobleme: Datum kann kommen als…
- UNIX Timestamp (10-stellig)
- Milliseconds (13-stellig)
Ymd(20251224)- ISO-String
- array/object mit
date,timestamp, etc.
Darum ist hw_parse_date_value() bewusst “defensiv” gebaut:
function hw_parse_date_value($raw) {
if (is_string($raw)) {
$raw = trim($raw);
// 10/13-digit timestamps
if (ctype_digit($raw)) {
if (strlen($raw) === 10) return (int)$raw;
if (strlen($raw) === 13) return (int) floor(((int)$raw)/1000);
}
// Ymd
if (preg_match('/^\d{8}$/', $raw)) {
$dt = DateTime::createFromFormat('Ymd', $raw, wp_timezone());
if ($dt) return $dt->getTimestamp();
}
// fallback
$ts = strtotime($raw);
return $ts ?: 0;
}
...
}
Das ist die Basis für event_scope="upcoming|past" und für die Anzeige im Card-Meta.
B) Event-Datum vs Post-Datum (und CSS-Klassen)
Beim Rendern der Card entscheide ich:
- Ist ein Event-Datum vorhanden?
- Ist
prefer_event_date="true"?
Dann bekommt die Zeile eine Klasse:
.card__meta-date--event- oder
.card__meta-date--post
$use_event = $prefer_event_date && $event_ts > 0;
$label = $use_event ? $event_prefix : $date_prefix;
$ts = $use_event ? $event_ts : $post_ts;
echo '<div class="card__meta-date'
. ($use_event ? ' card__meta-date--event' : ' card__meta-date--post')
. '">'
. esc_html($label . wp_date($date_format, $ts))
. '</div>';
Praktisch: Ich kann Event-Daten im CSS anders highlighten, ohne Zusatzlogik.
C) Aspect Ratio ohne neue Bildgrößen
Ich wollte keine extra Thumbnail-Sizes registrieren. Stattdessen nutze ich CSS aspect-ratio + object-fit.
Das Plugin injiziert CSS in wp_head:
.cards .card__thumb{
aspect-ratio: var(--sl-thumb-ratio, 3 / 2);
}
.cards .card__thumb img{
object-fit: var(--sl-thumb-fit, cover);
}
Und pro Shortcode setze ich:
--sl-thumb-ratio--sl-thumb-fit
Zusätzlich normalisiere ich Eingaben wie 3/2, 3:2, 1:
function hw_smartlist_normalize_ratio($raw){
$raw = str_replace(':', '/', trim((string)$raw));
if (preg_match('~^(\d+(\.\d+)?)\s*/\s*(\d+(\.\d+)?)$~', $raw, $m)) {
return ((float)$m[1]).' / '.((float)$m[3]);
}
if (is_numeric($raw)) return ((float)$raw).' / 1';
return '3 / 2';
}
D) Filter-UI: bewusst clientseitig (schnell & simpel)
Die Filter bauen auf data- Attributes pro Card:
$data .= ' data-tax-'.esc_attr($tx).'="'.esc_attr(wp_json_encode($slugs)).'"';
Im Frontend filtere ich dann rein über JS (Checkboxen → Set → Intersection):
function matches(card, selections){
for (var tax in selections){
var picked = selections[tax];
var cardSetTx = parseAttr(card, 'data-tax-' + tax);
if (picked.size && !intersects(cardSetTx, picked)) return false;
}
return true;
}
Warum so?
- Keine Reloads
- Keine Ajax-Abhängigkeit
- Minimaler Code
- Für typische “Portfolio/Projekte/Partner”-Seiten völlig ausreichend
E) Lazy Load: Token + Transient + Nonce (sauber & sicher)
Bei großen Listen will ich initial nicht 240 Cards ausgeben. Darum:
- initial
initial="12" - dann per Scroll
batch="12"
Das Plugin speichert die Query-Args serverseitig in einem Transient:
$pack = [
'args' => $args,
'opts' => [ ... ],
];
set_transient('hw_sl_'.$token, $pack, MINUTE_IN_SECONDS * 30);
Frontend ruft dann Ajax:
fd.append('action','hw_smart_list_next');
fd.append('nonce', nonce);
fd.append('token', token);
fd.append('offset', offset);
fd.append('limit', batch);
Server prüft:
check_ajax_referer('hw_smart_list_nonce', 'nonce');
$pack = get_transient('hw_sl_'.$token);
if (!$pack) wp_send_json_error(['msg'=>'Expired token']);
Das ist mir wichtig, weil ich keine kompletten Query-Args im Frontend herumschicken will.
F) Slider: “Peek-Effekt” + Pixel-Offset gegen Drift
Der Slider ist optional (slider="true") und nutzt flexbox + Track-Transform.
Desktop “Peek”-Effekt: die erste & letzte Karte sind halb sichtbar:
@media (min-width: 1024px){
.smartlist-slider{ --sl-peek: calc(50% / var(--sl-cols, 1)); }
.smartlist-slider__track{
margin-left: calc(-1 * var(--sl-peek));
margin-right: calc(-1 * var(--sl-peek));
transform: translateX(calc(var(--sl-translate) - var(--sl-peek)));
}
}
Und ich rechne pixelgenau, damit gap berücksichtigt wird:
var cardW = cards[0].getBoundingClientRect().width || 0;
var stepPx = cardW + gapPx;
var offsetPx = stepPx * currentIndex;
list.style.setProperty('--sl-translate', '-' + offsetPx + 'px');
Zusätzlich gibt’s:
- Buttons (prev/next)
- Bullets
- Swipe/Drag via Pointer Events (Touch + Maus)
Spezial-Features
1) “External Link”-Modus pro Post oder Term
Wenn ein Post/Term ein Meta-Feld has_external_link hat, dann:
- card verlinkt auf extern
- mit
target="_blank" rel="nofollow noopener noreferrer"- und der Single bekommt automatisch
noindex,nofollow
if (is_singular()) {
if (hw_get_external_link_for_post($pid)) {
echo '<meta name="robots" content="noindex,nofollow" />';
}
}
Für mich ist das perfekt für:
- Linklisten
- Ressourcen
- Partner, die extern laufen
- “Weiterleitung”-Posts
2) Titel & Bild pro Card überschreiben
Optional per Meta:
listing_text→ ersetzt Card-Titellisting_bild→ ersetzt Featured Image
Das ist Gold wert, wenn der eigentliche Post-Titel zu lang ist oder wenn ich pro Liste ein anderes Vorschaubild brauche.
Mini-FAQ – häufige Fragen zu Smart Card Lists
Kann ich mehrere Post Types gleichzeitig anzeigen?
Ja, absolut.
Der Shortcode akzeptiert eine kommagetrennte Liste von Post Types. Das ist besonders praktisch, wenn Inhalte aus unterschiedlichen Quellen (z. B. Blogposts, Projekte und Events) gemeinsam dargestellt werden sollen.
[smart_list post_types="post,projekt,event"]
Alle angegebenen Post Types werden in einer gemeinsamen Card-Liste ausgegeben und gleich behandelt. Sortierung, Filter, Lazy Load usw. greifen übergreifend.
Kann ich Taxonomien aus verschiedenen Post Types filtern?
Ja – solange die Taxonomien existieren.
Das Plugin prüft nicht, zu welchem Post Type eine Taxonomie „gehört“, sondern filtert sauber über WordPress-Tax-Queries.
[smart_list
post_types="projekt,event"
tax="branche:webdesign;typ:messe"
tax_filters="branche,typ"
]
👉 Wichtig:
- Die Filter-UI zeigt nur Taxonomien an, die tatsächlich vorhanden sind
- Cards ohne passende Terms werden korrekt ausgeblendet
Kann ich die Liste automatisch an Kategorie- oder Taxonomie-Archive anpassen?
Ja.
Wenn der Shortcode auf einem Taxonomie-Archiv verwendet wird, kann er den Kontext automatisch übernehmen:
[smart_list archive_aware="true"]
Beispiel:
- Du bist auf
/branche/webdesign/ - Der Shortcode filtert automatisch auf
branche = webdesign - Kein manuelles Setzen von
tax="..."nötig
Ideal für:
- Portfolio-Archive
- Themen-Landingpages
- dynamische Übersichtsseiten
Wie funktioniert die Event-Logik genau?
Events werden über ein Meta-Feld mit Datum gesteuert (z. B. JetEngine oder ACF).
[smart_list
post_types="event"
event_scope="upcoming"
event_date_meta_key="date_of_event,start_date"
prefer_event_date="true"
]
Das Plugin:
- erkennt unterschiedliche Datumsformate automatisch
- unterscheidet vergangene / zukünftige Events
- kann Event-Datum oder Post-Datum bevorzugen
- sortiert Events korrekt nach dem Event-Datum
Unterstützte Formate u. a.:
- UNIX Timestamp (10- & 13-stellig)
Ymd(JetEngine-Standard)- ISO-Strings
- PHP-Date-Strings
Kann ich Events und normale Posts mischen?
Ja – und das ist ausdrücklich vorgesehen.
[smart_list post_types="post,event" prefer_event_date="true"]
Ergebnis:
- Events zeigen ihr Event-Datum
- normale Posts zeigen ihr Veröffentlichungsdatum
- beide erscheinen in einer gemeinsamen Liste
CSS-Klassen helfen beim Styling:
.card__meta-date--event.card__meta-date--post
Funktionieren Lazy Load und Slider gleichzeitig?
Nein – und das ist Absicht.
Warum?
- Slider benötigen alle Cards gleichzeitig im DOM
- Lazy Load lädt Inhalte stückweise nach
Beides gleichzeitig würde:
- Layout-Fehler verursachen
- Slider-Berechnung zerstören
- unnötig komplexen JS-Code erfordern
👉 Empfehlung:
- Slider → kleinere, kuratierte Inhalte
- Lazy Load → große Listen, Archive, Verzeichnisse
Kann ich Lazy Load auch ohne Scroll auslösen?
Ja – Lazy Load unterstützt Button-basiertes Nachladen:
[smart_list lazy="button" initial="9" batch="9"]
Ergebnis:
- initial 9 Cards
- Button „Mehr laden“
- weitere 9 Cards pro Klick
Ideal für:
- Performance-kritische Seiten
- bessere UX auf Mobilgeräten
Kann ich einzelne Cards auf externe Links umleiten?
Ja, sehr elegant.
Wenn ein Post (oder Term) ein Meta-Feld für externe URLs besitzt:
- Card verlinkt direkt extern
- öffnet in neuem Tab
- bekommt
nofollow noopener noreferrer - Single-Seite wird automatisch
noindex,nofollow
Perfekt für:
- Linklisten
- Ressourcen-Sammlungen
- Partner- oder Empfehlungsseiten
Kann ich Titel und Bild pro Liste überschreiben?
Ja.
Über optionale Meta-Felder:
listing_text→ ersetzt Card-Titellisting_bild→ ersetzt Featured Image
Das ist extrem hilfreich, wenn:
- der eigentliche Post-Titel zu lang ist
- du für verschiedene Listen unterschiedliche Vorschaubilder brauchst
- Inhalte mehrfach, aber unterschiedlich präsentiert werden
Muss ich ein bestimmtes Theme oder Page-Builder nutzen?
Nein.
Smart Card Lists ist:
- Shortcode-basiert
- Gutenberg-kompatibel
- Elementor-tauglich
- Theme-agnostisch
Es bringt kein schweres Styling mit, sondern nur:
- saubere HTML-Struktur
- minimale Funktions-CSS
- klare Klassen für eigenes Design
Ist das Plugin für große Datenmengen geeignet?
Ja – genau dafür wurde es gebaut.
Features dafür:
- Lazy Load mit Transient-Token
- serverseitige Query-Kapselung
- kein Query-Leak ins Frontend
- keine unnötigen Re-Queries
Ich setze es problemlos für:
- Verzeichnisse mit mehreren hundert Einträgen
- Event-Archive
- Partner- & Projekt-Datenbanken