diff --git a/docs/index.rst b/docs/index.rst
index 5a14db6..6794982 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,31 +1,30 @@
 .. _swh-lister:
 
 Software Heritage - Listers
 ===========================
 
 Collection of listers for source code distribution places like development
 forges, FOSS distributions, package managers, etc. Each lister is in charge to
 enumerate the software origins (e.g., VCS, packages, etc.) available at a
 source code distribution place.
 
 
 Overview
 --------
 
 .. toctree::
    :maxdepth: 2
-   :caption: Overview:
    :titlesonly:
 
 
-   lister-tutorial
-   run-lister-tutorial
+   tutorial
+   run_a_new_lister
 
 
 Reference Documentation
 -----------------------
 
 .. toctree::
    :maxdepth: 2
 
    /apidoc/swh.lister
diff --git a/swh/lister/cgit/lister.py b/swh/lister/cgit/lister.py
index a8da6c6..d770cbd 100644
--- a/swh/lister/cgit/lister.py
+++ b/swh/lister/cgit/lister.py
@@ -1,146 +1,148 @@
 # Copyright (C) 2019 the Software Heritage developers
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import re
 import logging
 from urllib.parse import urlparse, urljoin
 
 from bs4 import BeautifulSoup
 from requests import Session
 from requests.adapters import HTTPAdapter
 
 from .models import CGitModel
 
 from swh.core.utils import grouper
 from swh.lister import USER_AGENT
 from swh.lister.core.lister_base import ListerBase
 
 
 logger = logging.getLogger(__name__)
 
 
 class CGitLister(ListerBase):
     """Lister class for CGit repositories.
 
     This lister will retrieve the list of published git repositories by
     parsing the HTML page(s) of the index retrieved at `url`.
 
     For each found git repository, a query is made at the given url found
     in this index to gather published "Clone" URLs to be used as origin
     URL for that git repo.
 
     If several "Clone" urls are provided, prefer the http/https one, if
     any, otherwise fall bak to the first one.
 
-    A loader task is created for each git repository:
+    A loader task is created for each git repository::
 
-    Task:
-        Type: load-git
-        Policy: recurring
-        Args:
-            <git_clonable_url>
+        Task:
+            Type: load-git
+            Policy: recurring
+            Args:
+                <git_clonable_url>
 
-    Example:
-        Type: load-git
-        Policy: recurring
-        Args:
-            'https://git.savannah.gnu.org/git/elisp-es.git'
+    Example::
+
+        Task:
+            Type: load-git
+            Policy: recurring
+            Args:
+                'https://git.savannah.gnu.org/git/elisp-es.git'
     """
     MODEL = CGitModel
     DEFAULT_URL = 'https://git.savannah.gnu.org/cgit/'
     LISTER_NAME = 'cgit'
     url_prefix_present = True
 
     def __init__(self, url=None, instance=None, override_config=None):
         """Lister class for CGit repositories.
 
         Args:
             url (str): main URL of the CGit instance, i.e. url of the index
                 of published git repositories on this instance.
             instance (str): Name of cgit instance. Defaults to url's hostname
                 if unset.
 
         """
         super().__init__(override_config=override_config)
 
         if url is None:
             url = self.config.get('url', self.DEFAULT_URL)
         self.url = url
 
         if not instance:
             instance = urlparse(url).hostname
         self.instance = instance
         self.session = Session()
         self.session.mount(self.url, HTTPAdapter(max_retries=3))
         self.session.headers = {
             'User-Agent': USER_AGENT,
         }
 
     def run(self):
         status = 'uneventful'
         total = 0
         for repos in grouper(self.get_repos(), 10):
             models = list(filter(None, (self.build_model(repo)
                                         for repo in repos)))
             injected_repos = self.inject_repo_data_into_db(models)
             self.schedule_missing_tasks(models, injected_repos)
             self.db_session.commit()
             total += len(injected_repos)
             logger.debug('Scheduled %s tasks for %s', total, self.url)
             status = 'eventful'
 
         return {'status': status}
 
     def get_repos(self):
         """Generate git 'project' URLs found on the current CGit server
 
         """
         next_page = self.url
         while next_page:
             bs_idx = self.get_and_parse(next_page)
             for tr in bs_idx.find(
                     'div', {"class": "content"}).find_all(
                         "tr", {"class": ""}):
                 yield urljoin(self.url, tr.find('a')['href'])
 
             try:
                 pager = bs_idx.find('ul', {'class': 'pager'})
                 current_page = pager.find('a', {'class': 'current'})
                 if current_page:
                     next_page = current_page.parent.next_sibling.a['href']
                     next_page = urljoin(self.url, next_page)
             except (AttributeError, KeyError):
                 # no pager, or no next page
                 next_page = None
 
     def build_model(self, repo_url):
         """Given the URL of a git repo project page on a CGit server,
         return the repo description (dict) suitable for insertion in the db.
         """
         bs = self.get_and_parse(repo_url)
         urls = [x['href'] for x in bs.find_all('a', {'rel': 'vcs-git'})]
 
         if not urls:
             return
 
         # look for the http/https url, if any, and use it as origin_url
         for url in urls:
             if urlparse(url).scheme in ('http', 'https'):
                 origin_url = url
                 break
         else:
             # otherwise, choose the first one
             origin_url = urls[0]
 
         return {'uid': repo_url,
                 'name': bs.find('a', title=re.compile('.+'))['title'],
                 'origin_type': 'git',
                 'instance': self.instance,
                 'origin_url': origin_url,
                 }
 
     def get_and_parse(self, url):
         "Get the given url and parse the retrieved HTML using BeautifulSoup"
         return BeautifulSoup(self.session.get(url).text,
                              features='html.parser')
