Repository: getpelican/pelican

Issue:
{category} in URLs isn't slugified
When trying to save articles using:

```
CATEGORY_SAVE_AS = "{slug}/index.html" 
ARTICLE_SAVE_AS = "{category}/{slug}/index.html"
```

the `{category}` in the second instance isn't slugified, so the URLs don't line up neatly and may end up with spaces or other URL-unfriendly characters in them. Ditto `{author}`.



Discussion:
Actually, I was thinking about this today, and there are some problems with my previous approach.  Slugs for categories may interfere, which is not at all good:

```
>>> from pelican.utils import slugify
>>> slugify("C")
u'c'
>>> slugify("C++")
u'c'
```

So how about this as an alternative solution:

a) url-escape `{category}` and `{author}`, so that the category "C#" at least makes a valid URL ("C%23")
b) add a config option like

```
CATEGORY_SLUGS = {
    'C#': 'csharp',
    'C++', 'cplusplus',
    'Odds and Ends': 'etc'
} 
```

  to let the SEO-concerned make their own damned slugs.  Categories not listed just get url escaped instead.

Hi Nick. Would Andrew's proposed changes in pull request #931 address the issue you raised here?

I considered URL-escaping in my change, but rejected it on the basis that it would make all URLs look unpleasant for the sake of the rare cases where it's required. I figured that a static mapping was a reasonable solution which keeps the common case pleasant at the expense of requiring a little effort from authors.

If this approach proves successful I suppose the defaults could be updated to include common cases like `C++`, but I guess that would need a little care (for example, settings files would probably want to merge new entries into the dictionary as opposed to replacing it entirely).

Hiya Justin & Andrew ... yep, I think that'd be work just fine.

I wrote a very small patch to call slugify() on 'category' and 'author' ... https://github.com/nickzoic/pelican/commit/d25d3503611059b29eb23e704ce506b7734a8f93

Combined with Andrew's patch expanding slugify to respect SLUG_SUBSTITUTIONS, it'd also solve the C vs C++ vs C# problem.

On the issue of default substitutions, I'd be tempted to just chuck the usual suspects (C++, C#) in the default pelicanconf.py file ... they're unlikely enough sequences that non-technical types are unlikely to stumble over them, and anyone smart enough to be blogging about F# or J++ can probably work it out from the examples :-) 

I would ask that there be a similar fix for tags, as they currently suffer from this same issue.

@cdhowie: totally agree, I'd forgotten about tags because I hadn't used them (yet).  I'll add that to my patch, and when I get a moment I'll turn it all into a pull request based on @Cartroo 's SLUG_SUBSTITUTIONS pull request #931 


Resolving patch:
diff --git a/pelican/contents.py b/pelican/contents.py
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -141,14 +141,21 @@ def url_format(self):
         """Returns the URL, formatted with the proper values"""
         metadata = copy.copy(self.metadata)
         path = self.metadata.get('path', self.get_relative_source_path())
+        default_category = self.settings['DEFAULT_CATEGORY']
+        slug_substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())
         metadata.update({
             'path': path_to_url(path),
             'slug': getattr(self, 'slug', ''),
             'lang': getattr(self, 'lang', 'en'),
             'date': getattr(self, 'date', datetime.now()),
-            'author': getattr(self, 'author', ''),
-            'category': getattr(self, 'category',
-                                self.settings['DEFAULT_CATEGORY']),
+            'author': slugify(
+                getattr(self, 'author', ''),
+                slug_substitutions
+            ),
+            'category': slugify(
+                getattr(self, 'category', default_category),
+                slug_substitutions
+            )
         })
         return metadata
 


Test patch:
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -191,6 +191,20 @@ def test_template(self):
         custom_article = Article(**article_kwargs)
         self.assertEqual('custom', custom_article.template)
 
+    def test_slugify_category_author(self):
+        settings = get_settings()
+        settings['SLUG_SUBSTITUTIONS'] = [ ('C#', 'csharp') ]
+        settings['ARTICLE_URL'] = '{author}/{category}/{slug}/'
+        settings['ARTICLE_SAVE_AS'] = '{author}/{category}/{slug}/index.html'
+        article_kwargs = self._copy_page_kwargs()
+        article_kwargs['metadata']['author'] = "O'Brien"
+        article_kwargs['metadata']['category'] = 'C# & stuff'
+        article_kwargs['metadata']['title'] = 'fnord'
+        article_kwargs['settings'] = settings
+        article = Article(**article_kwargs)
+        self.assertEqual(article.url, 'obrien/csharp-stuff/fnord/')
+        self.assertEqual(article.save_as, 'obrien/csharp-stuff/fnord/index.html')
+
 
 class TestURLWrapper(unittest.TestCase):
     def test_comparisons(self):