Repository: sunpy/ablog

Issue:
Atom feeds fail W3C validation
### Atom feeds fail W3C validation

The generated Atom feeds don't pass W3C validation

### Expected vs Actual behavior

Expected behavior: Generated Atom feeds pass W3C valdation
Actual behavior: Generated Atom feeds fail W3C valdation

```
This feed does not validate.

line 9, column 2: Duplicate alternate links with the same type and hreflang [help]

  <entry>
  ^
In addition, interoperability with the widest range of feed readers could be improved by implementing the following recommendation.

line 2, column 0: Missing atom:link with rel="self" [help]

<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
```

Also, the `blog_baseurl` value needs to have a trailing slash or the validator produces a 3rd error:

```
In addition, interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

line 3, column 32: Identifier "https://www.rpatterson.net" is not in canonical form (the canonical form would be "https://www.rpatterson.net/") [help]

  <id>https://www.rpatterson.net</id>
```

I greppd that adding the trailing slash doesn't result in double slashes in the generated site.  So the requirement for a trailing slash should be documented or better yet should be added when the site is built/generated.

### Steps to Reproduce

1. Create an Atom blog
2. Publish where publicly available
3. [Validate an Atom feed](https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Fwww.rpatterson.net%2Fblog%2Fatom.xml)

### System Details

```
$ ./.venv/bin/python --version
Python 3.8.6
$ ./.venv/bin/pip freeze
ablog==0.10.13
alabaster==0.7.12
Babel==2.9.0
certifi==2020.12.5
chardet==4.0.0
docutils==0.16
feedgen==0.9.0
idna==2.10
imagesize==1.2.0
invoke==1.5.0
Jinja2==2.11.3
lxml==4.6.2
MarkupSafe==1.1.1
packaging==20.9
Pygments==2.8.1
pygments-solarized==0.0.3
pyparsing==2.4.7
python-dateutil==2.8.1
pytz==2021.1
requests==2.25.1
six==1.15.0
snowballstemmer==2.1.0
Sphinx==3.5.2
sphinx-fasvg==0.1.4
sphinx-nervproject-theme==2.0.3
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
urllib3==1.26.4
watchdog==2.0.2
$ uname -a
Linux rpatterson.rpatterson.net 5.8.0-7642-generic #47~1614007149~20.10~82fb226-Ubuntu SMP Tue Feb 23 02
:59:01 UTC  x86_64 x86_64 x86_64 GNU/Linux                                                             
```

Discussion:
nabobalis: Thanks for your continued effort on this! 
nabobalis: I ran the RTD PR atom.xml through the validator: https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Fablog--97.org.readthedocs.build%2Fblog%2Fatom.xml 

```
Congratulations!
[Valid Atom 1.0] This is a valid Atom 1.0 feed.

Recommendations
This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

line 7, column 71: Self reference doesn't match document location [help]

  <link href="https://ablog.readthedocs.org/blog/atom.xml" rel="self"/>
                                                                       ^
line 106, column 38: Two entries with the same value for atom:updated: 2014-08-31T00:00:00+00:00 (8 occurrences) [help]

    <updated>2014-08-31T00:00:00+00:00</updated>
```                                      ^

The same entry issue I assume is an issue with the docs and their metadata.   
But I am curious about the first one.
rpatterson: > I ran the RTD PR atom.xml through the validator: https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Fablog--97.org.readthedocs.build%2Fblog%2Fatom.xml
> Should `rel` be something else? The message doesn't make much sense to me.

The `<link .../>` element with the `self` relationship should have the same `href` as the URL from which the feed was requested.  In this case you're requesting it from `https://ablog--97.org.readthedocs.build/blog/atom.xml` but the ABlog project from which the feed was built was configured with a different `blog_baseurl` resulting in a different URL in that `<link .../>`: `https://ablog.readthedocs.org/blog/atom.xml`.  For example, [my site's feed passes without that warning](https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Fwww.rpatterson.net%2Fblog%2Fatom.xml).  If you want that feed to pass from that particular URL, the project that generates it should be changed such that `blog_baseurl = "https://ablog--97.org.readthedocs.build/"`.
nabobalis: Thanks for the clarification. 
nabobalis: Thanks for the additions @rpatterson! 

Patch:
diff --git a/ablog/post.py b/ablog/post.py
index b6be6c8..e08dacc 100644
--- a/ablog/post.py
+++ b/ablog/post.py
@@ -709,7 +709,7 @@ def generate_atom_feeds(app):
         feed.title(feed_title)
         feed.link(href=url)
         feed.subtitle(blog.blog_feed_subtitle)
-        feed.link(href=feed_url)
+        feed.link(href=feed_url, rel="self")
         feed.language(app.config.language)
         feed.generator("ABlog", ablog.__version__, "https://ablog.readthedocs.org/")
 