diff --git a/swh/lister/core/lister_transports.py b/swh/lister/core/lister_transports.py
index fe9832d..a857167 100644
--- a/swh/lister/core/lister_transports.py
+++ b/swh/lister/core/lister_transports.py
@@ -1,232 +1,232 @@
 # Copyright (C) 2017-2018 the Software Heritage developers
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import abc
 import random
 from datetime import datetime
 from email.utils import parsedate
 from pprint import pformat
 import logging
 
 import requests
 import xmltodict
 
 from typing import Optional, Union
 
 from swh.lister import USER_AGENT_TEMPLATE, __version__
 
 from .abstractattribute import AbstractAttribute
 from .lister_base import FetchError
 
 
 logger = logging.getLogger(__name__)
 
 
 class ListerHttpTransport(abc.ABC):
     """Use the Requests library for making Lister endpoint requests.
 
     To be used in conjunction with ListerBase or a subclass of it.
     """
     DEFAULT_URL = None  # type: Optional[str]
     PATH_TEMPLATE = \
         AbstractAttribute(
             'string containing a python string format pattern that produces'
             ' the API endpoint path for listing stored repositories when given'
             ' an index, e.g., "/repositories?after=%s". To be implemented in'
             ' the API-specific class inheriting this.'
         )  # type: Union[AbstractAttribute, Optional[str]]
 
     EXPECTED_STATUS_CODES = (200, 429, 403, 404)
 
     def request_headers(self):
         """Returns dictionary of any request headers needed by the server.
 
         MAY BE OVERRIDDEN if request headers are needed.
         """
         return {
             'User-Agent': USER_AGENT_TEMPLATE % self.lister_version
         }
 
     def request_instance_credentials(self):
         """Returns dictionary of any credentials configuration needed by the
         forge instance to list.
 
         The 'credentials' configuration is expected to be a dict of multiple
         levels. The first level is the lister's name, the second is the
         lister's instance name, which value is expected to be a list of
         credential structures (typically a couple username/password).
 
-        For example:
-
-        credentials:
-          github:  # github lister
-            github:  # has only one instance (so far)
-            - username: some
-              password: somekey
-            - username: one
-              password: onekey
-            - ...
-          gitlab:  # gitlab lister
-            riseup:  # has many instances
-            - username: someone
-              password: ...
-            - ...
-            gitlab:
-            - username: someone
-              password: ...
-            - ...
+        For example::
+
+            credentials:
+              github:  # github lister
+                github:  # has only one instance (so far)
+                  - username: some
+                    password: somekey
+                  - username: one
+                    password: onekey
+                  - ...
+                gitlab:  # gitlab lister
+                  riseup:  # has many instances
+                    - username: someone
+                      password: ...
+                    - ...
+                  gitlab:
+                    - username: someone
+                      password: ...
+                    - ...
 
         Returns:
             list of credential dicts for the current lister.
 
         """
         all_creds = self.config.get('credentials')
         if not all_creds:
             return []
         lister_creds = all_creds.get(self.LISTER_NAME, {})
         creds = lister_creds.get(self.instance, [])
         return creds
 
     def request_uri(self, identifier):
         """Get the full request URI given the transport_request identifier.
 
         MAY BE OVERRIDDEN if something more complex than the PATH_TEMPLATE is
         required.
         """
         path = self.PATH_TEMPLATE % identifier
         return self.url + path
 
     def request_params(self, identifier):
         """Get the full parameters passed to requests given the
         transport_request identifier.
 
         This uses credentials if any are provided (see
         request_instance_credentials).
 
         MAY BE OVERRIDDEN if something more complex than the request headers
         is needed.
 
         """
         params = {}
         params['headers'] = self.request_headers() or {}
         creds = self.request_instance_credentials()
         if not creds:
             return params
         auth = random.choice(creds) if creds else None
         if auth:
             params['auth'] = (auth['username'], auth['password'])
         return params
 
     def transport_quota_check(self, response):
         """Implements ListerBase.transport_quota_check with standard 429
             code check for HTTP with Requests library.
 
         MAY BE OVERRIDDEN if the server notifies about rate limits in a
             non-standard way that doesn't use HTTP 429 and the Retry-After
             response header. ( https://tools.ietf.org/html/rfc6585#section-4 )
 
         """
         if response.status_code == 429:  # HTTP too many requests
             retry_after = response.headers.get('Retry-After', self.back_off())
             try:
                 # might be seconds
                 return True, float(retry_after)
             except Exception:
                 # might be http-date
                 at_date = datetime(*parsedate(retry_after)[:6])
                 from_now = (at_date - datetime.today()).total_seconds() + 5
                 return True, max(0, from_now)
         else:  # response ok
             self.reset_backoff()
             return False, 0
 
     def __init__(self, url=None):
         if not url:
             url = self.config.get('url')
         if not url:
             url = self.DEFAULT_URL
         if not url:
             raise NameError('HTTP Lister Transport requires an url.')
         self.url = url  # eg. 'https://api.github.com'
         self.session = requests.Session()
         self.lister_version = __version__
 
     def _transport_action(self, identifier, method='get'):
         """Permit to ask information to the api prior to actually executing
            query.
 
         """
         path = self.request_uri(identifier)
         params = self.request_params(identifier)
 
         logger.debug('path: %s', path)
         logger.debug('params: %s', params)
         logger.debug('method: %s', method)
         try:
             if method == 'head':
                 response = self.session.head(path, **params)
             else:
                 response = self.session.get(path, **params)
         except requests.exceptions.ConnectionError as e:
             logger.warning('Failed to fetch %s: %s', path, e)
             raise FetchError(e)
         else:
             if response.status_code not in self.EXPECTED_STATUS_CODES:
                 raise FetchError(response)
             return response
 
     def transport_head(self, identifier):
         """Retrieve head information on api.
 
         """
         return self._transport_action(identifier, method='head')
 
     def transport_request(self, identifier):
         """Implements ListerBase.transport_request for HTTP using Requests.
 
         Retrieve get information on api.
 
         """
         return self._transport_action(identifier)
 
     def transport_response_to_string(self, response):
         """Implements ListerBase.transport_response_to_string for HTTP given
             Requests responses.
         """
         s = pformat(response.request.path_url)
         s += '\n#\n' + pformat(response.request.headers)
         s += '\n#\n' + pformat(response.status_code)
         s += '\n#\n' + pformat(response.headers)
         s += '\n#\n'
         try:  # json?
             s += pformat(response.json())
         except Exception:  # not json
             try:  # xml?
                 s += pformat(xmltodict.parse(response.text))
             except Exception:  # not xml
                 s += pformat(response.text)
         return s
 
 
 class ListerOnePageApiTransport(ListerHttpTransport):
     """Leverage requests library to retrieve basic html page and parse
        result.
 
        To be used in conjunction with ListerBase or a subclass of it.
 
     """
     PAGE = AbstractAttribute(
         "URL of the API's unique page to retrieve and parse "
         "for information")  # type: Union[AbstractAttribute, str]
     PATH_TEMPLATE = None  # we do not use it
 
     def __init__(self, url=None):
         self.session = requests.Session()
         self.lister_version = __version__
 
     def request_uri(self, _):
         """Get the full request URI given the transport_request identifier.
 
         """
         return self.PAGE
