Skip to content
Permalink
Browse files

Atom syndication and archive support

Paginated current and archived feeds for indexes. New option
GENERATE_ATOM is off by default.
  • Loading branch information...
da2x committed May 2, 2015
1 parent cea211a commit 1fb7a80384f7251451f3ca41d4cd91b969a974a5
@@ -4,6 +4,11 @@ New in master
Features
--------

* New option GENERATE_ATOM, off by default
* Current and archive Atom feeds for indexes and category/tag indexes (RFC-4287 and RFC-5005)
* Atom feed auto-discovery in HTML indexes and category/tag indexes
* .atom included in the sitemap index
* New post metadata "updated", inherits "date" if unset
* Multilingual sitemaps (Issue #1610)
* Compatibility with doit v0.28.0 (Issue #1655)
* AddThis is no longer added by default to users’ sites
@@ -388,7 +388,7 @@ REDIRECTIONS = ${REDIRECTIONS}
# side optimization for very high traffic sites or low memory servers.
# GZIP_FILES = False
# File extensions that will be compressed
# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml')
# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.atom', '.xml')
# Use an external gzip command? None means no.
# Example: GZIP_COMMAND = "pigz -k {filename}"
# GZIP_COMMAND = None
@@ -557,7 +557,7 @@ INDEX_READ_MORE_LINK = ${INDEX_READ_MORE_LINK}
RSS_READ_MORE_LINK = ${RSS_READ_MORE_LINK}

# Append a URL query to the RSS_READ_MORE_LINK and the //rss/item/link in
# RSS feeds. Minimum example for Piwik "pk_campaign=rss" and Google Analytics
# Atom and feeds. Minimum example for Piwik "pk_campaign=rss" and Google Analytics
# "utm_source=rss&utm_medium=rss&utm_campaign=rss". Advanced option used for
# traffic source tracking.
RSS_LINKS_APPEND_QUERY = False
@@ -753,12 +753,21 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# links to it. Set this to False to disable everything RSS-related.
# GENERATE_RSS = True

# By default, Nikola does not generates Atom files for indexes and links to
# them. Generate Atom for tags by setting TAG_PAGES_ARE_INDEXES to True.
# Atom feeds are built based on INDEX_DISPLAY_POST_COUNT and not FEED_LENGHT
# Switch between plain-text summaries and full HTML content using the
# RSS_TEASER option. RSS_LINKS_APPEND_QUERY is also respected. Atom feeds
# are generated even for old indexes and have pagination link relations
# between each other. Old Atom feeds were no changes are marked as archived.
# GENERATE_ATOM = False

# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None,
# the base.tmpl will use the feed Nikola generates. However, you may want to
# change it for a FeedBurner feed or something else.
# RSS_LINK = None

# Show only teasers in the RSS feed? Default to True
# Show only teasers in the RSS and Atom feeds? Default to True
# RSS_TEASERS = True

# Strip HTML in the RSS feed? Default to False
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<title><xsl:value-of select="feed/title"/> (Atom feed)</title>
<style><![CDATA[html{margin:0;pdding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]></style>
</head>
<body>
<h1><xsl:value-of select="feed/title"/> (Atom feed)</h1>
<p>This is an Atom feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader. New to feeds? <a href="https://duckduckgo.com/?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
<p>
<label for="address">Atom feed address:</label>
<input><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="feed/link[@rel='self']/@href"/></xsl:attribute></input>
</p>
<p>Preview of the feed’s current headlines:</p>
<ol>
<xsl:for-each select="feed/entry">
<li><h2><a><xsl:attribute name="href"><xsl:value-of select="link[@rel='alternate']/@href"/></xsl:attribute><xsl:value-of select="title"/></a></h2></li>
</xsl:for-each>
</ol>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
@@ -93,6 +93,15 @@ lang="${lang}">
<link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
%endif
%endif
%if generate_atom:
%if len(translations) > 1:
%for language in translations:
<link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
%endfor
%else:
<link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
%endif
%endif
</%def>

<%def name="html_translations()">
@@ -49,6 +49,7 @@
import dateutil.tz
import logging
import PyRSS2Gen as rss
import lxml.etree
import lxml.html
from yapsy.PluginManager import PluginManager
from blinker import signal
@@ -395,6 +396,7 @@ def __init__(self, **config):
'RSS_LINKS_APPEND_QUERY': False,
'REDIRECTIONS': [],
'ROBOTS_EXCLUSIONS': [],
'GENERATE_ATOM': False,
'GENERATE_RSS': True,
'RSS_LINK': None,
'RSS_PATH': '',
@@ -762,6 +764,7 @@ def __init__(self, **config):
self._GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONFIG_TRANSITION')
self._GLOBAL_CONTEXT['content_footer'] = self.config.get(
'CONTENT_FOOTER')
self._GLOBAL_CONTEXT['generate_atom'] = self.config.get('GENERATE_ATOM')
self._GLOBAL_CONTEXT['generate_rss'] = self.config.get('GENERATE_RSS')
self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
@@ -1556,6 +1559,146 @@ def generic_post_list_renderer(self, lang, posts, output_name,

return utils.apply_filters(task, filters)

def atom_feed_renderer(self, lang, posts, output_path, filters,
extra_context):
"""Renders Atom feeds and archives with lists of posts."""
"""Feeds become archives when no longer expected to change"""

def atom_link(link_rel, link_type, link_href):
link=lxml.etree.Element("link")
link.set("rel", link_rel)
link.set("type", link_type)
link.set("href", link_href)
return link

deps = []
uptodate_deps = []
for post in posts:
deps += post.deps(lang)
uptodate_deps += post.deps_uptodate(lang)
context = {}
context["posts"] = posts
context["title"] = self.config['BLOG_TITLE'](lang)
context["description"] = self.config['BLOG_DESCRIPTION'](lang)
context["lang"] = lang
context["prevlink"] = None
context["nextlink"] = None
context.update(extra_context)
deps_context = copy(context)
deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in
posts]
deps_context["global"] = self.GLOBAL_CONTEXT

for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
deps_context[k] = deps_context['global'][k](lang)

deps_context['navigation_links'] = deps_context['global']['navigation_links'](lang)

nslist={}
if not context["feedpagenum"] == context["feedpagecount"] - 1 and not context["feedpagenum"] == 0:
nslist["fh"]="http://purl.org/syndication/history/1.0"
if not self.config["RSS_TEASERS"]:
nslist["xh"]="http://www.w3.org/1999/xhtml"
feed_xsl_link=self.abs_link("/assets/xml/atom.xsl")
feed_root=lxml.etree.Element("feed", nsmap=nslist)
feed_root.addprevious(lxml.etree.ProcessingInstruction(
"xml-stylesheet",
'href="' + feed_xsl_link + '" type="text/xsl media="all"'))
feed_root.set("{http://www.w3.org/XML/1998/namespace}lang", lang)
feed_root.set("xmlns", "http://www.w3.org/2005/Atom")
feed_title=lxml.etree.SubElement(feed_root, "title")
feed_title.text=context["title"]
feed_id=lxml.etree.SubElement(feed_root, "id")
feed_id.text=self.abs_link(context["feedlink"])
feed_updated=lxml.etree.SubElement(feed_root, "updated")
feed_updated.text=datetime.datetime.now(tz=dateutil.tz.tzutc()).replace(microsecond=0).isoformat()
feed_author=lxml.etree.SubElement(feed_root, "author")
feed_author_name=lxml.etree.SubElement(feed_author, "name")
feed_author_name.text=self.config["BLOG_AUTHOR"](lang)
feed_root.append(atom_link("self", "application/atom+xml",
self.abs_link(context["feedlink"])))
# Older is "next" and newer is "previous" in paginated feeds (opposite of archived)
if "nextfeedlink" in context:
feed_root.append(atom_link("next", "application/atom+xml",
self.abs_link(context["nextfeedlink"])))
if "prevfeedlink" in context:
feed_root.append(atom_link("previous", "application/atom+xml",
self.abs_link(context["prevfeedlink"])))
if not context["feedpagenum"] == 0:
feed_root.append(atom_link("current", "application/atom+xml",
self.abs_link(context["currentfeedlink"])))
# Older is "prev-archive" and newer is "next-archive" in archived feeds (opposite of paginated)
if "prevfeedlink" in context and not context["feedpagenum"] == context["feedpagecount"] - 1:
feed_root.append(atom_link("next-archive", "application/atom+xml",
self.abs_link(context["prevfeedlink"])))
if "nextfeedlink" in context:
feed_root.append(atom_link("prev-archive", "application/atom+xml",
self.abs_link(context["nextfeedlink"])))
if not context["feedpagenum"] == context["feedpagecount"] - 1:
feed_archive=lxml.etree.SubElement(feed_root, "{http://purl.org/syndication/history/1.0}archive")
feed_root.append(atom_link("alternate", "text/html",
self.abs_link(context["permalink"])))
feed_generator=lxml.etree.SubElement(feed_root, "generator")
feed_generator.set("uri", "http://getnikola.com/");
feed_generator.text="Nikola"

for post in posts:
data = post.text(lang, teaser_only=self.config["RSS_TEASERS"], strip_html=self.config["RSS_TEASERS"],
rss_read_more_link=True, rss_links_append_query=self.config["RSS_LINKS_APPEND_QUERY"])
if not self.config["RSS_TEASERS"]:
# FIXME: this is duplicated with code in Post.text() and generic_rss_renderer
try:
doc = lxml.html.document_fromstring(data)
doc.rewrite_links(lambda dst: self.url_replacer(post.permalink(), dst, lang, 'absolute'))
try:
body = doc.body
data = (body.text or '') + ''.join(
[lxml.html.tostring(child, encoding='unicode')
for child in body.iterchildren()])
except IndexError: # No body there, it happens sometimes
data = ''
except lxml.etree.ParserError as e:
if str(e) == "Document is empty":
data = ""
else: # let other errors raise
raise(e)

entry_root=lxml.etree.SubElement(feed_root, "entry")
entry_title=lxml.etree.SubElement(entry_root, "title")
entry_title.text=post.title(lang)
entry_id=lxml.etree.SubElement(entry_root, "id")
entry_id.text=post.permalink(lang, absolute=True)
entry_updated=lxml.etree.SubElement(entry_root, "updated")
entry_updated.text=post.updated.isoformat()
entry_published=lxml.etree.SubElement(entry_root, "published")
entry_published.text=post.date.isoformat()
entry_author=lxml.etree.SubElement(entry_root, "author")
entry_author_name=lxml.etree.SubElement(entry_author, "name")
entry_author_name.text=post.author(lang)
entry_root.append(atom_link("alternate", "text/html",
post.permalink(lang, absolute=True,
query=self.config["RSS_LINKS_APPEND_QUERY"])))
if self.config["RSS_TEASERS"]:
entry_summary=lxml.etree.SubElement(entry_root, "summary")
entry_summary.text=data
else:
entry_content=lxml.etree.SubElement(entry_root, "content")
entry_content.set("type", "xhtml")
entry_content_nsdiv=lxml.etree.SubElement(entry_content, "{http://www.w3.org/1999/xhtml}div")
entry_content_nsdiv.text=data
for category in post.tags:
entry_category=lxml.etree.SubElement(entry_root, "category")
entry_category.set("term", utils.slugify(category))
entry_category.set("label", category)

dst_dir = os.path.dirname(output_path)
utils.makedirs(dst_dir)
with io.open(output_path, "w+", encoding="utf-8") as atom_file:
data = lxml.etree.tostring(feed_root.getroottree(), encoding="UTF-8", pretty_print=True, xml_declaration=True)
if isinstance(data, utils.bytes_str):
data = data.decode('utf-8')
atom_file.write(data)

def generic_index_renderer(self, lang, posts, indexes_title, template_name, context_source, kw, basename, page_link, page_path, additional_dependencies=[]):
"""Creates an index page.
@@ -1592,6 +1735,10 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont
kw["indexes_static"] = self.config['INDEXES_STATIC']
kw['indexes_prety_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"]
kw['demote_headers'] = self.config['DEMOTE_HEADERS']
kw['generate_atom'] = self.config["GENERATE_ATOM"]
kw['feed_link_append_query'] = self.config["RSS_LINKS_APPEND_QUERY"]
kw['feed_teasers'] = self.config["RSS_TEASERS"]
kw['currentfeed'] = None

# Split in smaller lists
lists = []
@@ -1644,9 +1791,19 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont
if i < num_pages - 1:
nextlink = i + 1
if prevlink is not None:
context["prevlink"] = page_link(prevlink, utils.get_displayed_page_number(prevlink, num_pages, self), num_pages, False)
context["prevlink"] = page_link(prevlink,
utils.get_displayed_page_number(prevlink, num_pages, self),
num_pages, False)
context["prevfeedlink"] = page_link(prevlink,
utils.get_displayed_page_number(prevlink, num_pages, self),
num_pages, False, extension=".atom")
if nextlink is not None:
context["nextlink"] = page_link(nextlink, utils.get_displayed_page_number(nextlink, num_pages, self), num_pages, False)
context["nextlink"] = page_link(nextlink,
utils.get_displayed_page_number(nextlink, num_pages, self),
num_pages, False)
context["nextfeedlink"] = page_link(nextlink,
utils.get_displayed_page_number(nextlink, num_pages, self),
num_pages, False, extension=".atom")
context["permalink"] = page_link(i, ipages_i, num_pages, False)
output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False))
task = self.generic_post_list_renderer(
@@ -1661,6 +1818,30 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont
task['basename'] = basename
yield task

if kw['generate_atom']:
atom_output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False, extension=".atom"))
context["feedlink"] = page_link(i, ipages_i, num_pages, False, extension=".atom")
if not kw["currentfeed"]:
kw["currentfeed"] = context["feedlink"]
context["currentfeedlink"] = kw["currentfeed"]
context["feedpagenum"] = i
context["feedpagecount"] = num_pages
atom_task = {
"basename": basename,
"file_dep": [output_name],
"name": atom_output_name,
"targets": [atom_output_name],
"actions": [(self.atom_feed_renderer,
(lang,
post_list,
atom_output_name,
kw['filters'],
context,))],
"clean": True,
"uptodate": [utils.config_changed(kw, 'nikola.nikola.Nikola.atom_feed_renderer')] + additional_dependencies
}
yield utils.apply_filters(atom_task, kw['filters'])

if kw["indexes_pages_main"] and kw['indexes_prety_page_url'](lang):
# create redirection
output_name = os.path.join(kw['output_folder'], page_path(0, utils.get_displayed_page_number(0, num_pages, self), num_pages, True))
Oops, something went wrong.

0 comments on commit 1fb7a80

Please sign in to comment.
You can’t perform that action at this time.