toscho.design

WordPress: Shortlinks reloaded

Im April 2009 habe ich gezeigt, wie man in WordPress mit Bordmitteln kurze URLs für Seiten und Artikel erzeugt: Blog-Adresse plus »?p=« plus Post-Id. Das funktioniert einfach und schnell. Nicht.

Formatkrieg

Meine Grundidee war: Ich habe nur einen Ort für die Logik des Verfahrens – die functions.php. Das stellte sich als zu naiv heraus, als die ersten kaputten Crawler aufschlugen, die das Fragezeichen mit einem urlencode() vorbehandelten. Requests auf /%3F=762 sind eben etwas anderes als auf /?=762.

Jetzt mußte ich doch einen zweiten Ort anfassen: die .htaccess.

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} ^/\%3fp\=(\d+)$
RewriteRule ^.* /?p=%1 [L,R=301]

Dann versagte auch die URL-Erkennung bei Twitter: Verlinkt wurde nur der Teil bis zum Fragezeichen, alles dahinter fiel weg.
Also habe ich die Kurz-URLs auf das Format /p762 umgestellt und die .htaccess angepaßt:

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} ^/\%3fp\=(\d+)$ [OR]
RewriteCond %{REQUEST_URI} ^/p\=?(\d+)$
RewriteRule ^.* /?p=%1 [L,R=301]

Auftritt Rivva-Bot: Der hat das neue Format um einen Slash / ergänzt, die Fehlerseite bestaunt – und dann blieb er weg. Kein Problem:

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} ^/\%3fp\=(\d+)$ [OR]
RewriteCond %{REQUEST_URI} ^/p\=?(\d+)/?$
RewriteRule ^.* /?p=%1 [L,R=301]

Markup

In einem sehr fruchtbaren Ideen-Pingpong (vulgo: Dialog) mit Bernhard Häussner haben wir die perfekte Notation für die Bekanntgabe des Shortlinks gefunden: Ein separater HTTP-Header samt meta-Element:

<meta http-equiv="X-Short-URI" content="http://toscho.de/?p=762">

Interessiert kein Schwein. Am 14. August schuf WordPress.com Tatsachen und verstetigte die sehr schlechte Lösung rel="shortlink" (Referenz). Schade.

Jetzt könnte die URL also so angegeben werden:

<link rel="shortlink" href="/?p=762">
Opera: Shortlink im Infopanel

Opera: Shortlink im Infopanel

Könnte. Opera, auf den ich unter keinen Umständen verzichte, ignoriert leider genau diesen Link im Info-Panel. Also füge ich als Beziehung noch ein alternate hinzu, damit die Erkennung wieder klappt.
Wer es ausprobieren möchte: F4 drücken, »Leiste anpassen/Erscheinungsbild/Paneele/« und ein Häkchen bei »Info« setzen.

PHP-Code

Ich habe den Code zur Ausgabe in WordPress in eine kleine Klasse mit statischen Methoden gegossen – des armen Mannes Namensraum. Verzeiht mir.

<?php
/**
 * Class Short_URI: Short-URIs erzeugen und ausgeben.
 * @author Thomas Scholz <http://toscho.de>
 * @see http://microformats.org/wiki/rel-shortlink
 * @license GPL 3 <http://www.gnu.org/licenses/gpl.html>
 * @version 1.3
 *
 */
class Short_URI {

    /**
     * Kurz-URI
     * @var string
     */
    protected static $short_uri      = NULL;

    /**
     * Sendet einen Link-HTTP-Header
     * @param bool $headonly Nur bei HEAD-Requests senden?
     * @return void
     */
    public static function send_header($headonly = TRUE)
    {
        if ($headonly and ('HEAD' != $_SERVER['REQUEST_METHOD']) )
        {
            return;
        }
        if ( ! self::prepare() )
        {
            return;
        }
        header('Link: <' . self::$short_uri . '>; rel="shortlink"');
        return;
    }

    public static function link_elem($echo = TRUE)
    {
        if ( ! self::prepare() )
        {
            return;
        }
        $link = '<link rel="alternate shortlink" title="Shortlink"'
                . ' href="' . self::$short_uri . '" />';

        if ( ! $echo)
        {
            return $link;
        }

        echo $link;
        return;

    }