diff --git a/swh/lister/cran/lister.py b/swh/lister/cran/lister.py
index d176ca5..a818dd0 100644
--- a/swh/lister/cran/lister.py
+++ b/swh/lister/cran/lister.py
@@ -1,148 +1,139 @@
 # Copyright (C) 2019-2020 The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import json
 import logging
 import pkg_resources
 import subprocess
 
 from typing import List, Mapping, Tuple
 
 from swh.lister.cran.models import CRANModel
 
 from swh.lister.core.simple_lister import SimpleLister
 from swh.scheduler.utils import create_task_dict
 
 
 logger = logging.getLogger(__name__)
 
 
 CRAN_MIRROR = 'https://cran.r-project.org'
 
 
 class CRANLister(SimpleLister):
     MODEL = CRANModel
     LISTER_NAME = 'cran'
     instance = 'cran'
 
     def task_dict(self, origin_type, origin_url, version=None, html_url=None,
                   policy=None, **kwargs):
         """Return task format dict. This creates tasks with args and kwargs
         set, for example::
 
             args: []
             kwargs: {
                 'url': 'https://cran.r-project.org/Packages/<package>...',
                 'artifacts': [{
                     'url': 'https://cran.r-project.org/...',
                     'version': '0.0.1',
                 }]
             }
 
         """
         if not policy:
             policy = 'oneshot'
         artifact_url = html_url
         assert origin_type == 'tar'
         return create_task_dict(
             'load-cran', policy,
             url=origin_url, artifacts=[{
                 'url': artifact_url,
                 'version': version
             }], retries_left=3
         )
 
     def safely_issue_request(self, identifier):
         """Bypass the implementation. It's now the `list_packages` which
         returns data.
 
         As an implementation detail, we cannot change simply the base
         SimpleLister yet as other implementation still uses it. This shall be
         part of another refactoring pass.
 
         """
         return None
 
     def list_packages(self, response) -> List[Mapping[str, str]]:
         """Runs R script which uses inbuilt API to return a json response
            containing data about the R packages.
 
         Returns:
-            List of Dict about r packages. For example:
-
-            .. code-block:: python
+            List of Dict about R packages. For example::
 
                 [
                     {
                         'Package': 'A3',
                         'Version': '1.0.0',
-                        'Title':
-                            'Accurate, Adaptable, and Accessible Error Metrics
-                             for Predictive\nModels',
-                        'Description':
-                            'Supplies tools for tabulating and analyzing the
-                             results of predictive models. The methods employed
-                             are ... '
+                        'Title': 'A3 package',
+                        'Description': ...
                     },
                     {
                         'Package': 'abbyyR',
                         'Version': '0.5.4',
-                        'Title':
-                            'Access to Abbyy OCR (OCR) API',
-                        'Description': 'Get text from images of text using
-                                        Abbyy Cloud Optical Character\n ...'
+                        'Title': 'Access to Abbyy OCR (OCR) API',
+                        'Description': ...'
                     },
                     ...
                 ]
 
         """
         return read_cran_data()
 
     def get_model_from_repo(
             self, repo: Mapping[str, str]) -> Mapping[str, str]:
         """Transform from repository representation to model
 
         """
         logger.debug('repo: %s', repo)
         origin_url, artifact_url = compute_origin_urls(repo)
         package = repo['Package']
         version = repo['Version']
         return {
             'uid': f'{package}-{version}',
             'name': package,
             'full_name': repo['Title'],
             'version': version,
             'html_url': artifact_url,
             'origin_url': origin_url,
             'origin_type': 'tar',
         }
 
 
 def read_cran_data() -> List[Mapping[str, str]]:
     """Execute r script to read cran listing.
 
     """
     filepath = pkg_resources.resource_filename('swh.lister.cran',
                                                'list_all_packages.R')
     logger.debug('script list-all-packages.R path: %s', filepath)
     response = subprocess.run(filepath, stdout=subprocess.PIPE, shell=False)
     return json.loads(response.stdout.decode('utf-8'))
 
 
 def compute_origin_urls(repo: Mapping[str, str]) -> Tuple[str, str]:
     """Compute the package url from the repo dict.
 
     Args:
         repo: dict with key 'Package', 'Version'
 
     Returns:
         the tuple project url, artifact url
 
     """
     package = repo['Package']
     version = repo['Version']
     origin_url = f'{CRAN_MIRROR}/package={package}'
     artifact_url = f'{CRAN_MIRROR}/src/contrib/{package}_{version}.tar.gz'
     return origin_url, artifact_url
