Kategorie-Archiv:

Blogging

Beliebteste Beiträge

Ich weiß nicht, ob’s jemand schon bemerkt hat^^, aber seit letzter Woche findet sich in meiner Sidebar ein Abschnitt mit den beliebtesten Beiträgen, und wie das bei mir so üblich ist, bemühe ich da nicht groß ein Plugin, sondern hab das selber gebastelt. Und da es vielleicht den einen oder anderen auch interessieren könnte, gibt’s diesen Beitrag… Verbesserungsvorschläge sind natürlich willkommen.

Die Frage war also: Welche Kriterien sollen für die Beliebtheit gelten, und wie krieg ich das mit einer Datenbankabfrage hin? Die Kriterien, an die ich gedacht habe, sind:

  • Aufrufzahlen der Beiträge, gezählt mit WP-PostViews1;
  • Anzahl der Kommentare;
  • Länge der Kommentare2 – kurze à la „toller Beitrag“ weniger stark gewichten;
  • Alter der Beiträge – neue Beiträge mit vielen Aufrufen oder Kommentaren sollen bevorzugt werden – bzw. Aufrufe pro Tag.

Wer ein Plugin zur Beitragsbewertung einsetzt, mag dessen Werte natürlich auch noch mit einschließen; entsprechendes gilt für ein anderes Statistik-Plugin als WP-PostViews. Und natürlich gibt’s auch etliche „Popular Posts“-Plugins, doch die wenigsten davon berücksichtigen mehr als die Kommentaranzahl – und eine maßgeschneiderte Lösung ist mir eben auch lieber…

Generell stellt sich noch die Frage nach der Gewichtung – wenn bei euch fast jeder Besucher kommentiert, müsst ihr die Kommentaranzahl nicht deutlich stärker als die Aufrufe gewichten, und man kann natürlich eigene Schwerpunkte setzen. Und man muss die einzelnen Kriterien auch nicht einfach mit einem Faktor addieren, sondern kann etwa alle anderen durch das Alter teilen u.v.a.m. Meine Gewichtung seht ihr unten im/nach dem Code.

Nun könnte man dazu mehrere Datenbankabfragen machen, die alle der Post-ID, also der eindeutigen Nummer eines Beitrags, ihren jeweiligen Wert zuordnen, und diese Arrays dann mittels PHP verrechnen und gewichten – ich hab’s hingegen in eine einzige MySQL-Abfrage gequetscht (war auch ’ne kleine Übung in Sachen MySQL… und ist sicher nicht perfekt…); mag sein, dass einzelne Abfragen schneller wären, aber nach etwas Optimierung hatte ich meine Query so weit, dass sie nicht viel länger dauert als das „Einsammeln“ der Kommentarlängen (die bei >10000 Kommentaren/Trackbacks schon ein bisschen dauert), und vermutlich geht’s nicht viel schneller – konkret: von ca. 1½ auf 0,3 Sekunden.

(Aus der Zeit vor der Optimierung stammt auch die kleine Cache-Funktionalität, die ihr gleich im Code seht; bei Verwendung eines Datenbank-Caches oder Verlassen auf den Query Cache der Datenbank selbst wäre die auch weniger wichtig.)

Hier also der Code3:

function ag_popular_posts($num=10) {
    global $wpdb;
    $lastupdate = (int) get_option("ag_popular_lastupdate",0);
    $output = '';
    if (time()-$lastupdate > 1800) {
        $exclude = "
            AND ID NOT IN (78,70)
            AND ID NOT IN (
                SELECT object_id FROM $wpdb->term_relationships
                WHERE $wpdb->term_relationships.term_taxonomy_id IN (92,384)
            )"
;
        $posts = $wpdb->get_results("
            SELECT ID,post_date,post_title,post_name,comment_count,
                $wpdb->postmeta.meta_value AS views,
                TO_DAYS(NOW())-TO_DAYS(post_date) AS age,
                comment_length,
                ($wpdb->postmeta.meta_value/2
                 +comment_count*50
                 +comment_length/comment_count*5
                 +(TO_DAYS(post_date)-TO_DAYS('2006-09-06'))*5) AS popularity
            FROM $wpdb->posts,$wpdb->postmeta,(
                SELECT comment_post_id, SUM(LENGTH(comment_content)) AS comment_length
                FROM $wpdb->comments
                WHERE comment_approved='1'
                GROUP BY comment_post_id) AS thecomm
            WHERE $wpdb->posts.ID = $wpdb->postmeta.post_id
            AND $wpdb->posts.ID = thecomm.comment_post_id
            AND $wpdb->postmeta.meta_key = 'views'
            AND comment_count > 0
            AND post_status = 'publish'
            AND post_type = 'post'
            AND post_password = ''
            $exclude
            ORDER BY popularity DESC, post_date
            LIMIT $num"
);
        if ($posts) {
            $output="<ul>";
            $pop0=0;
            foreach ($posts as $p) {
                if ($pop0==0) $pop0=$p->popularity;
                $output.='<li><a href="'.get_permalink($p->ID).'" title="'.
                        $p->comment_count.' Kommentare mit durchschnittlich '.
                        ($p->comment_count==0?'0':number_format($p->comment_length/$p->comment_count,1)).' Zeichen, '.
                        number_format($p->views).'x aufgerufen, '.
                        number_format($p->age).' Tage alt'.
                    '">'.
                    $p->post_title."</a> ".
                    "<small>(".number_format($p->popularity/$pop0*100,0).'%'.")</small>".
                    "</li>\n";
            }
            $output.="</ul>\n";
        }
        update_option("ag_popular_lastupdate",time());
        update_option("ag_popular_cached",$output);
    } else {
        $output = get_option("ag_popular_cached",'');
    }
    if ($output!='') {
        echo "<li id=\"side-popular\">\n".
            "<h2>Beliebteste Beitr&auml;ge</h2>\n".
            $output.
            "</li>\n";
    }
}
 

