diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/ArticleModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/ArticleModel.java
new file mode 100644
index 000000000..c9c45ea33
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/pages/models/ArticleModel.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.librecms.pages.models;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.librecms.contentsection.ContentItem;
+import org.librecms.contenttypes.Article;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+/**
+ * Model for getting the special properties of an {@link Article}. For general
+ * properties of an content item use {@link ContentItemModel}.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsPagesArticleModel")
+public class ArticleModel {
+
+ @Inject
+ private ContentItemModel contentItemModel;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ public String getTitle() {
+ return getArticle()
+ .getTitle()
+ .getValue(globalizationHelper.getNegotiatedLocale());
+ }
+
+ public String getDescription() {
+ return getArticle()
+ .getDescription()
+ .getValue(globalizationHelper.getNegotiatedLocale());
+ }
+
+ public String getText() {
+ return getArticle()
+ .getText()
+ .getValue(globalizationHelper.getNegotiatedLocale());
+ }
+
+ protected Article getArticle() {
+ final ContentItem contentItem = contentItemModel.getContentItem();
+ if (contentItem instanceof Article) {
+ return (Article) contentItem;
+ } else {
+ throw new WebApplicationException(
+ "Current content item is not an article.",
+ Response
+ .status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("Current content item is not an article.")
+ .build()
+ );
+ }
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java
new file mode 100644
index 000000000..dd27e25ad
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.librecms.pages.models;
+
+import org.libreccm.categorization.Category;
+import org.librecms.pages.PagesRouter;
+
+import java.io.Serializable;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ * Model for MVC applications providing access to the current category. This
+ * model MUST be initalized by the calling application (for example
+ * {@link PagesRouter} with the current category.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsPagesCategoryModel")
+public class CategoryModel implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private Category category;
+
+ private String categoryPath;
+
+ public Category getCategory() {
+ return category;
+ }
+
+ public void setCategory(final Category category) {
+ this.category = category;
+ }
+
+ public String getCategoryPath() {
+ return categoryPath;
+ }
+
+ public void setCategoryPath(final String categoryPath) {
+ this.categoryPath = categoryPath;
+ }
+
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemModel.java
new file mode 100644
index 000000000..a6486a11a
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemModel.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2021 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.librecms.pages.models;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.libreccm.categorization.Categorization;
+import org.libreccm.categorization.Category;
+import org.libreccm.categorization.CategoryManager;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.librecms.contentsection.ContentItem;
+import org.librecms.contentsection.ContentItemVersion;
+import org.librecms.pages.PagesRouter;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
+import javax.persistence.criteria.Root;
+import javax.ws.rs.NotFoundException;
+
+/**
+ * Retrieves a categorized content item for the current category. To work, the
+ * {@link #itemName} property MUST be initalized by the using application (for
+ * example {@link PagesRouter}. The value for {@link #itemName} is usually
+ * determined from the requested URL.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsPagesCategorizedItemModel")
+public class ContentItemModel {
+
+ private static final Logger LOGGER = LogManager.getLogger(ContentItemModel.class
+ );
+
+ @Inject
+ private CategoryManager categoryManager;
+
+ @Inject
+ private CategoryModel categoryModel;
+
+ @Inject
+ private EntityManager entityManager;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ private DateTimeFormatter dateTimeFormatter
+ = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.systemDefault());
+
+ private String itemName;
+
+ private ContentItemVersion itemVersion;
+
+ private Optional contentItem;
+
+ public String getItemName() {
+ return itemName;
+ }
+
+ public void setItemName(final String itemName) {
+ this.itemName = itemName;
+ }
+
+ /**
+ * Retrieves the current content item. Depending if {@link #itemName} has
+ * been initalized with a value either the index item of the current
+ * category or the item in the category identified by {@link #itemName} will
+ * be retrieved. The item is only received once per request. The method will
+ * retrieve the item on the first call and store the result in
+ * {@link #contentItem}. Subsequent calls will return the value of
+ * {@link #contentItem}. If {@link #itemName} is not {@code null} and there
+ * is no content item with the requested name in the category this method
+ * throws a {@link NotFoundException}.
+ *
+ * @return The requested categorized item. If {@link #itemName} is
+ * {@code null}, and the current category has not index item, the
+ * method will return {@code null}.
+ *
+ * @throws NotFoundException If there is no item identified by the name in
+ * {@link #itemName}.
+ */
+ public ContentItem getContentItem() {
+ return getOrRetrieveContentItem().orElse(null);
+ }
+
+ public long getObjectId() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getObjectId)
+ .orElse(0L);
+ }
+
+ public String getUuid() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getUuid)
+ .orElse("");
+ }
+
+ public String getDisplayName() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getDisplayName)
+ .orElse("");
+ }
+
+ public String getItemUuid() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getItemUuid)
+ .orElse("");
+ }
+
+ public String getName() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getName)
+ .map(
+ localized -> localized.getValue(
+ globalizationHelper.getNegotiatedLocale()
+ )
+ )
+ .orElse("");
+ }
+
+ public String getTitle() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getTitle)
+ .map(
+ localized -> localized.getValue(
+ globalizationHelper.getNegotiatedLocale()
+ )
+ ).orElse("");
+ }
+
+ public String getDescription() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getDescription)
+ .map(
+ localized -> localized.getValue(
+ globalizationHelper.getNegotiatedLocale()
+ )
+ )
+ .orElse("");
+ }
+
+ public String getVersion() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getVersion)
+ .map(ContentItemVersion::toString)
+ .orElse("");
+ }
+
+ public String getCreationDate() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getCreationDate)
+ .map(Date::toInstant)
+ .map(instant -> instant.atZone(ZoneId.systemDefault()))
+ .map(ZonedDateTime::toLocalDateTime)
+ .map(dateTimeFormatter::format)
+ .orElse("");
+ }
+
+ public String getLastModified() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getLastModified)
+ .map(Date::toInstant)
+ .map(instant -> instant.atZone(ZoneId.systemDefault()))
+ .map(ZonedDateTime::toLocalDateTime)
+ .map(dateTimeFormatter::format)
+ .orElse("");
+ }
+
+ public String getCreationUser() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getCreationUserName)
+ .orElse("");
+ }
+
+ public String getLastModifyingUserName() {
+ return getOrRetrieveContentItem()
+ .map(ContentItem::getLastModifyingUserName)
+ .orElse("");
+ }
+
+ private Optional getOrRetrieveContentItem() {
+ if (contentItem == null) {
+ retrieveContentItem();
+ }
+ return contentItem;
+ }
+
+ private void retrieveContentItem() {
+ if (itemName == null) {
+ retrieveIndexItem();
+ } else {
+ retrieveCategorizedItem();
+ }
+ }
+
+ private void retrieveIndexItem() {
+ final Category category = categoryModel.getCategory();
+
+ contentItem = categoryManager
+ .getIndexObject(category)
+ .stream()
+ .filter(object -> object instanceof ContentItem)
+ .map(object -> (ContentItem) object)
+ .filter(item -> item.getVersion() == itemVersion)
+ .findFirst();
+ }
+
+ private void retrieveCategorizedItem() {
+ final Category category = categoryModel.getCategory();
+ final CriteriaBuilder builder = entityManager
+ .getCriteriaBuilder();
+ final CriteriaQuery criteriaQuery = builder
+ .createQuery(ContentItem.class);
+
+ final Root from = criteriaQuery.from(
+ ContentItem.class
+ );
+ final Join join = from.join(
+ "categories"
+ );
+
+ final TypedQuery query = entityManager
+ .createQuery(criteriaQuery
+ .select(from)
+ .where(builder.and(
+ builder.equal(from.get("displayName"), itemName),
+ builder.equal(
+ from.get("version"),
+ ContentItemVersion.DRAFT
+ ),
+ builder.equal(join.get("category"), category)
+ )));
+
+ try {
+ contentItem = Optional.of(query.getSingleResult());
+ } catch (NoResultException ex) {
+ LOGGER.warn(
+ "No ContentItem with name \"{}\" in Category \"{}\".",
+ itemName,
+ Objects.toString(category)
+ );
+ throw new NotFoundException(
+ String.format(
+ "No ContentItem with name \"%s\" in Category \"%s\".",
+ itemName,
+ Objects.toString(category)
+ )
+ );
+ }
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemTypeModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemTypeModel.java
new file mode 100644
index 000000000..b61f27c66
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/pages/models/ContentItemTypeModel.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.librecms.pages.models;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.librecms.contentsection.ContentItem;
+import org.librecms.contentsection.ContentType;
+
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.swing.text.AbstractDocument.Content;
+
+/**
+ * MVC model for retrieving information about the content type of the current
+ * content item. If there is no current content item, the methods of this model
+ * will return an empty string.
+ *
+ * The content type is retrieved once per request cycle on the first call
+ *
+ * @see ContentItemModel
+ * @see ContentItemModel#getContentItem()
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsPagesContentItemTypeModel")
+public class ContentItemTypeModel {
+
+ @Inject
+ private ContentItemModel contentItemModel;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ private Optional contentType;
+
+ public ContentType getContentType() {
+ return getOrRetrieveContentType().orElse(null);
+ }
+
+ public long getContentTypeId() {
+ return getOrRetrieveContentType()
+ .map(ContentType::getObjectId)
+ .orElse(0L);
+ }
+
+ public String getUuid() {
+ return getOrRetrieveContentType()
+ .map(ContentType::getUuid)
+ .orElse("");
+ }
+
+ public String getDisplayName() {
+ return getOrRetrieveContentType()
+ .map(ContentType::getDisplayName)
+ .orElse("");
+ }
+
+ public String getLabel() {
+ return getOrRetrieveContentType()
+ .map(ContentType::getLabel)
+ .map(
+ label -> label.getValue(
+ globalizationHelper.getNegotiatedLocale())
+ )
+ .orElse("");
+ }
+
+ public String getDescription() {
+ return getOrRetrieveContentType()
+ .map(ContentType::getDescription)
+ .map(
+ description -> description.getValue(
+ globalizationHelper.getNegotiatedLocale()
+ )
+ )
+ .orElse("");
+ }
+
+ private Optional getOrRetrieveContentType() {
+ if (contentType == null) {
+ retrieveContentType();
+ }
+ return contentType;
+ }
+
+ private void retrieveContentType() {
+ contentType = Optional
+ .ofNullable(contentItemModel.getContentItem())
+ .map(ContentItem::getContentType);
+
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/SiteInfoModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/SiteInfoModel.java
new file mode 100644
index 000000000..ed3f19a27
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/pages/models/SiteInfoModel.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.librecms.pages.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ * Model providing information about the current site. Must be initalized by the
+ * calling application.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsPagesSiteInfoModel")
+public class SiteInfoModel implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private String host;
+
+ private String domain;
+
+ private String name;
+
+ private List availableLanguages;
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(final String host) {
+ this.host = host;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(final String domain) {
+ this.domain = domain;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public List getAvailableLanguages() {
+ return Collections.unmodifiableList(availableLanguages);
+ }
+
+ public void setAvailableLanguages(final List availableLanguages) {
+ this.availableLanguages = new ArrayList<>(availableLanguages);
+ }
+
+}