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:

public class BookmarkablePageDestination<T extends WebPage> {

  private final Class<? extends T> _pageClass;
  private final PageParameters _pageParameters;

  public BookmarkablePageDestination(Class<? extends T> pageClass, PageParameters pageParameters) {
    _pageClass = pageClass;
    _pageParameters = new PageParameters(pageParameters);
  }

  public Class<? extends T> getPageClass() {
    return _pageClass;
  }

  public PageParameters getPageParameters() {
    // not immutable, so we have to copy
    return new PageParameters(_pageParameters);
  }

  public String asUrl() {
    return RequestCycle.get().urlFor(_pageClass, _pageParameters).toString();
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass) {
    return new BookmarkablePageDestination<T>(pageClass, new PageParameters());
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass,
      PageParameters pageParameters) {
    return new BookmarkablePageDestination<T>(pageClass, pageParameters);
  }
}

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.

public abstract class AjaxBookmarkablePageLink<T, P extends WebPage> extends AjaxLink<T> {

  public AjaxBookmarkablePageLink(String id, IModel<T> model) {
    super(id, model);
  }

  @Override
  protected void onInitialize() {
    super.onInitialize();

    add(new AttributeModifier("href", new LoadableDetachableModel<String>() {

      @Override
      protected String load() {
        return getDestination(getModelObject()).asUrl();
      }
    }));
  }

  protected abstract BookmarkablePageDestination<P> getDestination(T modelValue);
}

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.

public interface IState extends Serializable {
  IState oneUp();
  IState oneDown();
  int getCounter();
}

public class StateConverter {

  private StateConverter() {
    // no instance
  }

  enum StateParameter {
    Count,
  };

  public static IModel<IState> asModel(PageParameters pageParameters) {
    return new Model<IState>(asState(pageParameters));
  }

  private static State asState(PageParameters pageParameters) {
    int count = pageParameters.get(StateParameter.Count.name()).toInt(0);
    return new State(count);
  }

  public static PageParameters asPageParameters(IState state) {
    return new PageParameters().add(StateParameter.Count.name(), state.getCounter());
  }

  static class State implements IState, Serializable {

    final int _count;

    public State(int count) {
      _count = count;
    }
    
    @Override
    public IState oneUp() {
      return new State(_count + 1);
    }
    
    @Override
    public IState oneDown() {
      return new State(_count - 1);
    }

    @Override
    public int getCounter() {
      return _count;
    }
  }
}

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

public abstract class CountLink extends AjaxBookmarkablePageLink<IState, SeoPage> {

  private final WebMarkupContainer _ajaxBorder;

  public CountLink(String id, IModel<IState> model, WebMarkupContainer ajaxBorder) {
    super(id, model);
    _ajaxBorder = ajaxBorder;
  }

  @Override
  protected BookmarkablePageDestination<SeoPage> getDestination(IState state) {
    return BookmarkablePageDestination.with(SeoPage.class, StateConverter.asPageParameters(nextState(state)));
  }

  @Override
  public void onClick(AjaxRequestTarget target) {
    setModelObject(nextState(getModelObject()));
    target.add(_ajaxBorder);
  }

  protected abstract IState nextState(IState state);
}

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:

public class SeoPage extends WebPage {

  public SeoPage(PageParameters pageParameters) {
    final IModel<IState> stateModel = StateConverter.asModel(pageParameters);

    final WebMarkupContainer ajaxUpdate = new WebMarkupContainer("ajaxUpdate");
    ajaxUpdate.setOutputMarkupId(true);

    ajaxUpdate.add(new Label("count", new PropertyModel<Integer>(stateModel, "counter")));

    ajaxUpdate.add(new CountLink("up", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneUp();
      }
    });

    ajaxUpdate.add(new CountLink("down", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneDown();
      }
    });

    add(ajaxUpdate);
  }
}

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>
<head>
  <title>Seo Ajax Bookmarkable Page Links</title>
</head>
<body>
  <div wicket:id="ajaxUpdate">
    <span wicket:id="count"></span><br> 
    <a wicket:id="up">Up</a>
     | 
    <a wicket:id="down">Down</a>
  </div>
</body>
</html>

Und dann erhält man folgendes Ergebnis:

<body>
  <div wicket:id="ajaxUpdate" id="ajaxUpdate1">
    <span wicket:id="count">0</span><br> 
    <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>
     | 
    <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>
  </div>
</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:)

Tags:

Veröffentlicht in Wicket, .