Morphium based Blogsoftware

Info

Datum: 29. 07. 2019 um 22:57:47

Schlagworte: Blog

Kategorie: morphium

erstellt von Stephan Bösebeck

logged in

ADMIN


Morphium based Blogsoftware

Morphium based Blog software Jblog

Ich habe ja schon vor einiger Zeit den Sprung weg von Wordpress "gewagt" und auch darüber berichtet. Jetzt ist das schon über 2 Jahre (emoji github:astonished) her und da ist es doch an der Zeit mal ein Fazit zu ziehen und ein wenig zu berichten, wie es so lief.

Jblog - the good the bad the ugly

Das gute vorweg: gehackt wurde der Server damit nicht. Die Zugriffe, die auf irgendwelche PHP-Lücken oder so abzielten sind alle wirkungslos "abgeprallt" - genau das wollte ich ja erreichen. Das Update Thema ist auch gelöst: es gab keine emoji github:smirk Ich habe mich selbst um die Weiterentwicklung gekümmert. Allerdings hatten und haben wir da schon auch den ein oder anderen Bug, und einige sind doch ein wenig nervig. Auch haben sich einige Dinge als nicht so ganz praktikabel herausgestellt. Die recht aufwändige Versionierung der einzelnen Blog-Posts ist eigentlich unnötig und verkompliziert alles nur.

Aber an Sonsten...

Technik

Was steckt aktuell hinter Jblog:

  • Morphium V4.0.6
  • Spring Boot
  • Apache Freemarker
  • Bootstrap 4
  • JDK11
  • MongoDB 4.0

ja, das war es eigentlich schon, ist recht übersichtlich geworden. D.h. die Anwendung läuft im Moment als Spring-Boot-App hinter einem Nginx.

Features

Ich habe mir mein Blog so gestrickt, wie ich es haben wollte. Also sehr stark auf meine Art des Bloggens ausgelegt, was vielleicht auch der Grund ist, warum es auf Github niemand geforked hat und ich das Projekt jetzt auf privat umgestellt habe. emoji github:sob

Mehrsprachigkeit

Jblog ist durchweg zweisprachig ausgelegt. Theoretisch wären auch mehr möglich, aber ich wollte mich auf Deutsch und Englisch konzentrieren. D.h. jedes Element auf der Webseite wird übersetzt, alle Blog-Einträge müssen in 2 Sprachen erfasst werden.

Dafür wird eine MessageSource verwendet, die die Daten aus der Mongo liest. Das schöne: wird eine Übersetzung nicht gefunden, wird dafür ein Link auf die Übersetzungsmaske ausgegeben. D.h. wenn ich Jblog mal auf einer leeren Übersetzungsdatenbank laufen lasse, bekomme ich einen Screen voller links, die mich zum Übersetzungstool bringen. Nach und nach wird das dann korrigiert.

Dadurch, dass wir Morphium für den Zugriff auf die Mongo nutzen, können diese Übersetzungen auch gecached werden (man muss nur die @Cache-Annotation an die Klassse schreiben).

Das Schöne daran ist, dass der Cache, wenn gewollt, clusterfähig ist. D.h. ich kann mehrere Instanzen von Jblog laufen lassen und über einen Loadbalancer zugreifen. Und diese würden ihre Caches über die Mongo synchron halten, d.h. es gibt selbst bei Round-Robin Einstellung keine Dirty Reads!

Markdown

Markdown ist ja der Hit in letzter Zeit um Text einzugeben. Ich finde das auch recht cool, vor allem, da ich ja auch recht gut tippe und mich der Griff zur Maus immer ausbremst. Deswegen ist das sehr angenehm einfach alles tippen zu können. (und wer so wie ich früher Textverarbeitung mit LaTeX gemacht hat, kennt das emoji github:smirk.)

Jblog ist komplett auf Markdown ausgelegt. D.h. man gibt seine Blogs in Markdown ein und sieht dann gleich den Preview. Ich habe auch einige Erweiterungen gemacht, insbesondere um einfach Emojis (emoji github:confused emoji github:smile emoji github:sweat) einzubinden oder einen schön formatierten Java Code-Block einbinden zu können.