    /**
     * Erzeugt einen Link zum Twittern der aktuellen Seite.
     * @param string|array $args
     * @return string
     */
    static function tweet_link($args = NULL)
    {
        $message_template = 'Lese gerade: %TITLE %URL von @toscho';
        $link_text        = 'Artikel twittern';
        $title_length     = 80;
        $class            = 'twitterlink';
        // Zum Ändern echo=0 übergeben.
        $echo             = TRUE;

        if ( ! is_null($args) )
        {
            if ( is_array($args) )
            {
                extract($args);
            }
            elseif ( is_string($args) )
            {
                parse_str($args);
            }
            else
            {
                trigger_error(
                    '$args muss ein Array oder ein String sein.',
                    E_USER_ERROR
                );
            }
        }
        global $post;

        $title       = trim($post->post_title);

        if ( isset ( $title[$title_length+2] ) )
        {// Verkürzen bei Bedarf
            $title = self::end_on_word(substr($title, 0, $title_length)) . '';
        }

        $message = str_replace(
            array('%TITLE', '%URL'),
            array($title, self::$short_uri ),
            $message_template
        );

        $link = '<a class="' . $class
            . '" href="http://twitter.com/home?status=' . urlencode($message)
            . '" title="' . $message . '">'
            . $link_text . '</a>';

        if ($echo)
        {
            echo $link;
            return;
        }

        return $link;
    }

    /**
     * Prüft, ob eine Short-URI erstellt werden soll
     * @return bool
     */
    public static function prepare()
    {
        if ( is_front_page() )
        {
            return FALSE;
        }
        if ( is_page() or is_single() or is_attachment() )
        {
            global $post;

            if ( is_null( self::$short_uri ) )
            {
                self::$short_uri =
                    rtrim(get_option('home'), '/') . '/p' . $post->ID;
            }
            return TRUE;
        }
        // Archive
        return FALSE;
    }

    /**
     * Entfernt unvollständige Worte am Ende eines Strings.
     * @see http://toscho.de/2009/php-funktion-end_on_word/
     * @param $str Zeichenkette
     * @return string
     */
    static function end_on_word($str)
    {
        $str = trim($str);

        // Nur ein Wort, wird also nicht gekürzt.
        if ( FALSE === strpos($str, ' ') )
        {
            return $str;
        }

        // Jedes Wort ein Eintrag im Array …
        $arr   = explode(' ', $str);
        // … letztes Stück wegwerfen …
        array_pop($arr);
        // … den String wieder herstellen und Kommas trimmen.
        return rtrim( implode(' ', $arr), ',;');
    }
}

Wer bis hierhin durchgehalten hat, darf sich das auch als …

Den Tweetlink baut ihr innerhalb des Loops mit diesem Code ein:

do_action('tweet_link');

Oder so:

do_action('tweet_link', 'parameter=wert');

Die darin enthaltene Klasse könnt ihr natürlich auch einfach in die functions.php einbinden, um den Code direkt zu verwenden.

Ich habe ihn unter die GPL 3 gestellt (hallo Helmut!), damit ihr damit machen könnt, was immer ihr wollt. Er ist mir länger geraten, als ich erhofft hatte; das heißt: Er hat sicher auch mehr Fehler. Findet sie!

