diff --git a/content/docs/api/db/obj/contents.lr b/content/docs/api/db/obj/contents.lr
index c41e3d70..b824d3ac 100644
--- a/content/docs/api/db/obj/contents.lr
+++ b/content/docs/api/db/obj/contents.lr
@@ -29,22 +29,35 @@ Most plugins will not have source objects that actually originate on the
file system. This means that their "source" is entirely virtual. Because
this is a very common situation there is a base class, the
`VirtualSourceObject` which plugins can subclass. The constructor takes one
-argument which is the parent source object the virtual source lives below.
+argument which is the source record the virtual source lives below. Virtual
+sources are separated from the records they belong to with an at sign (`@`)
+in the path. The part after the at sign is called the “virtual path”.
+
+For instance in the below example the canonical path for the object would be
+the record's path + `@source`. So if the record was `/hello` then the
+path would be `/hello@source`. The true base record it belongs to can be
+referenced from the [record :ref](record/) property.
```python
from lektor.sourceobj import VirtualSourceObject
+from lektor.utils import build_url
class Source(VirtualSourceObject):
+ @property
+ def path(self):
+ return self.record.path + '@source'
+
@property
def source_content(self):
- with open(self.parent.source_filename) as f:
+ with open(self.record.source_filename) as f:
return f.read().decode('utf-8')
@property
def url_path(self):
- return self.parent.url_path + 'source.txt'
+ return build_url([self.record.url_path, 'source.txt'])
```
For more information see [add-build-program
-:ref](../../environment/add-build-program/).
+:ref](../../environment/add-build-program/) as well as
+[virtualpathresolver :ref](../../environment/virtualpathresolver/).
diff --git a/content/docs/api/db/obj/record/contents.lr b/content/docs/api/db/obj/record/contents.lr
new file mode 100644
index 00000000..22561dd7
--- /dev/null
+++ b/content/docs/api/db/obj/record/contents.lr
@@ -0,0 +1,28 @@
+title: record
+---
+summary: A reference to the associated base record.
+---
+type: property
+---
+version_added: 2.0
+---
+body:
+
+Most records (other than the root) have a [parent :ref](../parent/). This as
+a concept generally makes in most situations. However when virtual sources
+come into play the parent often just refers to another virtual source but not
+the root record they all belong to.
+
+This property for the most part refers back to the object itself. However
+for virtual sources this refers back to the record that was associated with
+the virtual source. Most of the time this matches `parent` but for nested
+source objects it's useful to be able to refer back to the base in all cases
+without having to walk up the parents.
+
+## Example
+
+```html+jinja
+{% if this != this.record %}
+ go back to overview
+{% endif %}
+```
diff --git a/content/docs/api/db/pad/get/contents.lr b/content/docs/api/db/pad/get/contents.lr
index 43eb15e7..c307afcc 100644
--- a/content/docs/api/db/pad/get/contents.lr
+++ b/content/docs/api/db/pad/get/contents.lr
@@ -17,7 +17,8 @@ The path needs to be absolute with folder separated by slashes.
The default behavior is to load the unpaginated version of a record. If you
want to select a specific page for pagination, then you need to pass
-`page_num` with a valid page number.
+`page_num` with a valid page number or you use the virtual path (`@1` for the
+first page for instance).
## Examples
@@ -34,3 +35,15 @@ Here another example that loads the current page but in another language:
{% set other_lang = site.get(this._path, alt='ru') %}
This page in Russian: {{ other_lang.title }}
```
+
+## Virtual Paths
+
+This method can also be used to look up [virtual paths
+:ref](../../../../content/paths/). For instance to fetch a specific version of a
+pagination you can use a virtual path instead of using the `page_num`
+parameter:
+
+```pycon
+>>> pad.get('/blog@3')
+
+```
diff --git a/content/docs/api/db/record/pagination/contents.lr b/content/docs/api/db/record/pagination/contents.lr
index 14542b03..1c8fec77 100644
--- a/content/docs/api/db/record/pagination/contents.lr
+++ b/content/docs/api/db/record/pagination/contents.lr
@@ -29,7 +29,8 @@ The following attributes exist on the pagination object:
| `for_page()` | Returns the record for a different page.
The `for_page()` function accepts a page number and returns the record for
-the other page.
+the other page. For more information also see the virtual path example
+below.
!! *Changed in Lektor 2.0:* The `for_page()` method was added.
@@ -66,3 +67,23 @@ next page link as well as the number of the current page:
{% endif %}
```
+
+## Virtual Paths
+
+!! *New in Lektor 2.0:* virtual paths did not exist in earlier Lektor versions.
+
+The pagination is implemented in a way where each page in the pagination is
+a virtual path below the record itself. The value of the path is just the
+number of the page. So for instance to link to the second page you can just
+do this:
+
+```html+jinja
+Go to Page 2
+```
+
+Alternatively you can also use the `for_path()` function which returns the
+entire pagination for a page:
+
+```html+jinja
+Go to Page 2
+```
diff --git a/content/docs/api/db/record/parent/contents.lr b/content/docs/api/db/record/parent/contents.lr
deleted file mode 100644
index 94567b3c..00000000
--- a/content/docs/api/db/record/parent/contents.lr
+++ /dev/null
@@ -1,16 +0,0 @@
-title: parent
----
-summary: Returns the parent record for a record.
----
-type: property
----
-body:
-
-Because Lektor's database is a tree all records with the exception of the
-root record have a parent. This can be accessed with this property.
-
-## Example
-
-```html+jinja
-Up to {{ this.parent.title }}
-```
diff --git a/content/docs/api/environment/add-build-program/contents.lr b/content/docs/api/environment/add-build-program/contents.lr
index 2257da31..b28c7132 100644
--- a/content/docs/api/environment/add-build-program/contents.lr
+++ b/content/docs/api/environment/add-build-program/contents.lr
@@ -19,17 +19,22 @@ artifacts out of source objects.
```python
from lektor.sourceobj import VirtualSourceObject
from lektor.build_programs import BuildProgram
+from lektor.utils import build_url
class Source(VirtualSourceObject):
+ @property
+ def path(self):
+ return self.record.path + '@source'
+
@property
def source_content(self):
- with open(self.parent.source_filename) as f:
+ with open(self.record.source_filename) as f:
return f.read().decode('utf-8')
@property
def url_path(self):
- return self.parent.url_path + 'source.txt'
+ return build_url([self.record.url_path, 'source.txt'])
class SourceBuildProgram(BuildProgram):
@@ -43,6 +48,11 @@ class SourceBuildProgram(BuildProgram):
this=self.source)
env.add_build_program(Source, SourceBuildProgram)
+
+@env.virtualpathresolver('source')
+def resolve_virtual_path(record, pieces):
+ if not pieces:
+ return Source(record)
```
And here the example `view_source.html` template:
diff --git a/content/docs/api/environment/generator/contents.lr b/content/docs/api/environment/generator/contents.lr
index 5b5ef146..9a0252ce 100644
--- a/content/docs/api/environment/generator/contents.lr
+++ b/content/docs/api/environment/generator/contents.lr
@@ -20,12 +20,17 @@ exists for a page.
```python
from lektor.sourceobj import VirtualSourceObject
from lektor.db import Record
+from lektor.utils import build_url
class Source(VirtualSourceObject):
+ @property
+ def path(self):
+ return self.record.path + '@source'
+
@property
def url_path(self):
- return self.parent.url_path + 'source.txt'
+ return build_url([self.record.url_path, 'source.txt'])
@env.generator
def generate_source_file(node):
diff --git a/content/docs/api/environment/urlresolver/contents.lr b/content/docs/api/environment/urlresolver/contents.lr
index b96ee0af..2ea2f33b 100644
--- a/content/docs/api/environment/urlresolver/contents.lr
+++ b/content/docs/api/environment/urlresolver/contents.lr
@@ -19,12 +19,17 @@ URL path segments.
```python
from lektor.sourceobj import VirtualSourceObject
+from lektor.utils import build_url
class Source(VirtualSourceObject):
+ @property
+ def path(self):
+ return self.record.path + '@source'
+
@property
def url_path(self):
- return self.parent.url_path + 'source.txt'
+ return build_url([self.record.url_path, 'source.txt'])
@env.urlresolver
def match_source_file(node, url_path):
diff --git a/content/docs/api/environment/virtualpathresolver/contents.lr b/content/docs/api/environment/virtualpathresolver/contents.lr
new file mode 100644
index 00000000..b22b250c
--- /dev/null
+++ b/content/docs/api/environment/virtualpathresolver/contents.lr
@@ -0,0 +1,63 @@
+title: virtualpathresolver
+---
+signature: prefix
+---
+summary: Registers a virtual path resolver with the environment.
+---
+type: method
+---
+version_added: 2.0
+---
+body:
+
+When implementing [Virtual Source Objects :ref](../../db/obj/) it's important
+that you can locate them. While virtual sources do not appear as children of
+pages they can be navigated to through the database pad. This is achieved
+through custom virtual path resolvers.
+
+Each source object needs to be a unique prefix which identifies all instances
+of it. For instance if you want to implement a special page (like a feed)
+that would exist for specific pages, then you can register the virtual path
+prefix `feed` with your plugin. Though you should use more descriptive names
+for your plugins as these paths are shared.
+
+If a user then resolves the page `/my-page@feed` it would invoke your URL
+resolver with the record `my-page` and an empty list (rest of the path
+segments). If they would request `/my-page@feed/recent` then it would
+pass `['recent']` as path segments.
+
+## Example
+
+Here an example that would implement a virtual source for feeds and an
+associated virtual path resolver:
+
+```python
+from lektor.sourceobj import VirtualSourceObject
+from lektor.utils import build_url
+
+class Feed(VirtualSourceObject):
+
+ def __init__(self, record, version='default'):
+ VirtualSourceObject.__init__(self, record)
+ self.version = version
+
+ @property
+ def path(self):
+ return '%s@feed%s' % (
+ self.record.path,
+ '/' + self.version if self.version != 'default' else '',
+ )
+
+ @property
+ def url_path(self):
+ return build_url([self.record.url_path, 'feed.xml'])
+
+
+def on_setup_env(self, **extra):
+ @self.env.virtualpathresolver('feed')
+ def resolve_virtual_path(record, pieces):
+ if not pieces:
+ return Feed(record)
+ elif pieces == ['recent']:
+ return Feed(record, version='recent')
+```
diff --git a/content/docs/api/utils/build-url/contents.lr b/content/docs/api/utils/build-url/contents.lr
new file mode 100644
index 00000000..3488fc90
--- /dev/null
+++ b/content/docs/api/utils/build-url/contents.lr
@@ -0,0 +1,32 @@
+title: build_url
+---
+module: lektor.utils
+---
+signature: pieces, trailing_slash=None
+---
+summary: Helper function that assists in building URL paths.
+---
+type: function
+---
+version_added: 2.0
+---
+body:
+
+This function assists in creating URL paths from individual segments. This
+is particularly useful for building virtual source objects. It takes a bunch
+of path segments and returns an absolute path. The default behavior is to
+guess the trailing slash based on the presence of a dot in the last path
+segment. If you want to override the detection you can explicitly pass
+`True` to enforce a trailing slash or `False` to avoid it.
+
+## Example
+
+```pycon
+>>> from lektor.utils import build_url
+>>> build_url(['foo', 42])
+u'/foo/42/'
+>>> build_url(['foo', None, 23])
+u'/foo/23/'
+>>> build_url(['foo', 'hello.html'])
+u'/foo/hello.html'
+```
diff --git a/content/docs/api/utils/join-path/contents.lr b/content/docs/api/utils/join-path/contents.lr
new file mode 100644
index 00000000..b17422f4
--- /dev/null
+++ b/content/docs/api/utils/join-path/contents.lr
@@ -0,0 +1,45 @@
+title: join_path
+---
+module: lektor.utils
+---
+signature: a, b
+---
+summary: Joins two Lektor paths together correctly.
+---
+template_var:
+---
+type: function
+---
+version_added: 2.0
+---
+body:
+
+Given two Lektor paths this joins them together with the rules that Lektor
+set up for this. In particular this is important in the presence of
+virtual paths which have their own join semantics.
+
+## Example
+
+```pycon
+>>> from lektor.utils import join_path
+>>> join_path('/blog', 'hello-world')
+'/blog/hello-world'
+>>> join_path('/blog', '@archive')
+'/blog@archive'
+>>> join_path('/blog@archive', '2015')
+'/blog@archive/2015'
+>>> join_path('/blog@archive/2015', '..')
+'/blog@archive'
+>>> join_path('/blog@archive', '..')
+'/blog'
+```
+
+Special note should be taken for the numeric virtual path of paginations. It
+is considered to be on the same level as the actual record:
+
+```pycon
+>>> join_path('/blog@2', '..')
+'/'
+>>> join_path('/blog@2', '.')
+'/blog@2'
+```
diff --git a/content/docs/api/utils/parse-path/contents.lr b/content/docs/api/utils/parse-path/contents.lr
new file mode 100644
index 00000000..1d69ee2f
--- /dev/null
+++ b/content/docs/api/utils/parse-path/contents.lr
@@ -0,0 +1,25 @@
+title: parse_path
+---
+module: lektor.utils
+---
+summary: Parses a path into components.
+---
+type: function
+---
+version_added: 2.0
+---
+body:
+
+This function parses a path into the individual components it's made from.
+The path is always assumed to be absolute and made absolute if it's not yet
+so. The root path is the empty list.
+
+## Example
+
+```pycon
+>>> from lektor.utils import parse_path
+>>> parse_path('/foo/bar')
+['foo', 'bar']
+>>> parse_path('/')
+[]
+```
diff --git a/content/docs/content/alts/contents.lr b/content/docs/content/alts/contents.lr
index 15933242..ad5bdff0 100644
--- a/content/docs/content/alts/contents.lr
+++ b/content/docs/content/alts/contents.lr
@@ -72,3 +72,11 @@ targeted, different files will be used. This table visualizes this:
| | ✓ | | fr | contents+fr.lr
| | | ✓ | en | contents+en.lr
| | | ✓ | fr | *missing*
+
+## Alternatives and Paths
+
+Alternatives have a special behavior with regards to paths. They alternative
+code does not exist in the path! This can be confusing at first, but has the
+advantage that they automatically work in most places as the paths are the
+same for differnent alternatives. For more information see
+[Alternatives and Paths :ref](../paths/#alternatives-and-paths).
diff --git a/content/docs/content/paths/contents.lr b/content/docs/content/paths/contents.lr
new file mode 100644
index 00000000..92987ddf
--- /dev/null
+++ b/content/docs/content/paths/contents.lr
@@ -0,0 +1,96 @@
+title: Paths
+---
+summary: An explanation about how paths in Lektor work.
+---
+body:
+
+!! This refers mostly to Lektor 2.0. If you are using an older version of
+Lektor the virtual path feature is not implemented yet and a lot of this just
+does not apply.
+
+Lektor attempts to map the paths from the content folder as closely as possible
+to the final URLs (at least in the default configuration). There are various
+ways in which this can be customized (see [URLs and Slugs :ref](../urls/)) and
+there are situations in which Lektor needs to render out content that does not
+actually directly correspond to a path on the file system.
+
+Here we try to explain how the path system in Lektor works and what it means.
+
+## What is a Path
+
+A path in Lektor is a string that uniquely identifies a [Source Object
+:ref](../../api/db/obj/). For the most part these directly point to
+`content.lr` files or attachments therein. These paths always use forward
+slashes, no matter on which platform Lektor runs. So for instance if you
+have a file named `projects/my-project/contents.lr` then the path for this
+record will be `/projects/my-project`. Thereby it does not matter if the
+slug or final URL was changed, the path is always the same.
+
+Lektor uses paths to refer to records by default in all cases. This means that
+if you use the [url filter :ref](../../api/templates/filters/url/) for instance
+it will operate on paths and not URLs!
+
+But what about sources that do not directly correspond to something on the
+file system? This is where virtual paths come in. Virtual paths are
+separated from physical paths with an at (`@`) sign. Virtual paths are always
+attached to a record and point to things that are not actually coming from the
+content folder but something else.
+
+Virtual paths are for instance used to refer to paginated pages or to some
+resources that plugins add.
+
+## Virtual Paths
+
+Virtual paths are added behind physical paths and are separated by an at
+sign (`@`). The only virtual path that is supported by Lektor out of the box
+is the special numeric virtual path which can be used to refer to specific
+pages for a pagination. For instance `/blog@1` refers to the first page of
+the `blog` page, `/blog@2` to the second etc. There are however plugins that
+add virtual paths to refer to their own resources. For instance a plugin can
+register `/blog@feed` to refer to a RSS/Atom feed for the blog.
+
+## Relative Paths
+
+Now that we talked a bit about paths, we should probably cover how relative
+paths work. Relative paths work similar to how you expect them to work on
+most operating systems but they can operate on the virtual as well as the
+physical path. There is also some special behavior with regards to the numeric
+virtual path for pagination.
+
+For the most part `.` refers to the same page and `..` refers to the page one
+level up. If you use a path that just contains of a virtual path, then it's
+attached to the current page to replace the active virtual path.
+
+| Current | Relative | Result
+| --------------- | ------------ | ----------------
+| `/blog` | `..` | `/`
+| `/blog` | `.` | `/blog`
+| `/blog/post` | `..` | `/blog`
+| `/blog` | `@2` | `/blog@2`
+| `/blog@2` | `@1` | `/blog@1`
+| `/blog@feed` | `recent` | `/blog@feed/recent`
+| `/blog@feed` | `..` | `/blog@feed`
+
+However! The numeric path is special with regards because it's considered to
+belong to the current page:
+
+| Current | Relative | Result
+| --------------- | ------------ | ----------------
+| `/blog@2` | `.` | `/blog@2`
+| `/blog@2` | `..` | `/`
+
+## Alternatives and Paths
+
+If you have used Lektor a bit you might be wondering how [Alternatives
+:ref](../alts/) work with paths. The answer might be surprising but it's
+basically that they don't really work with paths. Alternatives are
+implemented on a level higher than paths. If you have a page that exists
+in both German and English alternative then they will have the same path.
+The alternative code is supplied separately.
+
+This is done so that you can later start introducing alternatives to sites
+that were never aware of them without having to go everywhere and start
+passing alternative information around. While there are indeed some places
+where you might have to perform some changes (especially if you perform
+manual queries in the templates) for the most part adding alternatives to an
+existing site later is a trivial matter.
diff --git a/packages/blog-archive/lektor_blog_archive.py b/packages/blog-archive/lektor_blog_archive.py
index 38c06d23..0b00f858 100644
--- a/packages/blog-archive/lektor_blog_archive.py
+++ b/packages/blog-archive/lektor_blog_archive.py
@@ -1,72 +1,45 @@
# -*- coding: utf-8 -*-
import posixpath
from datetime import date
+from itertools import chain
from werkzeug.utils import cached_property
from lektor.pluginsystem import Plugin
from lektor.sourceobj import VirtualSourceObject
from lektor.build_programs import BuildProgram
-from lektor.context import get_ctx
-
-
-def get_path_segments(str):
- pieces = str.split('/')
- if pieces == ['']:
- return []
- return pieces
-
-
-def push_path(pieces, item):
- if item:
- pieces.append(unicode(item))
+from lektor.utils import build_url, parse_path
class BlogArchive(VirtualSourceObject):
- def __init__(self, parent, plugin, items=None, year=None, month=None):
- VirtualSourceObject.__init__(self, parent)
+ def __init__(self, record, plugin, items=None):
+ VirtualSourceObject.__init__(self, record)
self.plugin = plugin
self._items = items
- self.year = year
- self.month = month
@property
- def date(self):
- if self.year is None:
- raise AttributeError()
- return date(self.year, self.month or 1, 1)
+ def path(self):
+ return self.record.path + '@blog-archive'
+
+ template_name = 'blog-archive/index.html'
@property
- def year_archive(self):
- if self.year is None:
- raise AttributeError()
- if self.month is None:
- return self
- return BlogArchive(self.parent, self.plugin, year=self.year)
-
- @property
- def archive_index(self):
- if self.year is None:
- return self
- return BlogArchive(self.parent, self.plugin)
+ def parent(self):
+ return self.record
@cached_property
def year_archives(self):
- if self.year is not None:
- return []
years = set()
- for item in self.parent.children:
+ for item in self.record.children:
pub_date = self.plugin.get_pub_date(item)
if pub_date:
years.add(pub_date.year)
- return [BlogArchive(self.parent, self.plugin,
- year=year) for year in sorted(years)]
+ return [BlogYearArchive(self.record, self.plugin,
+ year=year) for year in sorted(years)]
@property
def items(self):
- if self.year is None:
- return []
if self._items is not None:
return self._items
rv = list(self._iter_items())
@@ -74,13 +47,7 @@ class BlogArchive(VirtualSourceObject):
return rv
def _iter_items(self):
- for item in self.parent.children:
- pub_date = self.plugin.get_pub_date(item)
- if pub_date is None:
- continue
- if pub_date.year == self.year and \
- (self.month is None or pub_date.month == self.month):
- yield item
+ return iter(())
@property
def has_any_items(self):
@@ -97,34 +64,87 @@ class BlogArchive(VirtualSourceObject):
pub_date = self.plugin.get_pub_date(item)
months.setdefault(date(pub_date.year, pub_date.month, 1),
[]).append(item)
- return [(BlogArchive(self.parent, self.plugin,
- year=d.year, month=d.month), i)
+ return [(BlogMonthArchive(self.record, self.plugin,
+ year=d.year, month=d.month), i)
for d, i in sorted(months.items())]
+ def get_archive_url_path(self):
+ return self.plugin.get_url_path('archive_path')
+
@property
def url_path(self):
- prefix = self.parent.url_path.strip('/')
- pieces = []
- if prefix:
- pieces.append(prefix)
- if self.year is None:
- push_path(pieces, self.plugin.get_archive_index_path())
- elif self.month is None:
- push_path(pieces, self.plugin.get_month_archive_prefix())
- push_path(pieces, self.year)
- else:
- push_path(pieces, self.plugin.get_year_archive_prefix())
- push_path(pieces, self.year)
- push_path(pieces, self.month)
- return '/%s/' % '/'.join(pieces)
+ return build_url(chain([self.record.url_path.strip('/')],
+ self.get_archive_url_path() or ()))
+
+
+class BlogYearArchive(BlogArchive):
+ template_name = 'blog-archive/year.html'
+
+ def __init__(self, record, plugin, items=None, year=None):
+ BlogArchive.__init__(self, record, plugin, items)
+ self.year = year
+
+ def _iter_items(self):
+ for item in self.record.children:
+ pub_date = self.plugin.get_pub_date(item)
+ if pub_date is not None and \
+ pub_date.year == self.year:
+ yield item
@property
- def template_name(self):
- if self.year is None:
- return 'blog-archive/index.html'
- if self.month is None:
- return 'blog-archive/year.html'
- return 'blog-archive/month.html'
+ def path(self):
+ return '%s@blog-archive/%s' % (
+ self.record.path,
+ self.year,
+ )
+
+ @property
+ def parent(self):
+ return BlogArchive(self.record, self.plugin)
+
+ @property
+ def date(self):
+ return date(self.year, 1, 1)
+
+ def get_archive_url_path(self):
+ return self.plugin.get_url_path('year_archive_prefix') + [self.year]
+
+
+class BlogMonthArchive(BlogArchive):
+ template_name = 'blog-archive/year.html'
+
+ def __init__(self, record, plugin, items=None, year=None, month=None):
+ BlogArchive.__init__(self, record, plugin, items)
+ self.year = year
+ self.month = month
+
+ def _iter_items(self):
+ for item in self.record.children:
+ pub_date = self.plugin.get_pub_date(item)
+ if pub_date is not None and \
+ pub_date.year == self.year and \
+ pub_date.month == self.month:
+ yield item
+
+ @property
+ def path(self):
+ return '%s@blog-archive/%s/%s' % (
+ self.record.path,
+ self.year,
+ self.month
+ )
+
+ @property
+ def parent(self):
+ return BlogYearArchive(self.record, self.plugin, year=self.year)
+
+ @property
+ def date(self):
+ return date(self.year, self.month, 1)
+
+ def get_archive_url_path(self):
+ return self.plugin.get_url_path('month_archive_prefix') + [
+ self.year, self.month]
class BlogArchiveBuildProgram(BuildProgram):
@@ -150,77 +170,70 @@ class BlogArchivePlugin(Plugin):
def get_blog_path(self):
return self.get_config().get('blog_path', '/blog')
- def get_archive_index_path(self):
- return self.get_config().get('archive_path', 'archive').strip('/')
-
- def get_year_archive_prefix(self):
- return self.get_config().get('year_archive_prefix', 'archive').strip('/')
-
- def get_month_archive_prefix(self):
- return self.get_config().get('month_archive_prefix', 'archive').strip('/')
+ def get_url_path(self, name, default='archive'):
+ return parse_path(self.get_config().get(name, default))
def on_setup_env(self, **extra):
- blog_path = self.get_blog_path()
self.env.add_build_program(BlogArchive, BlogArchiveBuildProgram)
- def get_blog_archive():
- pad = get_ctx().pad
- blog = pad.get(blog_path)
- if blog is not None:
- return BlogArchive(blog, self)
- self.env.jinja_env.globals['get_blog_archive'] = get_blog_archive
+ @self.env.virtualpathresolver('blog-archive')
+ def blog_archive_resolver(node, pieces):
+ if node.path == self.get_blog_path():
+ if not pieces:
+ return BlogArchive(node, self)
+ elif len(pieces) == 1 and pieces[0].isdigit():
+ return BlogYearArchive(node, self, year=int(pieces[0]))
+ elif len(pieces) == 2 and pieces[0].isdigit() \
+ and pieces[1].isdigit():
+ return BlogMonthArchive(node, self, year=int(pieces[0]),
+ month=int(pieces[1]))
@self.env.urlresolver
- def archive_resolver(node, url_path):
- if node.path != blog_path:
+ def archive_urlresolver(node, url_path):
+ if node.path != self.get_blog_path():
return
- archive_index = get_path_segments(self.get_archive_index_path())
+ archive_index = self.get_url_path('archive_path')
if url_path == archive_index:
return BlogArchive(node, self)
- year_prefix = get_path_segments(self.get_year_archive_prefix())
- month_prefix = get_path_segments(self.get_month_archive_prefix())
-
- year = None
- month = None
-
+ year_prefix = self.get_url_path('year_archive_prefix')
if url_path[:len(year_prefix)] == year_prefix and \
url_path[len(year_prefix)].isdigit() and \
len(url_path) == len(year_prefix) + 1:
year = int(url_path[len(year_prefix)])
- elif (url_path[:len(month_prefix)] == month_prefix and
- len(url_path) == len(month_prefix) + 2 and
- url_path[len(month_prefix)].isdigit() and
- url_path[len(month_prefix) + 1].isdigit()):
+ rv = BlogYearArchive(node, self, year=year)
+ if rv.has_any_items:
+ return rv
+
+ month_prefix = self.get_url_path('month_archive_prefix')
+ if url_path[:len(month_prefix)] == month_prefix and \
+ len(url_path) == len(month_prefix) + 2 and \
+ url_path[len(month_prefix)].isdigit() and \
+ url_path[len(month_prefix) + 1].isdigit():
year = int(url_path[len(month_prefix)])
month = int(url_path[len(month_prefix) + 1])
- else:
- return None
-
- rv = BlogArchive(node, self, year=year, month=month)
- if rv.has_any_items:
- return rv
+ rv = BlogMonthArchive(node, self, year=year, month=month)
+ if rv.has_any_items:
+ return rv
@self.env.generator
def genererate_blog_archive_pages(source):
- if source.path != blog_path:
+ if source.path != self.get_blog_path():
return
- blog = source
-
years = {}
months = {}
- for post in blog.children:
+ for post in source.children:
pub_date = self.get_pub_date(post)
if pub_date:
years.setdefault(pub_date.year, []).append(post)
months.setdefault((pub_date.year,
pub_date.month), []).append(post)
- yield BlogArchive(blog, self)
+ yield BlogArchive(source, self)
for year, items in sorted(years.items()):
- yield BlogArchive(blog, self, year=year, items=items)
+ yield BlogYearArchive(source, self, year=year, items=items)
for (year, month), items in sorted(months.items()):
- yield BlogArchive(blog, self, year=year, month=month,
- items=items)
+ yield BlogMonthArchive(source, self, year=year, month=month,
+ items=items)
diff --git a/templates/blog-archive/month.html b/templates/blog-archive/month.html
index 3f4247ec..bd5301f0 100644
--- a/templates/blog-archive/month.html
+++ b/templates/blog-archive/month.html
@@ -3,7 +3,7 @@
{% block blog_body %}
The Transcript, {{ this.date|dateformat('MMMM yyyy') }}
- « Back to {{ this.year }}
+ « Back to {{ this.year }}