Auch das einbetten von Bildern habe ich darüber implementiert.

Ich habe dafür eine Markdown-Implementierung namens Markingbird benutzt und etwas angepasst. So habe ich etwas eingebaut, um die Emojis einfacher einbetten zu können und um Bilder leichter anzuzeigen.

Jeder Blog-Post ist in Markdown geschrieben und wird erst beim ersten Zugriff gerendert. Und das Ergebnis wird gecached, natürlich emoji github:smirk

Beim Tippen habe ich einen Live-Preview und kann genau sehen, wie der Post dann am Schluss aussehen wird.

Twitter Anbindung

neue Artikel werden via Twitter veröffentlicht. Dabei wurden einige Dinge berücksichtigt: Zeit des Tweet vorgeben, nur für neue Artikel etc.

Und es wird gleich in 2 Sprachen getwittert, deutsch und englisch

Mehrbenutzerfähigkeit

Ja, eigentlich kann man Jblog auch mit mehreren Usern benutzen. Ich habe zwar auch ein paar Freunde und Bekannte, die hier mal von Zeit zu Zeit reinschneien und evtl. was posten, aber sonst... Ist also ein sehr wenig genutztes Feature, weshalb ich das dann auch daktiviert habe.

Im Moment kann man sich nicht registrieren, der Registrierungsprozess ist De-Aktiviert. Möchte man einen Kommentar hinterlassen, so kann man das via Disqus tun.

Implementierung

Die Implementierung hat sich ein wenig geändert in den letzten 2 Jahren. Gestartet mit JDK1.8 als klassisches Spring-Web Projekt mit WAR-Deployment, haben wir jetzt eine Spring-Boot Anwendung mit integriertem Tomcat 9 auf JDK11.

Ich nutze hier AdaptOpenJdk V11 weil ich den Longtermsupport benötige, damit ich nicht alle 6 Monate mein Blog neu aufsetzen muss.

Morphium

Natürlich soll das auch ein kleiner "Showcase" für Morphium sein, deswegen hier ein paar Beispiele:

Translations

Um die Anwendung einfach mehrsprachig zu halten, empfiehlt es sich, die Übersetzungsdaten in der Datenbank zu halten. Das hat viele Vorteile:

  • man kann die Übersetzungen zur Laufzeit ändern
  • man kann auch Zahlenformate etc. zur Laufzeit anpassen und damit auch das Aussehen der Anwendung verändern
  • wenn man es zulässt, kann man auch Links, IMG-Tags etc in die Übersetzungstabelle einbauen und so noch mehr Flexibilität erreichen

Allerdings kommen diese Features natürlich mit einem Preisschild:

  • ein wenige Programmieraufwand
  • die Performance könnte leiden, weil für jeden Seitenaufbau, mehrere Daten aus der Datenbank gelesen werden müssen. Hier hilft uns das Caching-Feature von Morphium
  • Man benötigt immer eine Datenbank, um eine Seite zu Gesicht zu bekommen, sonst kommt unsinn raus. Und die Pflege kann etwas umständlich sein.

Gerade der letzte Punkt hat mich genervt, also habe ich Localization so geschrieben, dass in dem Fall, dass keine Übesetzung gefunden wird, der Link auf die entsprechende Pflege-Oberfläche ausgegeben wird. Das hatte zwar auch so seine seltsamen Effekte (link in einem Link, link im Title etc), war im Großen und Ganzen aber wirklich einfach.

Ein wichtiger Punkt war auch, dass ich in der Übersetzungsmaske durch einen Mausklick nur die Einträge anzeigen lassen kann, bei denen noch eine Übersetzung fehlt. So kommt man recht schell zu einer vollständigen Übersetzung.

