Skip to content

Instantly share code, notes, and snippets.

@aertmann
Last active December 8, 2017 15:43
Show Gist options
  • Save aertmann/0d280c31f787f889eab7 to your computer and use it in GitHub Desktop.
Save aertmann/0d280c31f787f889eab7 to your computer and use it in GitHub Desktop.
News with inline editable headline & lead & image, list view with ajax loading, overview, single view, tagging and RSS using News document node type and Elasticsearch for Neos – Included in https://speakerdeck.com/aertmann/tasty-recipes-for-every-day-neos
{namespace neos=TYPO3\Neos\ViewHelpers}
{namespace media=TYPO3\Media\ViewHelpers}
<f:layout name="Page" />
<f:section name="body">
<article itemscope="" itemtype="http://schema.org/Article" xmlns:f="http://www.w3.org/1999/html">
<header>
<f:if condition="{tags}">
<f:for each="{tags}" as="articleTag" iteration="iterator">
<f:if condition="{iterator.isFirst}">
<span class="tag">{articleTag.properties.title}</span>
</f:if>
</f:for>
</f:if>
<neos:contentElement.wrap node="{node}">
<neos:contentElement.editable property="title" tag="h1" />
</neos:contentElement.wrap>
<f:if condition="{author}">
<span itemprop="author" itemscope="" itemtype="http://schema.org/Person">By <span itemprop="name">{author}</span></span>
</f:if>
<neos:contentElement.wrap node="{image}">
<figure>
<f:if condition="{image.properties.image}">
<f:then>
<f:alias map="{imageUrl: '{media:uri.image(asset: image.properties.image, maximumWidth: 720, maximumHeight: 500, allowCropping: 1, allowUpScaling: 1)}'}">
<meta itemprop="image" content="{imageUrl}" />
<img src="{imageUrl}" alt="{image.properties.alternativeText}" title="{image.properties.title}" />
</f:alias>
<f:if condition="{image.properties.hasCaption}">
<figcaption>
<neos:contentElement.editable property="caption" node="{image}" />
</figcaption>
</f:if>
</f:then>
<f:else>
<f:security.ifAccess resource="TYPO3_Neos_Backend_GeneralAccess">
<f:if condition="{node.context.workspace.name} != 'live'">
<img src="{f:uri.resource(package: 'TYPO3.Neos', path: 'Images/dummy-image.svg')}" title="Dummy image" alt="Dummy image" />
<figcaption>
<neos:contentElement.editable property="caption" node="{image}" />
</figcaption>
</f:if>
</f:security.ifAccess>
</f:else>
</f:if>
</figure>
</neos:contentElement.wrap>
<neos:contentElement.wrap node="{node}">
<neos:contentElement.editable property="lead" tag="p" class="lead" />
</neos:contentElement.wrap>
<div class="byline">
<time datetime="{f:format.date(date: node.properties.datePublished, format: 'c')}" itemprop="datePublished"><f:format.date format="d/m-Y">{node.properties.datePublished}</f:format.date></time>
</div>
</header>
<div itemprop="articleBody">
{content.main -> f:format.raw()}
</div>
</article>
</f:section>
prototype(Acme.News:Article) >
prototype(Acme.News:Article) < prototype(Page) {
head.head {
openGraphImage = ${q(node).children('image').property('image')}
article = true
datePublished = ${q(node).property('datePublished')}
tags = ${q(node).property('tags')}
description = ${q(node).property('lead')}
}
body {
node = ${node}
title = ${q(node).property('title')}
datePublished = ${q(node).property('datePublished')}
lead = ${q(node).property('lead')}
image = ${q(node).children('image').get(0)}
caption = ${q(node).children('image').property('caption')}
alternativeText = ${q(node).property('alternativeText')}
author = ${q(node).property('author')}
tags = ${q(node).property('tags')}
tagSearchFilter = TYPO3.TypoScript:RawArray {
tags = ${Indexing.convertArrayOfNodesToArrayOfNodeIdentifiers(q(node).property('tags'))}
tags.@if.notEmpty = ${q(node).property('tags')}
}
# Can't use __identifier as a key name in TypoScript so we had to come up with this
identifierSearchFilter = Acme.News:RawArray {
key = '__identifier'
value = TYPO3.TypoScript:RawArray {
0 = ${node.nodeData.identifier}
}
}
searchQuery = ${Search.query(site).nodeType('Acme.News:Article')}
filteredSearchQuery = ${this.tagSearchFilter.tags != null ? this.searchQuery.queryFilter('terms', this.tagSearchFilter) : this.searchQuery}
@override.searchResults = ${this.filteredSearchQuery.queryFilter('terms', this.identifierSearchFilter, 'must_not').sortDesc('datePublished').limit(5).execute().nodes}
searchResults = ${searchResults}
hasRelated = ${this.searchResults != null && Array.length(this.searchResults) > 0}
relatedArticles = TYPO3.TypoScript:Collection {
collection = ${searchResults}
itemRenderer = 'Acme.News:RelatedArticle'
itemName = 'article'
prototype(TYPO3.Neos:Content) {
@process.contentElementWrapping >
}
}
}
}
{namespace neos=TYPO3\Neos\ViewHelpers}
<section{attributes -> f:format.raw()}>
<f:if condition="{node.properties.header}">
<h1 class="news-list-header">{node.properties.header -> f:format.raw()}</h1>
</f:if>
<f:render partial="NewsListArticles" arguments="{_all}" />
<f:if condition="{canLoadMore}">
<a href="?offset={nextOffset}" class="view-more-link">Load more</a>
</f:if>
</section>
prototype(Acme.News:List) {
attributes.class = 'news-list'
limit = 10
count = ${Search.query(site).nodeType('Acme.News:Article').count()}
articles = ${Search.query(site).nodeType('Acme.News:Article').sortDesc('datePublished').limit(this.limit).execute().nodes}
nextOffset = ${this.limit}
canLoadMore = ${this.nextOffset < this.count}
@cache {
mode = 'cached'
entryIdentifier {
node = ${node}
}
entryTags {
1 = ${'NodeType_Acme.News:Article'}
2 = ${'Node_' + node.identifier}
}
maximumLifetime = 3600
}
}
<f:if condition="{nextOffset} <= {count}">
<f:render partial="NewsListArticles" arguments="{_all}" />
<f:if condition="{nextOffset} < {count}">
<a href="?offset={nextOffset}" class="view-more-link">Load more</a>
</f:if>
</f:if>
listAjax = TYPO3.TypoScript:Template {
templatePath = 'resource://Acme.News/Private/Templates/NodeTypes/ListAjax.html'
count = ${Search.query(site).nodeType('Acme.News:Article').count()}
limit = 10
offset = ${String.toInteger(request.arguments.offset)}
nextOffset = ${this.limit + this.offset}
canLoadMore = ${this.nextOffset < this.count}
articles = ${Search.query(site).nodeType('Acme.News:Article').sortDesc('datePublished').limit(this.limit).from(this.offset).execute().nodes}
@cache {
mode = 'cached'
entryIdentifier {
identifier = 'listAjax'
offset = ${'offset' + request.arguments.offset}
}
entryTags {
1 = ${'NodeType_Acme.News:Article'}
}
maximumLifetime = 3600
}
}
{namespace neos=TYPO3\Neos\ViewHelpers}
<f:if condition="{f:count(subject: articles)} > 0">
<f:then>
<div class="articles">
<f:if condition="{offset}">
<f:else>
<a class="rss-link" href="{neos:uri.node(node: '~', format: 'xml')}">
<img src="{f:uri.resource(path: 'Images/rss.png', package: 'Acme.News')}" />
</a>
</f:else>
</f:if>
<f:for each="{articles}" as="article">
<article>
<div class="byline">
<time datetime="{f:format.date(date: node.properties.datePublished, format: 'c')}" itemprop="datePublished"><f:format.date format="d.m.Y">{article.node.properties.datePublished}</f:format.date></time>
</div>
<header>
<h3>
<f:format.stripTags>{article.node.properties.title}</f:format.stripTags>
</h3>
</header>
<f:if condition="{article.node.properties.lead}">
<summary>
<p class="sub-header">{article.node.properties.lead -> f:format.raw()}&hellip; <neos:link.node node="{article.node}">Read more</neos:link.node></p>
</summary>
</f:if>
</article>
</f:for>
</div>
</f:then>
<f:else>
<p>No articles were found</p>
</f:else>
</f:if>
'Acme.News:Article':
superTypes: ['TYPO3.Neos.NodeTypes:Page']
ui:
label: 'Article'
icon: 'icon-file-text'
inspector:
groups:
options:
label: 'Options'
position: 1
properties:
_hiddenInIndex:
defaultValue: TRUE
datePublished:
type: date
defaultValue: 'now'
ui:
label: 'Publication Date'
reloadIfChanged: true
inspector:
group: 'options'
editorOptions:
format: 'd-m-Y'
tags:
type: 'references'
ui:
label: 'Tags'
reloadIfChanged: TRUE
inspector:
group: 'options'
editorOptions:
nodeTypes: ['Acme.News:Tag']
author:
type: 'string'
ui:
label: 'Author'
reloadIfChanged: TRUE
inspector:
group: 'options'
editorOptions:
placeholder: 'Author'
title:
type: string
defaultValue: ''
ui:
inlineEditable: TRUE
inspector:
group: ~
aloha:
placeholder: 'Headline'
'format':
'b': FALSE
'i': FALSE
'u': FALSE
'sub': FALSE
'sup': FALSE
'p': FALSE
'h1': TRUE
'h2': TRUE
'h3': FALSE
'pre': FALSE
'removeFormat': TRUE
'table':
'table': FALSE
'list':
'ol': FALSE
'ul': FALSE
'link':
'a': TRUE
lead:
type: string
ui:
label: 'Lead'
inlineEditable: TRUE
aloha:
placeholder: 'Lead'
'format':
'b': FALSE
'i': FALSE
'u': FALSE
'sub': FALSE
'sup': FALSE
'p': FALSE
'h1': TRUE
'h2': TRUE
'h3': FALSE
'pre': FALSE
'removeFormat': TRUE
'table':
'table': FALSE
'list':
'ol': FALSE
'ul': FALSE
'link':
'a': TRUE
'Acme.News:List':
superTypes: ['TYPO3.Neos:Content']
ui:
label: 'News article list'
icon: 'icon-list'
group: 'plugins'
inspector:
groups:
options:
label: 'Settings'
position: 40
properties:
tags:
type: 'references'
ui:
label: 'Tags'
reloadIfChanged: TRUE
inspector:
group: 'options'
editorOptions:
nodeTypes: ['Acme.News:Tag']
header:
type: string
defaultValue: 'News'
ui:
label: 'Header'
inlineEditable: TRUE
aloha:
'format':
'b': FALSE
'i': FALSE
'u': FALSE
'sub': FALSE
'sup': FALSE
'p': FALSE
'h1': FALSE
'h2': TRUE
'h3': FALSE
'pre': FALSE
'removeFormat': TRUE
'table':
'table': FALSE
'list':
'ol': FALSE
'ul': FALSE
'link':
'a': FALSE
'Acme.News:Overview':
superTypes: ['TYPO3.Neos:Content']
ui:
label: 'News overview'
icon: 'icon-list'
group: 'plugins'
inspector:
groups:
options:
label: 'Settings'
position: 30
properties:
header:
type: string
defaultValue: 'News'
ui:
label: 'Header'
inlineEditable: TRUE
aloha:
'format':
'b': FALSE
'i': FALSE
'u': FALSE
'sub': FALSE
'sup': FALSE
'p': FALSE
'h1': FALSE
'h2': TRUE
'h3': FALSE
'pre': FALSE
'removeFormat': TRUE
'table':
'table': FALSE
'list':
'ol': FALSE
'ul': FALSE
'link':
'a': FALSE
numberOfArticles:
type: integer
defaultValue: 10
ui:
label: 'Number of articles'
reloadIfChanged: TRUE
inspector:
group: 'options'
linkToList:
type: reference
ui:
label: 'Link to list page'
reloadIfChanged: TRUE
inspector:
group: 'options'
editorOptions:
nodeTypes:
- 'TYPO3.Neos.NodeTypes:Page'
'Acme.News:Tag':
superTypes: ['TYPO3.Neos:Document']
ui:
label: 'Tag'
icon: 'icon-tag'
properties:
_hiddenInIndex:
defaultValue: TRUE
{namespace neos=TYPO3\Neos\ViewHelpers}
<section{attributes -> f:format.raw()}>
<f:security.ifAccess resource="TYPO3_Neos_Backend_GeneralAccess">
<f:then>
<neos:contentElement.editable property="header" tag="h2" />
</f:then>
<f:else>
<f:if condition="{node.properties.header}">
<h2>{node.properties.header}</h2>
</f:if>
</f:else>
</f:security.ifAccess>
<f:if condition="{f:count(subject: articles)} > 0">
<f:then>
<ul class="articles">
<f:for each="{articles}" as="article">
<li>
<h6><neos:link.node node="{article.node}"><f:format.stripTags>{article.node.properties.title -> f:format.raw()}</f:format.stripTags></neos:link.node></h6>
<f:if condition="{article.node.properties.lead}">
<p class="small">{article.node.properties.lead -> f:format.raw()}</p>
</f:if>
</li>
</f:for>
</ul>
</f:then>
<f:else>
<p>No articles were found</p>
</f:else>
</f:if>
<f:if condition="{node.properties.linkToList}">
<div class="news-link-to-list">
<neos:link.node node="{node.properties.linkToList}">See all articles</neos:link.node>
</div>
</f:if>
</section>
prototype(Acme.News:Overview) {
attributes.class = 'news-overview'
articles = ${Search.query(site).nodeType('Acme.News:Article').sortDesc('datePublished').limit(q(node).property('numberOfArticles')).execute().nodes}
filteredSearchQuery = ${this.tagSearchFilter.tags != null ? this.searchQuery.queryFilter('terms', this.tagSearchFilter) : this.searchQuery}
@cache {
mode = 'cached'
entryIdentifier {
node = ${node}
}
entryTags {
1 = ${'NodeType_Acme.News:Article'}
2 = ${'Node_' + node.identifier}
}
maximumLifetime = 3600
}
}
root {
news {
@position = 'before default'
condition = ${q(node).is('[instanceof Acme.News:Article]')}
type = 'Acme.News:Article'
}
rss {
@position = 'before format'
condition = ${request.parentRequest.uri.path == '/rss.xml'}
renderPath = '/rss'
}
listAjax {
@position = 'before format'
condition = ${request.arguments.offset != null && request.arguments.offset != ''}
renderPath = '/listAjax'
}
@cache {
entryIdentifier {
path = ${request.parentRequest.uri.path}
offset = ${'offset' + request.arguments.offset}
}
}
}
-
name: 'RSS Feed'
uriPattern: '{node}rss.xml'
defaults:
'@package': 'TYPO3.Neos'
'@controller': 'Frontend\Node'
'@action': 'show'
'@format': 'xml'
routeParts:
'node':
handler: 'TYPO3\Neos\Routing\FrontendNodeRoutePartHandlerInterface'
options:
onlyMatchSiteNodes: TRUE
rss = TYPO3.TypoScript:Http.Message {
feed = TYPO3.TypoScript:Template {
templatePath = 'resource://Acme.News/Private/Templates/NodeTypes/Rss.xml'
articles = ${Search.query(site).nodeType('Acme.News:Article').sortDesc('datePublished').limit(100).execute().nodes}
site = ${site}
@cache {
mode = 'cached'
entryIdentifier {
identifier = 'rss'
}
entryTags {
1 = ${'NodeType_Acme.News:Article'}
}
maximumLifetime = 3600
}
}
httpResponseHead.headers.Content-Type = 'application/xml'
}
{namespace neos=TYPO3\Neos\ViewHelpers}<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>{siteRoot.label} - Nyheder</title>
<link><neos:uri.node node="{site}" format="html" absolute="1" /></link>
<description>Latest news</description>
<language>da</language>
<lastBuildDate>{f:format.date(date: 'now', format: 'D, d M Y H:i:s O')}</lastBuildDate>
<f:if condition="{f:count(subject: articles)} > 0"><f:for each="{articles}" as="article"><item>
<title>{article.node.properties.title -> f:format.stripTags()}</title>
<link><neos:uri.node node="{article.node}" format="html" absolute="1" /></link>
<f:if condition="{article.node.properties.tags -> f:count()} > 0"><category><f:for each="{article.node.properties.tags}" as="tag" iteration="iterator"><f:if condition="{iterator.isFirst}">{tag.properties.title}</f:if></f:for></category></f:if>
<description><f:format.stripTags value="{article.node.properties.lead}" /></description>
<pubDate>{f:format.date(date: article.node.properties.datePublished, format: 'D, d M Y H:i:s O')}</pubDate>
</item></f:for></f:if>
</channel>
</rss>
<f:if condition="{node.context.workspace.name} != 'live'">
<f:then>
<neos:contentElement.wrap node="{node}">
<div>
<neos:contentElement.editable property="title" tag="h1" />
</div>
</neos:contentElement.wrap>
</f:then>
<f:else>
<h1>{title}</h1>
</f:else>
</f:if>
prototype(Acme.News:Tag) >
prototype(Acme.News:Tag) < prototype(Page) {
body {
node = ${node}
title = ${q(node).property('title')}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment