Skip to content


Wicket AjaxBookmarkablePageLink – Ajax und SEO

Nutzer haben eine hohe Erwartung an die Geschwindigkeit, mit der auch Webanwendungen auf Nutzereingaben reagieren sollen. Nicht erst seit gestern wird deshalb auf verschiedene Arten versucht, die Antwortzeit auf ein Minimum zu drücken. Häufig werden dann Seiteninhalte per Ajax ausgetauscht. Das ist angenehm für den Nutzer, Suchmaschinen sind aber (zur Zeit) nicht in der Lage, über solche Interaktionshürden zu springen und damit diese Inhalte in den Suchindex aufzunehmen.

Bisher hat man sich dann oft für das eine (kein Ajax) oder das andere (Ajax, versteckte SEO-Links) entschieden. Mit Wicket ist es nicht so schwer, einen sehr mächtigen Ansatz zu wählen, der beides kombiniert und das Leben des Entwicklers vereinfachen kann.

Wir zäumen das Pferd etwas von hinten aus, aber am Ende wird das alles einen Sinn ergeben. Versprochen. Zuerst brauchen wir etwas, was eine Seitenklasse und PageParameter in einen Wert vereint:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class BookmarkablePageDestination<T extends WebPage> {
  2.   private final Class<? extends T> _pageClass;
  3.   private final PageParameters _pageParameters;
  4.   public BookmarkablePageDestination(Class<? extends T> pageClass, PageParameters pageParameters) {
  5.     _pageClass = pageClass;
  6.     _pageParameters = new PageParameters(pageParameters);
  7.   }
  8.   public Class<? extends T> getPageClass() {
  9.     return _pageClass;
  10.   }
  11.   public PageParameters getPageParameters() {
  12.     // not immutable, so we have to copy
  13.     return new PageParameters(_pageParameters);
  14.   }
  15.   public String asUrl() {
  16.     return RequestCycle.get().urlFor(_pageClass, _pageParameters).toString();
  17.   }
  18.   public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass) {
  19.     return new BookmarkablePageDestination<T>(pageClass, new PageParameters());
  20.   }
  21.   public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass,
  22.       PageParameters pageParameters) {
  23.     return new BookmarkablePageDestination<T>(pageClass, pageParameters);
  24.   }
  25. }

Da die Klasse PageParameters nicht unveränderlich ist, müssen wir eine Kopie anfertigen, um unerwünschte Nebeneffekte zu vermeiden. Als nächstes erweitern wir die AjaxLink-Klasse um eine Funktion, die sie aus Suchmaschinensicht zu einem normalen Link macht.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public abstract class AjaxBookmarkablePageLink<T, P extends WebPage> extends AjaxLink<T> {
  2.   public AjaxBookmarkablePageLink(String id, IModel<T> model) {
  3.     super(id, model);
  4.   }
  5.   @Override
  6.   protected void onInitialize() {
  7.     super.onInitialize();
  8.     add(new AttributeModifier("href", new LoadableDetachableModel<String>() {
  9.       @Override
  10.       protected String load() {
  11.         return getDestination(getModelObject()).asUrl();
  12.       }
  13.     }));
  14.   }
  15.   protected abstract BookmarkablePageDestination<P> getDestination(T modelValue);
  16. }

Da sich das Ziel jederzeit ändern kann, muss das href-Attribut immer wieder erneuert werden. Dabei machen wir das Ergebnis abhängig vom Wert aus dem Model. Wichtiger Hinweis an dieser Stelle: normalerweise müsste man innerhalb der LoadableDetachableModel-Klasse für das verwendete Model die detach()-Methode aufrufen. Da das Model bereits durch die Link-Komponente in den Wicket-Renderzyklus eingebunden ist, ist das hier nicht notwendig. Man sollte das aber immer im Hinterkopf behalten, wenn man die Funktionsweise ändert.

Wir benötigen noch etwas, wass den Zustand der Seite abbildet und das man in Seitenparameter konvertieren und aus Seitenparametern auslesen kann. Wir nehmen ein einfaches Beispiel und verändern einen Zähler.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public interface IState extends Serializable {
  2.   IState oneUp();
  3.   IState oneDown();
  4.   int getCounter();
  5. }

 

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class StateConverter {
  2.   private StateConverter() {
  3.     // no instance
  4.   }
  5.   enum StateParameter {
  6.     Count,
  7.   };
  8.   public static IModel<IState> asModel(PageParameters pageParameters) {
  9.     return new Model<IState>(asState(pageParameters));
  10.   }
  11.   private static State asState(PageParameters pageParameters) {
  12.     int count = pageParameters.get(StateParameter.Count.name()).toInt(0);
  13.     return new State(count);
  14.   }
  15.   public static PageParameters asPageParameters(IState state) {
  16.     return new PageParameters().add(StateParameter.Count.name(), state.getCounter());
  17.   }
  18.   static class State implements IState, Serializable {
  19.     final int _count;
  20.     public State(int count) {
  21.       _count = count;
  22.     }
  23.     
  24.     @Override
  25.     public IState oneUp() {
  26.       return new State(_count + 1);
  27.     }
  28.     
  29.     @Override
  30.     public IState oneDown() {
  31.       return new State(_count - 1);
  32.     }
  33.     @Override
  34.     public int getCounter() {
  35.       return _count;
  36.     }
  37.   }
  38. }