Das hier ist die MessageSource... ´´´java @Service public class LocalizationMessageSource extends AbstractMessageSource { @Autowired private LocalizationService localizationService;

@Override
protected MessageFormat resolveCode(String s, Locale locale) {

    String lang = locale.getLanguage();
    if (!LocalizationService.supportedLangs.contains(lang)) {
        return null;
    }
    return createMessageFormat(localizationService.getText(lang, s), locale);
}

} ´´´

Und hier der zugehörige Service, der den entsprechenden HTML-Link rausgibt, wenn es keine Übersetzung gibt.

´´´java @Service public class LocalizationService { public final static List supportedLangs = Arrays.asList("de", "en");

@Autowired
private Morphium morphium;


public String getText(String locale, String key) {
    key = key.replaceAll("[ \"'?!`´<>(){}-]", "_");
    Localization lz = morphium.findById(Localization.class, key);
    if (lz == null) {
        lz = new Localization();
        lz.setKey(key);
        for (String l : supportedLangs) {

            lz.getTxt().put(l, l + "_" + key);

        }
        morphium.store(lz);
    }

    if (key.startsWith("format_") && lz.getTxt().get(locale).equals(locale + "_" + key)) {
        lz.getTxt().put(locale, "yyyy-MM-dd HH:mm:ss 'default'");
        morphium.store(lz);
        return lz.getTxt().get(locale);
    }

    if (lz.getTxt().get(locale).equals(locale+"_"+key)){
        return "<a href=/admin/translations/query?queryKey="+key+">"+key+"</a>";
    }
    return lz.getTxt().get(locale);
}

} ´´´

Das ist super praktisch wenn man neue Features rein bringt.

Würde man das für eine größere Webseite machen, könnte das schnell umständlich sein. Vor allem, wenn man auch noch mehr als 2 Sprachen unterstützen will. Deswegen empfiehlt sich in so einem Fall ein Im-/Export der Daten, am besten in einem für Übersetzungsagenturen lesbaren Format!

Es gibt noch ein weiteres "Problem" bei den übersetzungen: Waisen! Es passiert relativ häufig, dass man während der Entwicklung übersetzungen für irgendetwas anlegt, was im Laufe der Zeit aber obsolet wird. Dann hat man Übersetzungen in der Datenbank, die keiner mehr benötigt. Diese zu identifizieren ist nicht so einfach möglich, da die Schlüssel für die Übersetzungen teilweise abhängig von Daten sind und diese erst zu Laufzeit bereit stehen.

Morphium bietet für so einen Fall die Option in einem Feld einen lastAccessTimestamp abzulegen. Damit kann man sehen, wann einträge zuletzt gelesen wurden. (Achtung: das nur in Verbindung mit Caching nutzen, da jeder Lesezugriff auf diese Elemente einen Schreibzugriff zur Folge hat!).

Wenn man das alles zusammenwirft, kommt folgende Entity raus:

´´´ @Entity @Cache(maxEntries = 10000, timeout = 60 * 60 * 1000, clearOnWrite = true, strategy = Cache.ClearStrategy.FIFO, syncCache = Cache.SyncCacheStrategy.CLEAR_TYPE_CACHE) @LastAccess public class Localization { @Id private String key; @LastAccess @Index private long lastAccess; private Map<String, String> txt;

public String getKey() {
    return key;
}

public void setKey(String key) {
    this.key = key;
}

public Map<String, String> getTxt() {
    if (txt == null) txt = new HashMap<>();
    return txt;
}

public void setTxt(Map<String, String> txt) {
    this.txt = txt;
}

public long getLastAccess() {
    return lastAccess;
}

public void setLastAccess(long lastAccess) {
    this.lastAccess = lastAccess;
}

} ´´´

Der Cache hier behält seine Einträge für eine Stunde (60x60x1000ms) und darf maximal 10000 Einträge haben. Elemente werden aus dem Cache entfernt, wenn sie entweder länger als eine Stunde im Cache sind, oder die 10000 Einträge erreicht werden. IM letzteren Fall werden Elemente gemäß der Strategy entfernt. Diese ist per default ClearStrategy.FIFO (d.h. das am längsten im Cache befindliche Object wird entfernt um eines hinzuzufügen). Es gibt auch noch die Strategien ClearStrategy.LRU (Least Recently Used - also am längsten nicht benutzt) sowie ClearStrategy.RANDOM welches einen Zufälligen entry entfernt.