diff --git a/swh/lister/gnu/lister.py b/swh/lister/gnu/lister.py
index ddd3371..3c00573 100644
--- a/swh/lister/gnu/lister.py
+++ b/swh/lister/gnu/lister.py
@@ -1,108 +1,111 @@
 # Copyright (C) 2019 the Software Heritage developers
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import logging
 
 from swh.scheduler import utils
 from swh.lister.core.simple_lister import SimpleLister
 
 from swh.lister.gnu.models import GNUModel
 from swh.lister.gnu.tree import GNUTree
 
 
 logger = logging.getLogger(__name__)
 
 
 class GNULister(SimpleLister):
     MODEL = GNUModel
     LISTER_NAME = 'gnu'
     instance = 'gnu'
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.gnu_tree = GNUTree('https://ftp.gnu.org/tree.json.gz')
 
     def task_dict(self, origin_type, origin_url, **kwargs):
         """Return task format dict
 
         This is overridden from the lister_base as more information is
         needed for the ingestion task creation.
 
         This creates tasks with args and kwargs set, for example:
 
         .. code-block:: python
 
             args:
             kwargs: {
                 'url': 'https://ftp.gnu.org/gnu/3dldf/',
                 'artifacts': [{
                     'url': 'https://...',
                     'time': '2003-12-09T21:43:20+00:00',
                     'length': 128,
                     'version': '1.0.1',
                     'filename': 'something-1.0.1.tar.gz',
                 },
                 ...
                 ]
             }
 
         """
         artifacts = self.gnu_tree.artifacts[origin_url]
         assert origin_type == 'tar'
         return utils.create_task_dict(
             'load-archive-files',
             kwargs.get('policy', 'oneshot'),
             url=origin_url,
             artifacts=artifacts,
             retries_left=3,
         )
 
     def safely_issue_request(self, identifier):
         """Bypass the implementation. It's now the GNUTree which deals with
         querying the gnu mirror.
 
         As an implementation detail, we cannot change simply the base
         SimpleLister as other implementation still uses it. This shall be part
         of another refactoring pass.
 
         """
         return None
 
     def list_packages(self, response):
         """List the actual gnu origins (package name) with their name, url and
            associated tarballs.
 
         Args:
             response: Unused
 
         Returns:
-            List of packages name, url, last modification time
+            List of packages name, url, last modification time::
 
-            .. code-block:: python
                 [
-                    {'name': '3dldf',
-                     'url': 'https://ftp.gnu.org/gnu/3dldf/',
-                     'time_modified': '2003-12-09T20:43:20+00:00'},
-                    {'name': '8sync',
-                     'url': 'https://ftp.gnu.org/gnu/8sync/',
-                     'time_modified': '2016-12-06T02:37:10+00:00'},
+                    {
+                        'name': '3dldf',
+                        'url': 'https://ftp.gnu.org/gnu/3dldf/',
+                        'time_modified': '2003-12-09T20:43:20+00:00'
+                    },
+                    {
+                        'name': '8sync',
+                        'url': 'https://ftp.gnu.org/gnu/8sync/',
+                        'time_modified': '2016-12-06T02:37:10+00:00'
+                    },
                     ...
                 ]
 
         """
         return list(self.gnu_tree.projects.values())
 
     def get_model_from_repo(self, repo):
         """Transform from repository representation to model
 
         """
         return {
             'uid': repo['url'],
             'name': repo['name'],
             'full_name': repo['name'],
             'html_url': repo['url'],
             'origin_url': repo['url'],
             'time_last_updated': repo['time_modified'],
             'origin_type': 'tar',
         }
