Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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 1fb7a80
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 35 deletions.
5 changes: 5 additions & 0 deletions CHANGES.txt
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions nikola/conf.py.in
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions nikola/data/themes/base/assets/xml/atom.xsl
@@ -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>
9 changes: 9 additions & 0 deletions nikola/data/themes/base/templates/base_helper.tmpl
Expand Up @@ -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()">
Expand Down
185 changes: 183 additions & 2 deletions nikola/nikola.py
Expand Up @@ -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
Expand Down Expand Up @@ -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': '',
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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(
Expand All @@ -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))
Expand Down

0 comments on commit 1fb7a80

Please sign in to comment.