Sollte es einen Schreibzugriff auf die Übersetzungen geben, wird der Cache komplett geleert.

Falls man Jblog im Cluster verwendet, benötigt man eine Möglichkeit die Caches auf den Knoten synchron zu halten. Dafür definiert man eine SyncCacheStrategy. Dabei passiert folgendes:

  • alle Jblog instanzen initialisieren ein Messagingsystem in Morphium
  • jedes update auf gecachten Elementen sendet eine Update-Nachricht über das Messaging
  • je nach Strategie aktualisieren die einzelnen Knoten ihren cache.
    • SyncCacheStrategy.CLEAR_TYPE_CACHE löscht den ganzen Cache für dieses Entity
    • SyncCacheStrategy.REMOVE_ENTRY_FROM_TYPE_CACHE entfernt den Eintrag aus allen Suchergebnissen im Cache (Achtung: das Element wird dann in gecachten Suchergebnissen nicht mit ausgegeben, obwohl es evtl. dazu passen würde)
    • SyncCacheStrategy.UPDATE_ENTRY updated den Eintrag im Cache (Achtung: kann dazu führen, dass das Element im Cache zu Suchergebnissen gezählt wird, obwohl es eigentlich nicht mehr auf die Kriterien passt - dirty read!)
    • SyncCacheStrategy.NONE - naja... eben nicht syncen emoji github:smirk

Damit das aber sauber funktioniert, muss die Anwendung wissen, welche Sprache verwendet wird. Dazu werden verschiedene Schritte benutzt:

  • steht in der URL ein lang-Parameter drin, überschreibt der alles, was gesetzt ist
  • gibt es ein Cookie mit der Language, dann nehmen wir das
  • Sendet der Browser Info über die unterstützten Sprachen, nutzen wir das.
  • wenn alles fehl schlägt, nimm englisch emoji github:smirk

Implementiert wurde das mit einem Interceptor:

´´´java @Component public class ValidLocaleCheckInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String lang = "de"; if (request.getParameter("lang") != null) { String ln = request.getParameter("lang"); if (ln.contains("")) { ln = ln.substring(0, ln.indexOf("")); } ln = ln.toLowerCase(); if (ln.isEmpty()) { ln = "de"; }

        if (!LocalizationService.supportedLangs.contains(ln)) {
            StringBuilder url = new StringBuilder();
            url.append(request.getRequestURI());
            Map<String, String[]> map = request.getParameterMap();
            String sep = "?";
            for (String n : map.keySet()) {
                if (n.equals("lang")) {
                    continue;
                }
                for (String v : map.get(n)) {
                    url.append(sep);
                    sep = "&";
                    url.append(n);
                    url.append("=");
                    url.append(v);
                }

            }
            url.append(sep);
            url.append("lang=de"); //Default locale
            response.sendRedirect(url.toString());
            //Utils.showError(404,"unsupported language",response);
            return false;
        }
        lang = ln;
    } else {
        Cookie[] cookies = request.getCookies();
        boolean found = false;
        if (cookies != null) {
            for (int i = 0; i < cookies.length; i++) {
                if (cookies[i].getName().equals("lang")) {
                    lang = cookies[i].getValue();
                    found = true;
                    break;
                }
            }
        }
        if (!found) {
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            Locale l = localeResolver.resolveLocale(request);
            if (l != null) {
                lang = l.getLanguage();
            }
        }
    }
    request.getSession().setAttribute("lang", lang);
    Cookie c = new Cookie("lang", lang);
    c.setPath("/");
    c.setMaxAge(365 * 24 * 60 * 60);
    response.addCookie(c);
    return true;
}

´´´

Tag Cloud