Jetzt brauchen wir eine Linkklasse, die den Zähler verändert.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public abstract class CountLink extends AjaxBookmarkablePageLink<IState, SeoPage> {
  2.   private final WebMarkupContainer _ajaxBorder;
  3.   public CountLink(String id, IModel<IState> model, WebMarkupContainer ajaxBorder) {
  4.     super(id, model);
  5.     _ajaxBorder = ajaxBorder;
  6.   }
  7.   @Override
  8.   protected BookmarkablePageDestination<SeoPage> getDestination(IState state) {
  9.     return BookmarkablePageDestination.with(SeoPage.class, StateConverter.asPageParameters(nextState(state)));
  10.   }
  11.   @Override
  12.   public void onClick(AjaxRequestTarget target) {
  13.     setModelObject(nextState(getModelObject()));
  14.     target.add(_ajaxBorder);
  15.   }
  16.   protected abstract IState nextState(IState state);
  17. }

Bei einem Klick auf den Link wird der Wert im Model verändert, der Wert aus getDestination() zeigt auf das potentielle Ziel, der Einfachheit halber wird außerdem eine Komponente übergeben, die per Ajax aktualisiert werden soll. Jetzt fügen wir alles zusammen:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class SeoPage extends WebPage {
  2.   public SeoPage(PageParameters pageParameters) {
  3.     final IModel<IState> stateModel = StateConverter.asModel(pageParameters);
  4.     final WebMarkupContainer ajaxUpdate = new WebMarkupContainer("ajaxUpdate");
  5.     ajaxUpdate.setOutputMarkupId(true);
  6.     ajaxUpdate.add(new Label("count", new PropertyModel<Integer>(stateModel, "counter")));
  7.     ajaxUpdate.add(new CountLink("up", stateModel, ajaxUpdate) {
  8.       @Override
  9.       protected IState nextState(IState state) {
  10.         return state.oneUp();
  11.       }
  12.     });
  13.     ajaxUpdate.add(new CountLink("down", stateModel, ajaxUpdate) {
  14.       @Override
  15.       protected IState nextState(IState state) {
  16.         return state.oneDown();
  17.       }
  18.     });
  19.     add(ajaxUpdate);
  20.   }
  21. }

Zuerst wird aus den PageParametern der aktuelle Zustand der Seite ermittelt und in ein Model gepackt. Das Label zeigt den aktuellen Wert an, die zwei Links verändern den Wert. Das passende Markup dazu:

 HTML |  copy code |? 
  1. <html>
  2. <head>
  3.   <title>Seo Ajax Bookmarkable Page Links</title>
  4. </head>
  5. <body>
  6.   <div wicket:id="ajaxUpdate">
  7.     <span wicket:id="count"></span><br>
  8.     <a wicket:id="up">Up</a>
  9.      |
  10.     <a wicket:id="down">Down</a>
  11.   </div>
  12. </body>
  13. </html>

Und dann erhält man folgendes Ergebnis:

 HTML |  copy code |? 
  1. <body>
  2.   <div wicket:id="ajaxUpdate" id="ajaxUpdate1">
  3.     <span wicket:id="count">0</span><br>
  4.     <a wicket:id="up" id="up2" href="./seoPage?Count=1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-up&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;up2&#039;) != null;}.bind(this));return !wcall;">Up</a>
  5.      |
  6.     <a wicket:id="down" id="down3" href="./seoPage?Count=-1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-down&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;down3&#039;) != null;}.bind(this));return !wcall;">Down</a>
  7.   </div>
  8. </body>

Wie man sieht, kann man beide Anforderungen mit etwas Aufwand vereinen. Je nach Anforderung kann man das Konzept in verschiedenste Richtungen erweitern. Ich hoffe, ich konnte ein wenig die Richtung zeigen:)

Posted in Wicket.

Tagged with , , .


2 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. A.L. says

    Das ist in der Tat ein sehr gutes Konzept, vielen Dank für den schönen Artikel.

    Solange allerdings in Wicket Seiten, die Ajax enthalten, konzeptionell immer eine Seiten-ID als Request-Parameter bekommen, hat man mit Wicket und SEO sowieso ein Problem. Es gibt einige Ansätze, wie man das wegbekommt, die haben allerdings alle hochgradigen “Hack”-Charakter und scheinen mir nicht besonders ausgereift zu sein.

    • admin says

      Man kann auch den Anforderungen von SEO gerecht werden. Das fällt aber nicht einfach ab. Stateless-Ajax ist sicher eher etwas fragil, aber soweit muss man nicht gehen.



Some HTML is OK

or, reply to this post via trackback.