22 Kommentare

  1. David am 12.01.2010 · 16:32

    Und wozu der ganze Aufwand? Nur weil Twitter mir nicht erlaubt mehr als ein paar Zeichen zu zwitschern und man dabei nicht mal in der Lage ist, einen vernünftigen Linktext einzusetzen, so wie es seit je her im Internet üblich ist?

    Wieso bindet man sich freiwillig eine solche Kugel ans bein?

  2. Thomas Scholz am 12.01.2010 · 16:38

    @David: Twitter kann ganz ordentlich Leser vermitteln. Zudem existieren noch ein paar Dienste mit ähnlichen Beschränkungen, und einige E-Mail-Programme brechen immer noch lange URLs kaputt. Für die Kurzvariante existiert also ein hinreichend breites Einsatzfeld.
    Und wenn man das einmal am Laufen hat, ist es ja kein Aufwand mehr.

  3. Bernhard Häussner am 12.01.2010 · 17:11

    "Ideen-Pingpong", das muss ich mir merken...
    @David: Ich benutze die kurzen URLs nicht nur für twitter, sondern weil sie auch schneller zu tippen sind. Schließlich ist (lange) Adresse eingeben und Link in Navigation anklicken wesentlich umständlicher als die (für diesen Zweck gekaufte) Kurzdomain und 1-2 Buchstaben eingeben.
    Natürlich fallen dabei die automatisch durchnummereirten URLs meist heraus, weil man sie sich nicht immer merkt, aber z.B. habe ich /b für Blog, /bt für Tags, /ls für den Lifestream und weitere, sodass ich dann im Endeffekt beispielsweise nur 1-co.de/bn eingeben muss, um die letzten Kommentare zu sehen.

    Auch ganz praktisch, wenn man jemandem eine URL mitteilen will, anders als über Mail oder IM, z.B. am Telefon oder schriftlich. Dann spart man sich doch einige Strapazen.

  4. Michael am 12.01.2010 · 20:41

    Danke für den Artikel
    Sorry, wenn ich dumm nachfrage:
    Um aus .../?p=123
    die URL .../p123
    zu machen, reicht da die oben angesprochene Änderung in der .htaccess ?
    Oder muss man da am WordPress Code noch etwas ändern?
    Bei mir ( http://www.fotografr.de ) funktioniert das nämlich leider nicht.

    Danke und Gruß Michael

  5. Thomas Scholz am 12.01.2010 · 22:14

    @Michael: Es funktioniert genau umgekehrt: Von /p123 leitet der Server per .htaccess nach /?p=123 um. Hier übernimmt dann WordPress: Es sieht in der Datenbank nach, ob es einen Artikel mit dieser Nummer gibt und ob er eine kanonische Adresse besitzt. Dann leitet WordPress auf diese Adresse um.

    Wenn du also nur /?p=123 als Shortlink benutzt, brauchst du die .htaccess nicht anzufassen. WordPress schafft das ganz alleine. Leider scheitern daran, wie im ersten Abschnitt dargestellt, einige Crawler.

  6. Michael am 12.01.2010 · 23:12

    Nein, ich möchte das schon genau so machen, dass ich /p123 als Shortlink benutze, weil Twitter das sonst nicht versteht.
    Und die htaccess soll das dann in /?p=?123 umsetzen, damit WP das versteht.
    Und um das zu erreichen, brauche ich nur die htaccess anfassen?
    Klappt nur leider nicht mit dem obg. Code ....

    Danke und Gruß Michael

  7. Thomas Scholz am 12.01.2010 · 23:22

    @Michael: Steht die Regel auch vor der allgemeinen für WordPress in der .htaccess? Und was sagt dein Rewrite-Log? Wenn du keins hast, dann dein Hoster. Darin steht, wohin der Server umleitet.

  8. Michael am 14.01.2010 · 20:44

    Sorry, Thomas, wenn ich schon wieder nerve, aber ich bekomme es immer noch nicht gebacken:

    Mit der Rewrite Condition möchte ich ja herausfinden, ob die URL mit folgendem String endet:
    /p123

    Du codierst dort
    RewriteCond %{REQUEST_URI} ^/p\=?(\d+)$

    Das (/d+)$ verstehe ich ja noch, das sind wohl ein oder mehrere Ziffern am Ende, aber der Rest ist mir nicht klar.
    Kannst Du da ein wenig Licht ins Dunkel bringen?

    Auf Deine Fragen: Ja, es steht vor dem WordPress Teil und nein, ich habe leider kein Rewrite Log. (1&1 Hosting)

    Danke vielmals, Michael

  9. Bernhard Häussner am 15.01.2010 · 16:06

    @Michael: Du liegst richtig.
    Das ^ und $ um das Regexp lassen es die ganze URI untersuchen. Das = kann so \= codiert werden, damit es nicht als Metazeichen interpretiert wird und das Fragezeichen bedeutet, dass der Buchstabe davor optional ist, also ein = kann vorkommen oder nicht, und für den Rivva-Bot der Slash / am Ende kann vorkommen oder eben nicht.

    Eine Überischt über die Regexp-Symbole und ihre Bedeutung findet sich hier: Regular Expressions (RegEx) - Quick Reference

  10. Thomas Scholz am 15.01.2010 · 16:45

    @Michael: Das optionale Fragezeichen resultiert noch aus den Problemen mit der ursprünglichen Form /?p=123: Ein Crawler (habe den Namen vergessen) hat da das Fragezeichen herausgekürzt und /p=123 angefordert.

    Bitte vergib mir, wenn ich dich mit deinem konkreten Problem auf das Mod_Rewrite-Forum verweise. Dort helfen dir Leute, die sich viel besser auskennen als ich – und mir fehlt auch gerade die Zeit dafür.

  11. Michael am 15.01.2010 · 17:12

    Danke Bernhard und Thomas,

    ich versuche es mal im mod rewrite Forum

    Gruß Michael

  12. Michael am 20.01.2010 · 18:41

    Das Thema hat sich übrigens erledigt, weil Twitter jetzt neuerdings die Links so verarbeiten kann, wie sie von WordPress out of the box geliefert werden :-)))

    Danke trotzdem!

    Gruß Michael

  13. Thomas Scholz am 21.01.2010 · 12:53

    @Michael: Twitters URL-Parser war nie das Problem, von dem kurzzeitigen Bug mit den Fragezeichen mal abgesehen. Aber jedes Zeichen, das für die URL verbraucht wird, fehlt dem Text, denn die Begrenzung von 140 Zeichen pro Tweet besteht immer noch. Ebenso die Bugs anderer Crawler und einiger E-Mail-Programme. Ich bezweifle sehr, daß sich das in den nächsten Jahren bessert.

  14. Bernhard am 06.05.2010 · 14:44

    Also soviel Code ist mir für Twitter zu viel. Ich hab dies mit dem Plugin Tweetly Updater und der Verwendung von bit.ly gelöst.

  15. Markus am 10.05.2010 · 13:41

    Also ich brauch gar keinen Code; aus irgendeinem Grund wird bei mir aus /?p=201/ automatisch /?p=201. Naja, halt ein paar Zeilen Code gespart. :-)

  16. Andi am 30.10.2010 · 17:53

    Hab auch grad so ein ähnliches Problem, hoffe kann was brauchen von hier!
    Gruss aus der Schweiz

  17. Dieter am 29.07.2011 · 07:07

    Hallo Thomas,

    habe Dein Plugin runtergeladen und aktivierte es. Allerdings wurden dann Umlaute und Sonderzeichen auf dieter-welzel.de nicht mehr korrekt angezeigt. Zeichensatz ist UTF-8.

    Auch die Ergänzung der.htaccess-Datei entsprechend der readme.txt änderte nichts. Habe ich etwas vergessen?

    Gruß
    Dieter

  18. Thomas Scholz am 29.07.2011 · 07:24

    @Dieter: Der Code hat einige Macken. Den benutze ich selbst so nicht mehr. Verwende nicht den HTTP-Header, dann sollte es zumindest keine Probleme mit der Zeichenkodierung geben.

  19. Dieter am 29.07.2011 · 07:43

    @Thomas: Schade, ich dachte Du setzt diese Lösung hier ein. Welchen HTTP-Header müsste ich denn wo entfernen? Aus dem Plugin oder meiner WordPress-Installation?

  20. Thomas Scholz am 29.07.2011 · 07:45

    @Dieter: Nimm das hier weg, oder kommentiere es aus:

    add_filter('wp_headers', 'shortlink_header', 10, $headers);
  21. Dieter am 29.07.2011 · 08:19

    @Thomas Scholz: Danke! Werde ich ausprobieren, wenn ich wieder zuhause bin.

  22. Dieter am 29.07.2011 · 18:26

    @Thomas Scholz: Es funktioniert. ☺

    Mindestens ein Getränk beim WordCamp geht auf mich!!! Du bist mein Retter und das nicht das erste Mal!

    Ich hatte schon fast aufgegeben, nachdem die Lösung von Peter Kröner nicht mehr funktionierte und ich Deine auch nicht zum Laufen bekam.

    Nochmals danke!!!