Also gehen wir den Code mal durch:

Wir beginnen mit der Abfrage, ob die Liste aktualisiert werden soll – das Intervall liegt hier bei 1800 Sekunden, also einer halben Stunde.

Um bestimmte Beiträge auszuschließen, kann man ihre ID wie in der ersten Zeile des ausgelagerten $exclude-Statements angeben – bei mir ist es 78 = die Liste der Musik-Zitate, die aufgrund ihrer hohen Abrufzahlen unangefochten an Platz 1 wäre, aber nicht so interessant ist, dass ich sie hier in der Beliebtheitsliste hervorheben möchte, und die 70 = die damalige Technorati4-Ketten-Aktion, die nur bei manchen Gewichtungsversuchen in der Top 10 wäre –, oder alternativ z.B. auch post_name.

Eine ganze Kategorie oder ein ganzes Tag kann man mit dem zweiten AND ID NOT IN-Statement ausschließen. Dazu muss man die term_taxonomy_id der/des fraglichen Kategorie/Tag herausfinden – indem man in der Tabelle wp_terms anhand des Namens die term_id findet und mit dieser in wp_term_taxonomy nachschlägt (oder irgendwelche WP-Funktionen verwendet, mit get_term_by() könnt’s vielleicht gehen) – und am Ende einsetzen; bei mir will ich alle Quiz-Beiträge (92) und nur englische (384) ausschließen (bzw. in englischer Ansicht alle deutschen, da steht dann 385).

Weiter geht’s mit der eigentlichen SELECT-Abfrage. In deren ersten vier Zeilen stehen ein paar für die Ausgabe benötigte Angaben, danach in Klammern (...) AS popularity der eigentliche Beliebtheitswert.

Wie ihr (vielleicht) seht, gewichte ich die Seitenaufrufe mit 0,5, die Kommentaranzahl mit 50, die durchschnittliche Kommentarlänge ebenso wie die Neuheit des Beitrags (Tage seit Blog-Start, was ihr für euch natürlich ändern müsst, siehe gelb hinterlegtes Datum – wenn ihr stattdessen das Alter, also die Tage zwischen Beitrag und heute, verwenden wollt, nehmt TO_DAYS(NOW())-TO_DAYS(post_date)) mit 5, was sich nach etwas Herumspielen als recht passend für mein Blog gezeigt hat.

(Die kurz darüber deklarierten Felder (AS views, AS age) kann man hier übrigens nicht mit den neuen Namen nehmen, da meckert MySQL – also eben nochmal komplett einsetzen.)

Die FROM-Zeile enthält neben der wp_posts-Tabelle noch wp_postmeta, weil dort die Aufrufzahlen von WP-PostViews liegen – braucht ihr nicht, wenn ihr diese Werte nicht verwendet – und das untergeordnete SELECT für die Kommentarlänge, genauer: die Summe der Länge aller Kommentare mit derselben post_id (inkl. Trackbacks; diese könnte man mit AND comment_type='' vor GROUP ausschließen).

Die WHERE– und die erste AND-Zeile verknüpfen die beteiligten Tabellen über die Post-ID, die nächste wählt die WP-PostViews-Werte aus (die eben „views“ heißen) und der Rest beschränkt das ganze auf geeignete Beiträge.