@@ -741,17 +741,18 @@ def generate_atom_feeds(app):
 
             # Entry values that support templates
             title = post.title
-            if post.excerpt:
-                summary = " ".join(paragraph.astext() for paragraph in post.excerpt[0])
-            else:
-                summary = ""
+            summary = "".join(paragraph.astext() for paragraph in post.excerpt)
             template_values = {}
             for element in ("title", "summary", "content"):
                 if element in feed_templates:
                     template_values[element] = jinja2.Template(feed_templates[element]).render(**locals())
             feed_entry.title(template_values.get("title", title))
-            feed_entry.summary(template_values.get("summary", summary))
-            feed_entry.content(content=template_values.get("content", content), type="html")
+            summary = template_values.get("summary", summary)
+            if summary:
+                feed_entry.summary(summary)
+            content = template_values.get("content", content)
+            if content:
+                feed_entry.content(content=content, type="html")
 
         parent_dir = os.path.dirname(feed_path)
         if not os.path.isdir(parent_dir):


Test patch:
diff --git a/tests/roots/test-build/foo-empty-post.rst b/tests/roots/test-build/foo-empty-post.rst
new file mode 100644
index 0000000..e2221df
--- /dev/null
+++ b/tests/roots/test-build/foo-empty-post.rst
@@ -0,0 +1,5 @@
+.. post:: 2021-03-23
+
+##############
+Foo Empty Post
+##############
diff --git a/tests/roots/test-build/post.rst b/tests/roots/test-build/post.rst
index d8f1d1e..bef264d 100644
--- a/tests/roots/test-build/post.rst
+++ b/tests/roots/test-build/post.rst
@@ -4,6 +4,8 @@
 Foo Post Title
 ==============
 
-    Foo post description.
+    Foo post description `with link`_.
 
 Foo post content.
+
+.. _`with link`: https://example.com
diff --git a/tests/test_build.py b/tests/test_build.py
index ff4211b..10c077b 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -26,13 +26,13 @@ def test_feed(app, status, warning):
     with feed_path.open() as feed_opened:
         feed_tree = lxml.etree.parse(feed_opened)
     entries = feed_tree.findall("{http://www.w3.org/2005/Atom}entry")
-    assert len(entries) == 1, "Wrong number of Atom feed entries"
+    assert len(entries) == 2, "Wrong number of Atom feed entries"
 
     entry = entries[0]
     title = entry.find("{http://www.w3.org/2005/Atom}title")
     assert title.text == "Foo Post Title", "Wrong Atom feed entry title"
     summary = entry.find("{http://www.w3.org/2005/Atom}summary")
-    assert summary.text == "Foo post description.", "Wrong Atom feed entry summary"
+    assert summary.text == "Foo post description with link.", "Wrong Atom feed entry summary"
     categories = entry.findall("{http://www.w3.org/2005/Atom}category")
     assert len(categories) == 2, "Wrong number of Atom feed categories"
     assert categories[0].attrib["label"] == "Foo Tag", "Wrong Atom feed first category"
@@ -42,6 +42,16 @@ def test_feed(app, status, warning):
     content = entry.find("{http://www.w3.org/2005/Atom}content")
     assert "Foo post content." in content.text, "Wrong Atom feed entry content"
 
+    empty_entry = entries[1]
+    title = empty_entry.find("{http://www.w3.org/2005/Atom}title")
+    assert title.text == "Foo Empty Post", "Wrong Atom feed empty entry title"
+    summary = empty_entry.find("{http://www.w3.org/2005/Atom}summary")
+    assert summary is None, "Atom feed empty entry contains optional summary element"
+    categories = empty_entry.findall("{http://www.w3.org/2005/Atom}category")
+    assert len(categories) == 0, "Atom categories rendered for empty post"
+    content = empty_entry.find("{http://www.w3.org/2005/Atom}content")
+    assert 'id="foo-empty-post"' in content.text, "Atom feed empty entry missing post ID"
+
     social_path = app.outdir / "blog/social.xml"
     assert (social_path).exists(), "Social media feed was not built"
 
@@ -54,7 +64,7 @@ def test_feed(app, status, warning):
     title = social_entry.find("{http://www.w3.org/2005/Atom}title")
     assert title.text == "Foo Post Title", "Wrong Social media feed entry title"
     summary = social_entry.find("{http://www.w3.org/2005/Atom}summary")
-    assert summary.text == "Foo post description.", "Wrong Social media feed entry summary"
+    assert summary.text == "Foo post description with link.", "Wrong Social media feed entry summary"
     categories = social_entry.findall("{http://www.w3.org/2005/Atom}category")
     assert len(categories) == 2, "Wrong number of Social media feed categories"
     assert categories[0].attrib["label"] == "Foo Tag", "Wrong Social media feed first category"