Um die Tag Cloud zu berechnen nutzen wir einfach den in Mongo befindlichen Aggregator, und dafür gibt es natürlich auch eine Unterstüztung in Morphium:

´´´java Aggregator<BlogEntry,WordCloud> a=morphium.createAggregator(BlogEntry.class,WordCloud.class); a.match(morphium.createQueryFor(BlogEntry.class).f(BlogEntry.Fields.state).eq(Status.LIVE).f("wl").eq(wl));

Map<String,Object> projection=new HashMap<>(); Map<String,Object> arrayElemAt=new HashMap<>(); ArrayList l = new ArrayList<>(); l.add("$category_path"); l.add(0); arrayElemAt.put("$arrayElemAt", l); projection.put("category",arrayElemAt); a.project(projection);

a.group("$category").sum("count",1).end(); a.sort("-count"); log.debug("Preparing category cloud"); List cCloud = a.aggregate(); ´´´

Hier wird eine einfache Aggregation auf den in der Mongo gespeicherten BlogEntry - Einträgen gemacht. Dabei wird das ganze nach Whitelabel (wl) gefiltert (Jblog unterstützt mehrere Whitelabel, bei mir sind es boesebeck.biz, boesebeck.name und caluga.de) und es werden nur Posts im Status LIVE in Betracht gezogen.

Auf diesen Daten wird eine Projektion durchgeführt, um die einzelnen Kategorien herauzufiltern ($arrayElementAt). Dann zählen wir noch die Vorkommen dieser Kategoreien und voila! Eine Cloud...

und damit das ganze auch in Java richtig abgebildet wird, kann man das Ergebnis der Aggregation in eine @Entity packen:

´´´java @Entity public class WordCloud { @Id public String word; public int count; public int sz=0;

    public String getWord() {
        return word;
    }

    public void setWord(String word) {
        this.word = word;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public int getSz() {
        return sz;
    }

    public void setSz(int sz) {
        this.sz = sz;
    }
}

´´´

nun wird im Frontend nur noch die liste der WordCounts ausgegeben, mit entsprechender Größe Sz - und damit es "lustig" aussieht, wird das ganze noch randomisiert...

Fertig!

Caching

Caching nutzt Jblog ziemlich häufig, um die Performance hoch zu halten. Insbesondere die Übersetzungen sind gecached. Dadurch hat man die Möglichkeit, die Übersetzungen jederzeit über ein Frontend zu ändern, ohne dass man neu deployen oder neu starten muss, andererseits werden diese Daten eben nur selten geändert, weslhalb es gut ist, diese im Speicher zu halten um die Zugriffe auf die Datenbank zu minimieren und somit die Performance zu maximieren.

In Morphium geht das recht einfach: bei einem beliebigen Entity einfach die Annotation @Cache hinzufügen.

´´´java @Cache @Entity public class Translation { ... } ´´´

Statistiken

Mal abgesehen davon, dass Morphium eigene Statistiken für z.B. die Cache-Effizienz einzelner Entities mitbringt (abzurufen über morphium.getStatistics();- liefert eine Map<String,Double> mit insbesondere Anzahl Elementen im Cache, cache hit ratio etc.), benötigt so ein Blog doch ein paar Informationen über die Zugriffe darauf.

Das ganze wird in Jblog über einen eigenen Service gehandhabt, der die zugriffe tagesgenau mitzählt:

  • die Stats werden in einer eigenen Collection abgelegt, und als _id wird das Datum verwendet
  • auf den Stats werden dann mit dem Datum zusammen inc commandos ausgeführt um die Anzahl der Hits / Visitors zu erhöhen: morphium.createQueryFor(Stats.class).f(Stats.Fields.id).eq(dateString).inc(Stats.Fields.hits,1);
  • damit die Zugriffe durch Bots nicht verfälscht werden, sollte man Bots weitgehend herausfiltern. Dazu schaut man sich den Request-Header an, der UserAgent gibt Auskunft (zumindest ein wenig - 100% erwischt man damit auch nicht).
  • Ich logge auch noch die maximale Anzahl Conccurent User. Laut meiner Defintion für meine Applikationen ist das die Anzahl der unique Session Ids, welche einen Hit in den letzten 3 Minuten hatten. Um das zu messen, muss die Anwendung timestamps mitführen und die Daten entsprechend aggregieren.

Das in die Mongo zu packen ist so etwas umständlich und reicht für mein Mini-Blog aus. Hätte ich mehr Traffic würde ich das auf keinen Fall so machen, sondern eine dedizierte Time-Series-DB dafür nutzen, wie z.B. Influx oder Graphite.

Versionierung der BlogPosts

Das ist ein Feature, dass es eigentlich nicht bedarf und sicherlich unter "over-engineering" abgehakt werden kann. Die Idee: man hat für jeden Block-Post eine History an Änderungen und kann zu jeder Änderung zurückspringen. Das ist zwar ganz nett, aber in meinem Fall durchaus nutzlos emoji github:smirk

Hier die Entity für einen BlogPost:

´´´java @Entity @CreationTime @LastChange @Lifecycle @Cache(timeout = 60000 * 10, maxEntries = 1000) @Index(value = {"text.de:text,text.en:text,title.de:text,title.en:text", "state,visible_since"}) public class BlogEntry extends BlogEntryEmbedded { @Id protected MorphiumId id; private List revisions; private BlogEntryEmbedded currentEdit;

public List<BlogEntryEmbedded> getRevisions() {
    return revisions;
}

public void setRevisions(List<BlogEntryEmbedded> revisions) {
    this.revisions = revisions;
}

public BlogEntryEmbedded getCurrentEdit() {
    return currentEdit;
}

public void setCurrentEdit(BlogEntryEmbedded currentEdit) {
    this.currentEdit = currentEdit;
}

public MorphiumId getId() {
    return id;
}

public void setId(MorphiumId id) {
    this.id = id;
}

@PreStore
public void preStore() {
    if (year == 0) {
        GregorianCalendar cal = new GregorianCalendar();
        year = cal.get(Calendar.YEAR);
        month = cal.get(Calendar.MONTH) + 1;
        day = cal.get(Calendar.DAY_OF_MONTH);
    }
}

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    //        if (!(o instanceof BlogEntry)) {
    //            return false;
    //        }
    if (!super.equals(o)) {
        return false;
    }

    BlogEntry entry = (BlogEntry) o;

    if (id != null ? !id.equals(entry.id) : entry.id != null) {
        return false;
    }
    if (revisions != null ? !revisions.equals(entry.revisions) : entry.revisions != null) {
        return false;
    }
    return currentEdit != null ? currentEdit.equals(entry.currentEdit) : entry.currentEdit == null;
}

@Override
public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (id != null ? id.hashCode() : 0);
    result = 31 * result + (revisions != null ? revisions.hashCode() : 0);
    result = 31 * result + (currentEdit != null ? currentEdit.hashCode() : 0);
    return result;
}

} ´´´

und darin embedded die einzelnen Versionen:

´´´java @Embedded public class BlogEntryEmbedded {

protected Map<String, String> title;
//@Reference(lazyLoading = true)
protected MorphiumId creator;

@Index
protected String wl;

@CreationTime
protected Long created;
@LastChange
protected Long lastUpdate;
protected int year;
protected int month;
protected int day;
protected List<String> tags;


protected Map<String, String> text;
protected Map<String, String> renderedPreview;
protected long visibleSince;
@Index
protected Status state = Status.NEW;
private List<String> categoryPath;
@Index
private Map<String, String> titleEscaped;
private boolean tweetAboutIt;
private long tweetedAt;
private long tweetId;
private List<String> additionalVisibleOn;

public MorphiumId getCreator() {
    return creator;
}

public void setCreator(MorphiumId creator) {
    this.creator = creator;
}

public long getTweetId() {
    return tweetId;
}

public void setTweetId(long tweetId) {
    this.tweetId = tweetId;
}

public Long getCreated() {
    return created;
}

public void setCreated(Long created) {
    this.created = created;
}

public Long getLastUpdate() {
    return lastUpdate;
}

public void setLastUpdate(Long lastUpdate) {
    this.lastUpdate = lastUpdate;
}

public int getYear() {
    return year;
}

public void setYear(int year) {
    this.year = year;
}

public int getMonth() {
    return month;
}

public void setMonth(int month) {
    this.month = month;
}

public int getDay() {
    return day;
}

public void setDay(int day) {
    this.day = day;
}

public List<String> getTags() {
    if (tags == null) tags = new ArrayList<>();
    return tags;
}

public void setTags(List<String> tags) {
    this.tags = tags;
}



public long getVisibleSince() {
    return visibleSince;
}

public void setVisibleSince(long visibleSince) {
    this.visibleSince = visibleSince;
}

public Status getState() {
    return state;
}

public void setState(Status state) {
    this.state = state;
}

public Map<String, String> getTitle() {
    if (title == null) title = new HashMap<>();
    return title;
}

public void setTitle(Map<String, String> title) {
    this.title = title;
}

public Map<String, String> getText() {
    if (text == null) text = new HashMap<>();
    return text;
}

public void setText(Map<String, String> text) {
    this.text = text;
}

public Map<String, String> getRenderedPreview() {
    if (renderedPreview == null) renderedPreview = new HashMap<>();
    return renderedPreview;
}

public void setRenderedPreview(Map<String, String> renderedPreview) {
    this.renderedPreview = renderedPreview;
}

public List<String> getCategoryPath() {
    if (categoryPath == null) categoryPath = new ArrayList<>();
    return categoryPath;
}

public void setCategoryPath(List<String> categoryPath) {
    this.categoryPath = categoryPath;
}

public Map<String, String> getTitleEscaped() {
    if (titleEscaped == null) titleEscaped = new HashMap<>();
    return titleEscaped;
}

public void setTitleEscaped(Map<String, String> titleEscaped) {
    this.titleEscaped = titleEscaped;
}

public String getWl() {
    return wl;
}

public void setWl(String wl) {
    this.wl = wl;
}

public boolean isTweetAboutIt() {
    return tweetAboutIt;
}

public void setTweetAboutIt(boolean tweetAboutIt) {
    this.tweetAboutIt = tweetAboutIt;
}

public long getTweetedAt() {
    return tweetedAt;
}

public void setTweetedAt(long tweetedAt) {
    this.tweetedAt = tweetedAt;
}

public List<String> getAdditionalVisibleOn() {
    if (additionalVisibleOn == null) additionalVisibleOn = new ArrayList<>();
    return additionalVisibleOn;
}

public void setAdditionalVisibleOn(List<String> additionalVisibleOn) {
    this.additionalVisibleOn = additionalVisibleOn;
}

} ´´´

Wie einleitend schon erwähnt, ist das ein Feature, dass ich wohl wieder ausbauen werde. So richtig viel bringen tut es nicht. Aber ist schön, sowas mal implementiert zu haben.

Fazit

Jblog ist sicherlich nicht für jeden Blogger geeignet, insbesondere die vielen Features, die Anpassungsfähigkeit von z.B. Wordpress fehlen hier völlig! Wenn ich mein Blog anders aussehen lassen will, muss ich mich schon selbst ran setzen und das Ding umbauen. Da gibt’s keine Plugins.

Allerdings muss ich sagen, dass ich mit Jblog so gut wie keine Probleme mehr hatte, seit es eingesetzt ist. Und das ist doch auch was... für mich als privaten Hobby-Blogger sicherlich eines der wichtigsten, wenn nicht sogar das allerwichtigste Feature überhaupt.

Falls doch jemand Interesse hat, sich jblog mal näher anzugucken, ich stelle es gerne wieder online - einfach hier nen Kommentar hinterlassen, oder ne Mail an mich. Im Moment läuft das ganze als private project in GitHub, weil es so einfach simpler ist, z.B. mit deployment keys etc zu arbeiten.

© 2023 Caluga - Java blog All rights reserved.