Danach wird die Ausgabe zusammengebastelt, insb. in der foreach-Schleife, die über die Beiträge läuft, wo der Link samt Tooltip (title) mit statistischen Infos und einem Prozentwert der Beliebtheit mit 100% für den 1. Platz zusammengeklebt wird.

Wenn diese Ausgabe zusammengeklöppelt ist, wird sie noch schnell in die Datenbank für unseren kleinen Cache geschrieben, von wo aus sie (im else-Zweig) innerhalb der nächsten halben Stunde gelesen wird. Und zu guter Letzt muss das ganze natürlich noch (mit Überschrift) ausgegeben werden.

So, das war jetzt jede Menge Code. Wer Verständnis- oder sonstige Fragen, Anmerkungen, Stellungnahmen, Lob, Kritik, Verbesserungsvorschläge, Beleidigungen, Ideen oder Anregungen hat, nur raus damit – Moment, auf eine dieser Arten von Äußerungen solltet ihr besser verzichten.^^

 

  1. mit dem Nachteil, dass ich dieses anfangs alle Zugriffe inkl. Suchmaschinen zählen ließ, aber seitdem ich auch WP Super Cache nutze nur mittels JavaScript – wodurch die Zähler mittlerweile weit weniger stark steigen… ↺
  2. auf Anregung von Julia, dankeschön ↺
  3. ich hoffe, ich hab beim Wieder-Entfernen meiner Sprachumschaltung keinen Fehler eingebaut… ↺
  4. Berners-Lee hab es selig ↺

Link und Videos der Woche (2010/17)

(Hier war etwas als Flash-Objekt eingebunden. Nicht mehr zeitgemäß.)

 

  • In gewisser Weise das Lied der Woche – Tim Minchins Papst-Lied (NSFW). Hier in einer untertitelten Version, bei den Skepchicks (via) gibt’s auch den Text in Gänze zum Mitlesen.

Und wer sich darüber aufregt, wie hier mit dem Papst umgegangen wird, sollte besser auf den Text achten: Was ist schlimmer, dieses Lied oder der Verdacht, dass der Papst, Kopf einer sich als hohe moralische Autorität gebärdenden Institution, möglicherweise Kinderficker gedeckt hat?

 

  • Was? Widersprüche in der Bibel? Kann das sein?^^ Dazu eine Cartoon-Quizshow (via Pharyngula):

Links und Video der Woche (2010/11)

(Hier war etwas als Flash-Objekt eingebunden. Nicht mehr zeitgemäß.)

Kommentare zitieren

Pink Butterfly Little Girl With Colorful Message Clouds. Isolate Ich habe mal wieder eine kleine Bastelei ins Blog eingebaut: eine Möglichkeit, einen ganzen Kommentar als Zitat ins Eingabefeld zu übernehmen. Dazu findet sich jetzt in der Kopfzeile jedes Kommentars am Ende ein entsprechender Link, der diesen als HTML samt umschließendem <blockquote> und darüber einer Zeile „Zitat von …“ samt Link zum Kommentar an die bisherige Eingabe anhängt – und nicht etwa beim Cursor einfügt; dies würde natürlich auch gehen (mach ich bei den Smilies ja schon), aber ich denke, bei so langen Einfügungen ist das Anhängen besser.

Das Script

Das ganze benötigt zum einen ein kleines Script (was auch bedeutet, dass Javascript aktiviert sein muss), das man entweder in eine externe Javascript-Datei einfügt oder einfach inline in die comments.php des Themes, am besten ziemlich direkt nach der Zeile

<?php if ( have_comments() ) : ?>

vor der Ausgabe der Kommentare.

Und das ist das Script, das jQuery benötigt – wird in vielen Fällen ja eh schon geladen:

<script type="text/javascript">
<!--
    function quotecomment (cID) {
        p = jQuery('#edit-comment'+cID);
        if ((!p) || (p.length==0)) p = jQuery('#comment-text-'+cID);
        t = p.html();
        t = t.replace(/<img [^>]*alt=[" ]*([^ "]*)[" ]*[^>]*>/ig,' $1 ');
        jQuery('#comment').val(jQuery('#comment').val() + '\n' +
            'Zitat von <a href="#comment-'+cID+'">'+
            jQuery('#comment-author-'+cID).text()+
            '</a>:\n<blockquote>'+t+'</blockquote>\n\n');
    }
//-->
</script>

In der Funktion (zu deren Aufruf wir gleich noch kommen) wird zunächst das Element mit der ID edit-commentXXX gesucht (mit XXX=Kommentarnummer), das den Kommentartext enthält, wenn das Plugin Ajax Edit Comments aktiv ist und dieser Kommentar bearbeitet werden kann; gibt’s das nicht, ist comment-text-XXX gefragt – das ist eine der kleinen Theme-Anpassungen, die ihr werdet vornehmen müssen – auch dazu gleich mehr.

Von diesem Element wird dann der HTML-Inhalt genommen – so bleiben die ganzen Formatierungen erhalten, auch wenn es mehr Tags enthält (etwa <p>), als original eingegeben wurde. Gut, alternativ hätte ich den Originalinhalt via Ajax nachladen können wie dieses Plugin (oder gleich das Plugin verwenden) oder ihn nochmal direkt in den Funktionsaufruf im Seitenquelltext schreiben, aber dies ist irgendwie die sparsamste Lösung…

Da in diesem HTML-Inhalt auch Bilder – selbst die Smilies – als img-Tags drin sind, müssen sie mit dem regulären Ausdruck im replace wieder in die Smilie-Codes (die in den alt-Attributen stehen) umgewandelt werden – Bilder dürft ihr ja nicht direkt in euren Kommentaren verwenden, das darf nur ich. :mrgreen: (Und natürlich muss dieser Ausdruck komplizierter sein, weil der Internet-Explorer wieder ein eigenes Süppchen kocht und etwa die Anführungszeichen bei den Attributen weglässt.)

(Benutzer einer Live-Vorschau, die bei <br> einen Zeilenumbruch zu viel einfügte, sollten die Zeile ergänzen, die ixiter unten im Kommentar nennt.)

Und schließlich wird in dem langen jQuery-Aufruf der Text/Code dann an den Inhalt des Eingabefeldes – das wie üblich die ID "comment" haben muss, ansonsten müsst ihr’s ändern – angehängt.

Änderungen in der Kommentarausgabe

In der Kommentarausgabe des Themes müssen, wie oben schon erwähnt, zum einen die Namen der Elemente mit ausgegeben werden, zum anderen der Link mit dem Aufruf der obigen Funktion. Das wird natürlich von eurem Theme abhängen, aber wer etwas Erfahrung mit solchen Basteleien hat, wird die passenden Stellen schon finden und geeignet ändern können.

Da wäre zum ersten der Name des Kommentators, der die entsprechende ID braucht – und zwar möglichst nur der Name und nichts drumrum. Man nimmt am besten einen eigenen span direkt um den Aufruf der WordPress-Funktion comment_author_link() (oder wie auch immer bei euch der Name mit Link ausgegeben wird); bei mir sieht da sowieso ein strong zur Fettschrift, deswegen hab ich das gleich verwendet:

<strong id="comment-author-<?php comment_ID(); ?>">
<?php comment_author_link() ?></strong>

Das muss, wie man sieht, im HTML-Kontext stehen (also außerhalb des <?php ... ?>-Bereichs). Dann brauchen wir den „zitieren“-Link an der gewünschten Stelle – und in dieser Form muss es innerhalb von <?php ... ?> stehen:

if ($comment->comment_type=="") {
    echo ' <span class="quotecomment">';
    echo '<a href="#commentform" onclick="quotecomment('.get_comment_ID().');" '.
        'title="den Inhalt dieses Kommentars ans Ende des Eingabefelds anh&auml;ngen (als HTML)">'.
        'zitieren <img src="/pics/quote.png" width="17" height="16" alt="" /></a>';
    echo '</span>';
}

Wer nicht will, dass beim Klick auch die Fensterposition wieder zum Eingabefeld scrollt, ersetze das href="#commentform" onclick="quotecomment… durch href="javascript:quotecomment… Wer das Bildchen nicht will, muss das entspr. img-Tag weglassen; ansonsten dürft ihr euch gerne mein Bildchen kopieren1.

Zu guter Letzt muss noch der eigentliche Kommentarinhalt seine ID erhalten, wozu wir den comment_text()-Aufruf in einen div packen, wenn noch kein geeigneter vorhanden ist (ansonsten den einfach entsprechend benennen oder, wenn der in eurem Theme schon eine ID hat, im Script oben anpassen):

echo '<div id="comment-text-'.get_comment_ID().'">';
comment_text();
echo '</div>';

So, das war’s dann eigentlich, ich hoffe, ich habe nichts vergessen. Wenn ihr noch Fragen habt, nur raus damit…

Ausblick

Als weitere Idee wäre noch das Zitieren beliebiger Textabschnitte aus einem Kommentar oder dem Beitrag durch Markieren des Textes, wie es etwa hier bei Frank Bültge vorgestellt wurde. Aber das muss auf einen späteren Zeitpunkt warten…


Foto: Alexandr Zinchevici – Fotolia.com

  1. bitte auf euren Webspace kopieren und nicht etwa hotlinken ↺

Links und Video der Woche (2010/08)