toscho.design

WordPress: Vertippte URIs abfangen

Im Zuge eines kleinen Gespräches drüben bei ›Schnurpsel‹ entstand die Idee, kleine Vertipper besser abzufangen als mit einer schnöden 404-Seite.

Das ist so einfach, daß ich es gleich hier eingebaut habe. Folgender Code kommt in die 404.php des Themes vor jede HTML-Ausgabe:

function toscho_maybe_redirect()
{
    $parts = explode('/', trim($_SERVER['REQUEST_URI'], '/') );

    // Start with the last element.
    $parts = array_reverse($parts);

    foreach ( $parts as $slug)
    {
        toscho_slug_to_301($slug);
    }

    // Failed.
    return;
}
function toscho_slug_to_301($slug)
{
    global $wpdb;
    $slug  = stripslashes($slug);
    $slug  = mysql_real_escape_string($slug);

    $id = $wpdb->get_col(
        "SELECT ID FROM $wpdb->posts
            WHERE post_status = 'publish'
            AND post_name = '$slug'
            LIMIT 1");

    if ( empty ( $id ) )
    {
        return;
    }

    $url = get_permalink( (int) $id[0] );

    wp_redirect( $url, 301 );

    // Okay, stop.
    die( "<a href='$url'>Hier entlang</a> bitte." );

    return ;
}
toscho_maybe_redirect();

Wer jetzt eingibt: http://toscho.de/2020/webdesign-fuers-ipad/xyz, der landet dennoch auf der richtigen Seite.

Ich halte solche Vertipper nicht für sehr wahrscheinlich, aber wenn sie so leicht zu erfassen sind, dann sei es so.

Wer eine andere Permalinkstruktur benutzt, der sollte eventuell das Ermitteln des Slugs entsprechend anpassen.

17 Kommentare

  1. Fabian am 16.04.2010 · 17:17

    Die Idee die 404er-Seite etwas freundlicher zu gestalten hatte ich auch mal. Prinzip ganz ähnlich, Code nicht ganz sauber aber funktionieren tut es schon.
    Ich hab nur nach dem letzten Wort suchen lassen und dann dazugehörige Beiträge ausgegeben. Ist nicht so effektiv - wird aber auch nicht so genutzt;)

  2. Schnurpsel am 16.04.2010 · 17:29

    Eigentlich könntest Du Dir das Zerlegen der URI auch sparen, hat ja WordPress schon gemacht:

    global $wp_query;
    $slug = $wp_query->query_vars['name'];
    ...
    
  3. Thomas Scholz am 16.04.2010 · 17:43

    @Schnurpsel: $wp_query->query_vars['name'] bzw. $wp_query->query_vars enthalten nur Informationen zum letzten Teil (xyz); die davor (webdesign-fuers-ipad) müßte ich dann doch separat ermitteln. Da finde ich eine gleichförmige Behandlung aller Teile übersichtlicher.

  4. Schnurpsel am 16.04.2010 · 17:49

    Stimmt, ich hatte mich jetzt zusehr nur auf die falsche Jahreszahl konzentriert und gar nicht wahrgenommen, daß da hinten auch noch was dran steht. Da geht Deine Fehlertoleranz ja nun wirklich sehr weit :-)

    Bei meiner Permalink-Struktur mit der ID wäre es sogar noch einfacher, ich könnte praktisch jeden beliebigen Vertipper im Slug auf den richtigen Artikel weiterleiten, sofern irgendwie die ID auswertbar ist. Aber das mache ich dann wohl doch nicht.

  5. Thomas Scholz am 16.04.2010 · 18:05

    @Schnurpsel: Fehler treten am Ende häufiger auf als am Anfang. Jemand schreibt beispielsweise in einer Mailingliste: »Geh mal auf http://example.com/how-to-link/und lies dir das durch.« Ein vergessenes Leerzeichen, und schon stimmt die URI nicht mehr. Deshalb teste ich im Code von hinten nach vorne: Das letzte Wort mag oft Humbug sein, aber das davor vielleicht nicht.

    Bei deiner Struktur fände ich es schön, wären die Artikel auch ohne die ID erreichbar. Dann könnte man die URI ruhigen Gewissens auch mal Telefon weitergeben oder aus dem Gedächtnis heraus eintippen.

  6. Schnurpsel am 16.04.2010 · 18:19

    Naja, noch besser wäre, wenn die Seite nur über die Nummer erreichbar ist. Eine Zahl kann ich mir zumindest einfacher merken, als einen möglicherweise langen Text.

    So nach dem Motto "Guck mal bei Schnurpsel nach dem Artikel 448, der schreibt da ziemlichen Unsinn". Das ist doch schön kurz und knackiger als "Bei Schnurpsel unter 'yahoo bildersuche setzt falsche url zu trefferseiten', und alles mit Minuszeichen getrennt".

    Aber das mit der Zahl geht ja in der Form /?p=448 sowieso.
    Technisch gesehen kann ich natürlich beides umsetzen, also nach Slug oder nach ID.

  7. Schnurpsel am 16.04.2010 · 18:29

    Kleiner Nachtrag wegen Links in Foren und so. Was ich bisher erlebt habe war so, daß da das Problem eher nicht am Ende der URL liegt, sondern mitten drin. Da gibt es Experten, die kopieren nicht die URL sondern die mit ... verkürzte Darstellung als Link irgendwo rein.

    In dem Fall würde dann natürlich die Auflösung nach der am Ende stehenden und damit gut erhaltenen ID bestens passen, da der Slug unbrauchbar ist. Diese Verkürzungspunkte stehen ja meist in der Mitte.

  8. andre.roesti am 16.04.2010 · 21:16

    Um die Ähnlichkeit zu überprüfen, könnte man auch zu levenshtein() greifen - ab einer gewissen Ähnlichkeit würde dann auf die passende Seite weitergeleitet.

  9. Thomas Scholz am 16.04.2010 · 21:32

    @andre.roesti: Wenn ich mir den dazu erforderlichen Code ansehe, wird mir einfach schlecht. ☺

  10. Ralf am 17.04.2010 · 09:06

    So kompliziert ist der Vergleich mit leneshtein() auch wieder nicht.

  11. Patrick am 17.04.2010 · 11:24

    Hmm besser wäre es noch, wenn man schon vor der 404er-Seite ansetzen könnte, sodass kein 404er-Fehler mitgeloggt wird. Hätte man da eventuell noch die Möglichkeit, das irgendwo einzubauen, ohne den WordPress-Core zu verändern?

    P.S.: Ich hab deine Funktion mal verkürzt, die ganzen Funktionsaufrufe können schon ein bisschen Overhead erzeugen: überarbeitete Funktion auf nopaste.info

    Aber ansonsten eine tolle Idee :)

  12. Thomas Scholz am 17.04.2010 · 14:00

    @Ralf: Um levenshtein() in PHP verwenden zu können, brauche ich den Vergleichsausdruck. Der kommt aber aus der der DB – und wie wähle ich den aus? Alle Slugs ziehen, und dann durch die Liste rennen? Das skaliert doch nicht.

  13. Thomas Scholz am 17.04.2010 · 14:15

    @Patrick: Man könnte eventuell da ansetzen, wo das 404-Template gesucht wird, und einen Filter auf "404_template" setzen. Oder auf 'status_header'; hier wäre das zweite Argument 404. Das wäre vermutlich die sauberste Lösung.

    Deine Variante wirft einen Schwung Variablen in den globalen Namensraum. Das halte ich nicht für eine gute Idee: Kollisionen mit Plugins, Themes oder Core-Code lassen sich so kaum vermeiden, wenn du nicht gerade jede Variable mit einem langen Präfix versiehst. Dann aber leidet die Lesbarkeit des Codes.
    Da die erste Funktion nur einmal aufgerufen wird und die zweite höchstens … zehnmal, entsteht keine meßbare Verzögerung durch eine saubere Abkapselung.

  14. Patrick am 17.04.2010 · 14:16

    Man könnte zumindest alles in eine Funktion packen ;)

  15. Thomas Scholz am 17.04.2010 · 14:48

    @Patrick: Das ist natürlich eine Geschmacksfrage. Ich bevorzuge das Prinzip der kurzen Funktionen. Finde ich einfach übersichtlicher. Streng gesehen müßte man auch die Suche nach dem Slug in der DB vom Redirect abkapseln; dann könnte man diesen Teil wiederverwenden.

  16. Ralf am 18.04.2010 · 11:46

    Was meinst du mit "das skaliert nicht"? Etwa das es zu viel Aufwand für zu wenig Nutzen wäre?
    Die 404-Seite ist ja nicht die Index-Seite, wird (hoffentlich) dementsprechend selten aufgerufen. Auf solchen Seiten kann man durchaus mal etwas mehr Aufwand betreiben.

    Wie du selber schon geschrieben hast, ist solch ein Vertipper bei dem der Schluss korrekt, der Fehler sich jedoch irgendwo mittendrin befindet, eher selten. Häufiger dürfte es vorkommen das jemand nicht die komplette URL kopiert oder in einem Forum Zeichen abgeschnitten werden. Dann kommst du mit deiner Methode auch nicht viel weiter als bis zur Fehlerseite. In Folge dessen wird der Besucher die Seite frustriert verlassen, oder wenn es ihm sehr wichtig ist, eine Suche starten.
    Von daher "skaliert" es dann doch wenn ich dem Besucher bereits auf der Fehlerseite Alternativen anbiete.

    Im übrigen bringt WordPress bereits eine Funktion mit die solche Fälle behandeln soll. In wp-includes/canonical.php gibt es die Funktion redirect_guess_404_permalink().
    Ich finde die Funktion jedoch nicht sehr gelungen da sie mit dem MySQL LIKE arbeitet. "xyc" könnte damit u.U. noch gefunden werden, "kyz" würde man damit jedoch niemals finden.
    Auch dass das ggf. vorhandene Datum über AND verknüpft wird ist nicht wirklich schön gelöst. Ist der Titel korrekt und das Datum falsch, werden ebenfalls keine Ergebnisse ausgegeben.
    Die Funktion zeigt jedoch das du dir das Zerlegen der URl tatsächlich hättest sparen können. In der globalen Variablen $wp_query steht so ziemlich alles drin was man braucht. Über get_query_var() kommt man auch recht bequem an das ran, was man sucht.

  17. Thomas Scholz am 23.04.2010 · 19:42

    @Ralf: Fast vergessen, aber doch nicht ganz.

    Die Funktion zeigt jedoch das du dir das Zerlegen der URl tatsächlich hättest sparen können. In der globalen Variablen $wp_query steht so ziemlich alles drin was man braucht.

    Schön wär’s. Ein Request auf /2020/webdesign-fuers-ipad/xyz mit einem var_dump($wp_query) bringt dies zutage:

    object(WP_Query)#292 (41) {
      ["query_vars"]=>
      array(49) {
        ["attachment"]=>
        string(3) "xyz"
        ["error"]=>
        string(0) ""
        ["m"]=>
        int(0)
        ["p"]=>
        int(0)
        ["post_parent"]=>
        string(0) ""
        ["subpost"]=>
        string(0) ""
        ["subpost_id"]=>
        string(0) ""
        ["attachment_id"]=>
        int(0)
        ["name"]=>
        string(3) "xyz"
        ["hour"]=>
        string(0) ""
        ["static"]=>
        string(0) ""
        ["pagename"]=>
        string(0) ""
        ["page_id"]=>
        int(0)
        ["second"]=>
        string(0) ""
        ["minute"]=>
        string(0) ""
        ["day"]=>
        int(0)
        ["monthnum"]=>
        int(0)
        ["year"]=>
        int(0)
        ["w"]=>
        int(0)
        ["category_name"]=>
        string(0) ""
        ["tag"]=>
        string(0) ""
        ["cat"]=>
        string(0) ""
        ["tag_id"]=>
        string(0) ""
        ["author_name"]=>
        string(0) ""
        ["feed"]=>
        string(0) ""
        ["tb"]=>
        string(0) ""
        ["paged"]=>
        int(0)
        ["comments_popup"]=>
        string(0) ""
        ["meta_key"]=>
        string(0) ""
        ["meta_value"]=>
        string(0) ""
        ["preview"]=>
        string(0) ""
        ["category__in"]=>
        array(0) {
        }
        ["category__not_in"]=>
        array(0) {
        }
        ["category__and"]=>
        array(0) {
        }
        ["post__in"]=>
        array(0) {
        }
        ["post__not_in"]=>
        array(0) {
        }
        ["tag__in"]=>
        array(0) {
        }
        ["tag__not_in"]=>
        array(0) {
        }
        ["tag__and"]=>
        array(0) {
        }
        ["tag_slug__in"]=>
        array(0) {
        }
        ["tag_slug__and"]=>
        array(0) {
        }
        ["caller_get_posts"]=>
        bool(false)
        ["suppress_filters"]=>
        bool(false)
        ["post_type"]=>
        string(0) ""
        ["posts_per_page"]=>
        int(20)
        ["nopaging"]=>
        bool(false)
        ["comments_per_page"]=>
        string(1) "0"
        ["order"]=>
        string(4) "DESC"
        ["orderby"]=>
        string(23) "wp_posts.post_date DESC"
      }
      ["request"]=>
      string(150) " SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND wp_posts.post_name = 'xyz' AND wp_posts.post_type = 'attachment'  ORDER BY wp_posts.post_date DESC "
      ["post_count"]=>
      int(0)
      ["current_post"]=>
      int(-1)
      ["in_the_loop"]=>
      bool(false)
      ["post"]=>
      NULL
      ["comments"]=>
      NULL
      ["comment_count"]=>
      int(0)
      ["current_comment"]=>
      int(-1)
      ["comment"]=>
      NULL
      ["found_posts"]=>
      int(0)
      ["max_num_pages"]=>
      int(0)
      ["max_num_comment_pages"]=>
      int(0)
      ["is_single"]=>
      bool(false)
      ["is_preview"]=>
      bool(false)
      ["is_page"]=>
      bool(false)
      ["is_archive"]=>
      bool(false)
      ["is_date"]=>
      bool(false)
      ["is_year"]=>
      bool(false)
      ["is_month"]=>
      bool(false)
      ["is_day"]=>
      bool(false)
      ["is_time"]=>
      bool(false)
      ["is_author"]=>
      bool(false)
      ["is_category"]=>
      bool(false)
      ["is_tag"]=>
      bool(false)
      ["is_tax"]=>
      bool(false)
      ["is_search"]=>
      bool(false)
      ["is_feed"]=>
      bool(false)
      ["is_comment_feed"]=>
      bool(false)
      ["is_trackback"]=>
      bool(false)
      ["is_home"]=>
      bool(false)
      ["is_404"]=>
      bool(true)
      ["is_comments_popup"]=>
      bool(false)
      ["is_admin"]=>
      bool(false)
      ["is_attachment"]=>
      bool(false)
      ["is_singular"]=>
      bool(false)
      ["is_robots"]=>
      bool(false)
      ["is_posts_page"]=>
      bool(false)
      ["is_paged"]=>
      bool(false)
      ["query"]=>
      array(1) {
        ["attachment"]=>
        string(3) "xyz"
      }
      ["posts"]=>
      &array(0) {
      }
    }

    Da steht eigentlich nichts drin, was ich wirklich brauche. Schade.