diff --git a/swh/lister/packagist/lister.py b/swh/lister/packagist/lister.py
index 5461ed6..98e72f3 100644
--- a/swh/lister/packagist/lister.py
+++ b/swh/lister/packagist/lister.py
@@ -1,98 +1,100 @@
 # Copyright (C) 2019  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import json
 import logging
 import random
 
 from typing import Any, Dict, List, Mapping
 
 from swh.scheduler import utils
 from swh.lister.core.simple_lister import SimpleLister
 from swh.lister.core.lister_transports import ListerOnePageApiTransport
 
 from .models import PackagistModel
 
 
 logger = logging.getLogger(__name__)
 
 
 def compute_package_url(repo_name: str) -> str:
     """Compute packgist package url from repo name.
 
     """
     return 'https://repo.packagist.org/p/%s.json' % repo_name
 
 
 class PackagistLister(ListerOnePageApiTransport, SimpleLister):
     """List packages available in the Packagist package manager.
 
         The lister sends the request to the url present in the class
         variable `PAGE`, to receive a list of all the package names
         present in the Packagist package manager. Iterates over all the
         packages and constructs the metadata url of the package from
-        the name of the package and creates a loading task.
-
-        Task:
-            Type: load-packagist
-            Policy: recurring
-            Args:
-                <package_name>
-                <package_metadata_url>
-
-        Example:
-            Type: load-packagist
-            Policy: recurring
-            Args:
-                'hypejunction/hypegamemechanics'
-                'https://repo.packagist.org/p/hypejunction/hypegamemechanics.json'
+        the name of the package and creates a loading task::
+
+            Task:
+                Type: load-packagist
+                Policy: recurring
+                Args:
+                    <package_name>
+                    <package_metadata_url>
+
+        Example::
+
+            Task:
+                Type: load-packagist
+                Policy: recurring
+                Args:
+                    'hypejunction/hypegamemechanics'
+                    'https://repo.packagist.org/p/hypejunction/hypegamemechanics.json'
 
     """
     MODEL = PackagistModel
     LISTER_NAME = 'packagist'
     PAGE = 'https://packagist.org/packages/list.json'
     instance = 'packagist'
 
     def __init__(self, override_config=None):
         ListerOnePageApiTransport .__init__(self)
         SimpleLister.__init__(self, override_config=override_config)
 
     def task_dict(self, origin_type: str, origin_url: str,
                   **kwargs: Mapping[str, str]) -> Dict[str, Any]:
         """Return task format dict
 
         This is overridden from the lister_base as more information is
         needed for the ingestion task creation.
 
         """
         return utils.create_task_dict(
             'load-%s' % origin_type,
             kwargs.get('policy', 'recurring'),
             kwargs.get('name'), origin_url,
             retries_left=3)
 
     def list_packages(self, response: Any) -> List[str]:
         """List the actual packagist origins from the response.
 
         """
         response = json.loads(response.text)
         packages = [name for name in response['packageNames']]
         logger.debug('Number of packages: %s', len(packages))
         random.shuffle(packages)
         return packages
 
     def get_model_from_repo(self, repo_name: str) -> Mapping[str, str]:
         """Transform from repository representation to model
 
         """
         url = compute_package_url(repo_name)
         return {
             'uid': repo_name,
             'name': repo_name,
             'full_name': repo_name,
             'html_url': url,
             'origin_url': url,
             'origin_type': 'packagist',
         }