From 11a9b4f6405b30a5b48aa470abd8b90a67b3e39d Mon Sep 17 00:00:00 2001
From: Patrick Jentsch <p.jentsch@uni-bielefeld.de>
Date: Fri, 28 May 2021 15:51:23 +0200
Subject: [PATCH] create small subtemplates for each block und apply new
 breadcrumbs logic

---
 web/app/corpora/events2.py                    | 363 ++++++++++++++++++
 web/app/templates/_colors.html.j2             |  31 ++
 web/app/templates/_footer.html.j2             |  40 ++
 web/app/templates/_navbar.html.j2             |  38 ++
 web/app/templates/_roadmap.html.j2            |  34 ++
 web/app/templates/_scripts.html.j2            |  37 ++
 web/app/templates/_sidenav.html.j2            |  30 ++
 .../{_color.html.j2 => _styles.html.j2}       |  41 +-
 web/app/templates/admin/_breadcrumbs.html.j2  |  37 +-
 web/app/templates/admin/edit_user.html.j2     |   7 +-
 web/app/templates/admin/user.html.j2          |   7 +-
 web/app/templates/admin/users.html.j2         |   7 +-
 web/app/templates/auth/_breadcrumbs.html.j2   |  29 +-
 web/app/templates/auth/login.html.j2          |   7 +-
 web/app/templates/auth/register.html.j2       |   7 +-
 web/app/templates/auth/reset_password.html.j2 |   7 +-
 .../auth/reset_password_request.html.j2       |   7 +-
 web/app/templates/auth/unconfirmed.html.j2    |   7 +-
 web/app/templates/base.html.j2                |  49 +++
 .../templates/corpora/_breadcrumbs.html.j2    |  57 ++-
 web/app/templates/corpora/add_corpus.html.j2  |   7 +-
 .../templates/corpora/add_corpus_file.html.j2 |   7 +-
 .../templates/corpora/analyse_corpus.html.j2  |   7 +-
 web/app/templates/corpora/corpus.html.j2      |   7 +-
 web/app/templates/corpora/corpus_file.html.j2 |   7 +-
 .../templates/corpora/import_corpus.html.j2   |   7 +-
 .../query_results/_breadcrumbs.html.j2        |  25 +-
 .../query_results/add_query_result.html.j2    |   7 +-
 .../corpora/query_results/inspect.html.j2     |   7 +-
 .../query_results/query_result.html.j2        |   7 +-
 web/app/templates/errors/403.html.j2          |   2 +-
 web/app/templates/errors/404.html.j2          |   2 +-
 web/app/templates/errors/413.html.j2          |   2 +-
 web/app/templates/errors/500.html.j2          |   2 +-
 web/app/templates/jobs/_breadcrumbs.html.j2   |  17 +-
 web/app/templates/jobs/job.html.j2            |   7 +-
 web/app/templates/main/_breadcrumbs.html.j2   |  29 +-
 web/app/templates/main/about_and_faq.html.j2  |   7 +-
 web/app/templates/main/dashboard.html.j2      |   7 +-
 web/app/templates/main/index.html.j2          |   7 +-
 web/app/templates/main/news.html.j2           |   7 +-
 web/app/templates/main/privacy_policy.html.j2 |   7 +-
 web/app/templates/main/terms_of_use.html.j2   |   7 +-
 web/app/templates/nopaque.html.j2             | 222 -----------
 .../templates/services/_breadcrumbs.html.j2   |  29 +-
 .../services/corpus_analysis.html.j2          |   7 +-
 web/app/templates/services/file_setup.html.j2 |   7 +-
 web/app/templates/services/nlp.html.j2        |   7 +-
 web/app/templates/services/ocr.html.j2        |   7 +-
 .../templates/settings/_breadcrumbs.html.j2   |  25 +-
 .../settings/change_password.html.j2          |   7 +-
 .../settings/edit_general_settings.html.j2    |   7 +-
 .../edit_notification_settings.html.j2        |   7 +-
 53 files changed, 814 insertions(+), 544 deletions(-)
 create mode 100644 web/app/corpora/events2.py
 create mode 100644 web/app/templates/_colors.html.j2
 create mode 100644 web/app/templates/_footer.html.j2
 create mode 100644 web/app/templates/_navbar.html.j2
 create mode 100644 web/app/templates/_roadmap.html.j2
 create mode 100644 web/app/templates/_scripts.html.j2
 create mode 100644 web/app/templates/_sidenav.html.j2
 rename web/app/templates/{_color.html.j2 => _styles.html.j2} (86%)
 create mode 100644 web/app/templates/base.html.j2
 delete mode 100644 web/app/templates/nopaque.html.j2

diff --git a/web/app/corpora/events2.py b/web/app/corpora/events2.py
new file mode 100644
index 00000000..71f992c4
--- /dev/null
+++ b/web/app/corpora/events2.py
@@ -0,0 +1,363 @@
+from datetime import datetime
+from flask import current_app, request
+from flask_login import current_user
+from socket import gaierror
+from .. import db, socketio
+from ..decorators import socketio_login_required
+from ..events import socketio_sessions
+from ..models import Corpus, User
+import cqi
+import math
+import os
+import shutil
+import logging
+
+
+'''
+' A dictionary containing lists of, with corpus ids associated, Socket.IO
+' session ids (sid). {<corpus_id>: [<sid>, ...], ...}
+'''
+corpus_analysis_sessions = {}
+'''
+' A dictionary containing Socket.IO session id - CQi client pairs.
+' {<sid>: CQiClient, ...}
+'''
+corpus_analysis_clients = {}
+
+
+@socketio.on('corpus_analysis_init')
+@socketio_login_required
+def init_corpus_analysis(corpus_id):
+    corpus = Corpus.query.get(corpus_id)
+    if corpus is None:
+        response = {'code': 404, 'desc': None, 'msg': 'Not Found'}
+        socketio.emit('corpus_analysis_init', response, room=request.sid)
+        return
+    if not (corpus.creator == current_user or current_user.is_administrator()):  # noqa
+        response = {'code': 403, 'desc': None, 'msg': 'Forbidden'}
+        socketio.emit('corpus_analysis_init', response, room=request.sid)
+        return
+    if corpus.status not in ['prepared', 'start analysis', 'analysing']:
+        response = {'code': 424, 'desc': 'Corpus status is not "prepared", "start analysis" or "analying"', 'msg': 'Failed Dependency'}  # noqa
+        socketio.emit('corpus_analysis_init', response, room=request.sid)
+        return
+    if corpus.status == 'prepared':
+        corpus.status = 'start analysis'
+        db.session.commit()
+        event = 'user_{}_patch'.format(current_user.id)
+        jsonpatch = [{'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}]  # noqa
+        room = 'user_{}'.format(corpus.user_id)
+        socketio.emit(event, jsonpatch, room=room)
+    socketio.start_background_task(corpus_analysis_session_handler,
+                                   current_app._get_current_object(),
+                                   corpus_id, current_user.id, request.sid)
+
+
+def corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
+    with app.app_context():
+        ''' Setup analysis session '''
+        corpus = Corpus.query.get(corpus_id)
+        retry_counter = 15
+        while corpus.status != 'analysing':
+            db.session.refresh(corpus)
+            retry_counter -= 1
+            if retry_counter == 0:
+                response = {'code': 408, 'desc': 'Corpus analysis session took to long to start', 'msg': 'Request Timeout'}  # noqa
+                socketio.emit('corpus_analysis_init', response, room=request.sid)  # noqa
+            socketio.sleep(3)
+        client = cqi.CQiClient('cqpserver_{}'.format(corpus_id))
+        try:
+            connect_status = client.connect()
+            payload = {'code': connect_status, 'msg': cqi.api.specification.lookup[connect_status]}  # noqa
+        except cqi.errors.CQiException as e:
+            payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+            response = {'code': 500, 'desc': None,
+                        'msg': 'Internal Server Error', 'payload': payload}
+            socketio.emit('corpus_analysis_init', response, room=session_id)
+            return
+        except gaierror:
+            response = {'code': 500, 'desc': None,
+                        'msg': 'Internal Server Error'}
+            socketio.emit('corpus_analysis_init', response, room=session_id)
+            return
+        corpus_analysis_clients[session_id] = client
+        if corpus_id in corpus_analysis_sessions:
+            corpus_analysis_sessions[corpus_id].append(session_id)
+        else:
+            corpus_analysis_sessions[corpus_id] = [session_id]
+        client.status = 'ready'
+        response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
+        socketio.emit('corpus_analysis_init', response, room=session_id)
+        ''' Observe analysis session '''
+        while session_id in socketio_sessions:
+            socketio.sleep(3)
+        ''' Teardown analysis session '''
+        if client.status == 'running':
+            client.status = 'abort'
+            while client.status != 'ready':
+                socketio.sleep(0.1)
+        try:
+            client.disconnect()
+        except cqi.errors.CQiException:
+            pass
+        corpus_analysis_clients.pop(session_id, None)
+        corpus_analysis_sessions[corpus_id].remove(session_id)
+        if not corpus_analysis_sessions[corpus_id]:
+            corpus_analysis_sessions.pop(corpus_id, None)
+            corpus.status = 'stop analysis'
+            db.session.commit()
+            event = 'user_{}_patch'.format(corpus.user_id)
+            jsonpatch = [{'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}]  # noqa
+            room = 'user_{}'.format(corpus.user_id)
+            socketio.emit(event, jsonpatch, room=room)
+
+
+@socketio.on('corpus_analysis_meta_data')
+@socketio_login_required
+def corpus_analysis_get_meta_data(corpus_id):
+    # get meta data from db
+    db_corpus = Corpus.query.get(corpus_id)
+    metadata = {}
+    metadata['corpus_name'] = db_corpus.title
+    metadata['corpus_description'] = db_corpus.description
+    metadata['corpus_creation_date'] = db_corpus.creation_date.isoformat()
+    metadata['corpus_last_edited_date'] = \
+        db_corpus.last_edited_date.isoformat()
+    client = corpus_analysis_clients.get(request.sid)
+    if client is None:
+        response = {'code': 424, 'desc': 'No client found for this session',
+                    'msg': 'Failed Dependency'}
+        socketio.emit('corpus_analysis_meta_data', response, room=request.sid)
+        return
+    # check if client is busy or not
+    if client.status == 'running':
+        client.status = 'abort'
+        while client.status != 'ready':
+            socketio.sleep(0.1)
+    # get meta data from corpus in cqp server
+    client.status = 'running'
+    try:
+        client_corpus = client.corpora.get('CORPUS')
+        metadata['corpus_properties'] = client_corpus.attrs['properties']
+        metadata['corpus_size_tokens'] = client_corpus.attrs['size']
+
+        text_attr = client_corpus.structural_attributes.get('text')
+        struct_attrs = client_corpus.structural_attributes.list(
+            filters={'part_of': text_attr})
+        text_ids = range(0, (text_attr.attrs['size']))
+        texts_metadata = {}
+        for text_id in text_ids:
+            texts_metadata[text_id] = {}
+            for struct_attr in struct_attrs:
+                texts_metadata[text_id][struct_attr.attrs['name'][(len(text_attr.attrs['name']) + 1):]] = struct_attr.values_by_ids(list(range(struct_attr.attrs['size'])))[text_id]  # noqa
+        metadata['corpus_all_texts'] = texts_metadata
+        metadata['corpus_analysis_date'] = datetime.utcnow().isoformat()
+        metadata['corpus_cqi_py_protocol_version'] = client.api.version
+        metadata['corpus_cqi_py_package_version'] = cqi.__version__
+        # TODO: make this dynamically
+        metadata['corpus_cqpserver_version'] = 'CQPserver v3.4.22'
+
+        # write some metadata to the db
+        db_corpus.current_nr_of_tokens = metadata['corpus_size_tokens']
+        db.session.commit()
+        event = 'user_{}_patch'.format(db_corpus.user_id)
+        jsonpatch = [{'op': 'replace', 'path': '/corpora/{}/current_nr_of_tokens'.format(db_corpus.id), 'value': db_corpus.current_nr_of_tokens}]  # noqa
+        room = 'user_{}'.format(db_corpus.user_id)
+        socketio.emit(event, jsonpatch, room=room)
+
+        # emit data
+        payload = metadata
+        response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK',
+                    'payload': payload}
+        socketio.emit('corpus_analysis_meta_data', response, room=request.sid)
+    except cqi.errors.CQiException as e:
+        payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+        response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
+                    'payload': payload}
+        socketio.emit('corpus_analysis_meta_data', response, room=request.sid)
+    client.status = 'ready'
+
+
+@socketio.on('corpus_analysis_query')
+@socketio_login_required
+def corpus_analysis_query(query):
+    client = corpus_analysis_clients.get(request.sid)
+    if client is None:
+        response = {'code': 424, 'desc': 'No client found for this session',
+                    'msg': 'Failed Dependency'}
+        socketio.emit('corpus_analysis_query', response, room=request.sid)
+        return
+    if client.status == 'running':
+        client.status = 'abort'
+        while client.status != 'ready':
+            socketio.sleep(0.1)
+    client.status = 'running'
+    try:
+        corpus = client.corpora.get('CORPUS')
+        query_status = corpus.query(query)
+        results = corpus.subcorpora.get('Results')
+    except cqi.errors.CQiException as e:
+        client.status = 'ready'
+        payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+        response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
+                    'payload': payload}
+        socketio.emit('corpus_analysis_query', response, room=request.sid)
+        return
+    payload = {'status': query_status,
+               'msg': cqi.api.specification.lookup[query_status],
+               'match_count': results.attrs['size']}
+    response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
+    socketio.emit('corpus_analysis_query', response, room=request.sid)
+    chunk_size = 100
+    chunk_start = 0
+    context = 50
+    progress = 0
+    while chunk_start <= results.attrs['size']:
+        if client.status == 'abort':
+            break
+        try:
+            chunk = results.export(context=context, cutoff=chunk_size, offset=chunk_start)  # noqa
+        except cqi.errors.CQiException as e:
+            client.status = 'ready'
+            payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+            response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
+                        'payload': payload}
+            socketio.emit('corpus_analysis_query', response, room=request.sid)
+            return
+        if (results.attrs['size'] == 0):
+            progress = 100
+        else:
+            progress = ((chunk_start + chunk_size) / results.attrs['size']) * 100  # noqa
+            progress = min(100, int(math.ceil(progress)))
+        response = {'code': 200, 'desc': None, 'msg': 'OK',
+                    'payload': {'chunk': chunk, 'progress': progress}}
+        socketio.emit('corpus_analysis_query_results', response,
+                      room=request.sid)
+        chunk_start += chunk_size
+    client.status = 'ready'
+
+
+@socketio.on('corpus_analysis_inspect_match')
+@socketio_login_required
+def corpus_analysis_inspect_match(payload):
+    client = corpus_analysis_clients.get(request.sid)
+    if client is None:
+        response = {'code': 424, 'desc': 'No client found for this session',
+                    'msg': 'Failed Dependency'}
+        socketio.emit('corpus_analysis_inspect_match', response, room=request.sid)  # noqa
+        return
+    match_id = payload['match_id']
+    if client.status == 'running':
+        client.status = 'abort'
+        while client.status != 'ready':
+            socketio.sleep(0.1)
+    client.status = 'running'
+    try:
+        corpus = client.corpora.get('CORPUS')
+        results = corpus.subcorpora.get('Results')
+    except cqi.errors.CQiException as e:
+        client.status = 'ready'
+        payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+        response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
+                    'payload': payload}
+        socketio.emit('corpus_analysis_inspect_match', response, room=request.sid)  # noqa
+        return
+    context = 200
+    try:
+        payload = results.export(context=context, cutoff=1, offset=match_id)
+    except cqi.errors.CQiException as e:
+        client.status = 'ready'
+        payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+        response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
+                    'payload': payload}
+        socketio.emit('corpus_analysis_inspect_match', response, room=request.sid)  # noqa
+        return
+    response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
+    socketio.emit('corpus_analysis_inspect_match', response, room=request.sid)
+    client.status = 'ready'
+
+
+
+@socketio.on('corpus_analysis_get_match_with_full_context')
+@socketio_login_required
+def corpus_analysis_get_match_with_full_context(payload):
+    type = payload['type']
+    data_indexes = payload['data_indexes']
+    first_cpos = payload['first_cpos']
+    last_cpos = payload['last_cpos']
+    client = corpus_analysis_clients.get(request.sid)
+    if client is None:
+        response = {'code': 424, 'desc': 'No client found for this session',
+                    'msg': 'Failed Dependency'}
+        socketio.emit('corpus_analysis_get_match_with_full_context', response,
+                      room=request.sid)
+        return
+    if client.status == 'running':
+        client.status = 'abort'
+        while client.status != 'ready':
+            socketio.sleep(0.1)
+    client.status = 'running'
+    try:
+        corpus = client.corpora.get('CORPUS')
+        s = corpus.structural_attributes.get('s')
+        payload = {}
+        payload['matches'] = []
+        payload['cpos_lookup'] = {}
+        payload['text_lookup'] = {}
+        payload['progress'] = 0
+        i = 0
+        # Send data one match at a time.
+        for index, f_cpos, l_cpos in zip(data_indexes, first_cpos, last_cpos):
+            i += 1
+            tmp_match = s.export(f_cpos, l_cpos, context=10)
+            payload['matches'].append(tmp_match['matches'][0])
+            payload['cpos_lookup'].update(tmp_match['cpos_lookup'])
+            payload['text_lookup'].update(tmp_match['text_lookup'])
+            payload['progress'] = i/len(data_indexes)*100
+            response = {'code': 200,
+                        'desc': None,
+                        'msg': 'OK',
+                        'payload': payload,
+                        'type': type,
+                        'data_indexes': data_indexes}
+            socketio.emit('corpus_analysis_get_match_with_full_context',
+                          response, room=request.sid)
+            payload['matches'] = []
+            payload['cpos_lookup'] = {}
+            payload['text_lookup'] = {}
+    except cqi.errors.CQiException as e:
+        payload = {'code': e.code, 'desc': e.description, 'msg': e.name}
+        response = {'code': 500,
+                    'desc': None,
+                    'msg': 'Internal Server Error',
+                    'payload': payload,
+                    'type': type,
+                    'data_indexes': data_indexes}
+        socketio.emit('corpus_analysis_get_match_with_full_context',
+                      response,
+                      room=request.sid)
+    client.status = 'ready'
+
+
+@socketio.on('export_corpus')
+@socketio_login_required
+def export_corpus(corpus_id):
+    corpus = Corpus.query.get(corpus_id)
+    if corpus is None:
+        response = {'code': 404, 'msg': 'Not found'}
+        socketio.emit('export_corpus', response, room=request.sid)
+        return
+    if corpus.status != 'prepared':
+        response = {'code': 412, 'msg': 'Precondition Failed'}
+        socketio.emit('export_corpus', response, room=request.sid)
+        return
+    # delete old corpus archive if it exists/has been build before
+    if corpus.archive_file is not None and os.path.isfile(corpus.archive_file):
+        os.remove(corpus.archive_file)
+    zip_name = corpus.title
+    zip_path = os.path.join(current_user.path, 'corpora', zip_name)
+    corpus.archive_file = os.path.join(corpus.path, zip_name) + '.zip'
+    db.session.commit()
+    shutil.make_archive(zip_path, 'zip', corpus.path)
+    shutil.move(zip_path + '.zip', corpus.archive_file)
+    socketio.emit('export_corpus_' + str(corpus.id), room=request.sid)
diff --git a/web/app/templates/_colors.html.j2 b/web/app/templates/_colors.html.j2
new file mode 100644
index 00000000..04301c86
--- /dev/null
+++ b/web/app/templates/_colors.html.j2
@@ -0,0 +1,31 @@
+{% set primary = '#00426f' %}
+{% set primary_variant = '#1A5C89' %}
+{% set secondary = '#00426f' %}
+{% set secondary_variant = '#1A5C89' %}
+{% set background = '#ffffff' %}
+{% set surface = '#ffffff' %}
+{% set error = '#b00020' %}
+
+{% set service_corpus_analysis = '#aa9cc9' %}
+{% set service_corpus_analysis_darken = '#6b3f89' %}
+{% set service_corpus_analysis_lighten = '#ebe8f6' %}
+{% set service_file_setup = '#d5dc95' %}
+{% set service_file_setup_darken = '#a1b300' %}
+{% set service_file_setup_lighten = '#f2f3e1' %}
+{% set service_nlp = '#98acd2' %}
+{% set service_nlp_darken = '#0064a3' %}
+{% set service_nlp_lighten = '#e5e8f5' %}
+{% set service_ocr = '#a9d8c8' %}
+{% set service_ocr_darken = '#00a58b' %}
+{% set service_ocr_lighten = '#e7f4f1' %}
+
+{% set status_unprepared = '#9e9e9e' %}
+{% set status_submitted = '#9e9e9e' %}
+{% set status_queued = '#2196f3' %}
+{% set status_running = '#ffc107' %}
+{% set status_complete = '#4caf50' %}
+{% set status_failed = '#f44336' %}
+{% set status_prepared = '#4caf50' %}
+{% set status_start_analysis = '#2196f3' %}
+{% set status_analysing = '#4caf50' %}
+{% set status_stop_analysis = '#ff5722' %}
diff --git a/web/app/templates/_footer.html.j2 b/web/app/templates/_footer.html.j2
new file mode 100644
index 00000000..4938c574
--- /dev/null
+++ b/web/app/templates/_footer.html.j2
@@ -0,0 +1,40 @@
+<div class="container">
+  <div class="row">
+    <div class="col s6 m3">
+      <a href="https://www.dfg.de/">
+        <img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
+      </a>
+    </div>
+    <div class="col s6 m3 offset-m1 center-align">
+      <a href="https://www.uni-bielefeld.de/sfb1288/">
+        <img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_sfb_1288.png') }}">
+      </a>
+    </div>
+    <div class="col s12 m3 offset-m1">
+      <h5 class="white-text">Legal Notice</h5>
+      <ul>
+        <li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
+        <li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
+        <li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
+        <li></li>
+      </ul>
+    </div>
+  </div>
+</div>
+<div class="footer-copyright primary-color">
+  <div class="container">
+    <div class="row" style="margin-bottom: 0;">
+      <div class="col s12 m3">
+        <span>© 2020 Bielefeld University</span>
+      </div>
+      <div class="col s12 m9 right-align">
+        <a class="btn-small blue waves-effect waves-light" href="{{ url_for('main.about_and_faq') }}"><i class="left material-icons">info_outline</i>About and faq</a>
+        {% if config.NOPAQUE_CONTACT %}
+        <a class="btn-small pink waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Contact"><i class="left material-icons">rate_review</i>Contact</a>
+        <a class="btn-small green waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Feedback"><i class="left material-icons">feedback</i>Feedback</a>
+        {% endif %}
+        <a class="btn-small orange waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque"><i class="left material-icons">code</i>GitLab</a>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/web/app/templates/_navbar.html.j2 b/web/app/templates/_navbar.html.j2
new file mode 100644
index 00000000..485960b0
--- /dev/null
+++ b/web/app/templates/_navbar.html.j2
@@ -0,0 +1,38 @@
+<div class="navbar-fixed">
+  <nav class="nav-extended">
+    <div class="nav-wrapper primary-color">
+      {% if current_user.is_authenticated %}
+      <a href="#" data-target="sidenav" class="sidenav-trigger"><i class="material-icons">menu</i></a>
+      {% endif %}
+      <a href="{{ url_for('main.index') }}" class="brand-logo" style="height: 100%; overflow: hidden;">
+        <img class="hide-on-small-only" src="{{ url_for('static', filename='images/nopaque_-_logo_name_slogan.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
+        <img class="hide-on-med-and-up" src="{{ url_for('static', filename='images/nopaque_-_logo.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
+      </a>
+      <ul class="right">
+        <li><a class="dropdown-trigger no-autoinit" data-target="nav-more-dropdown" href="#!" id="nav-more-dropdown-trigger"><i class="material-icons">more_vert</i></a></li>
+      </ul>
+    </div>
+    <div class="nav-content primary-variant-color">
+      <ul class="tabs tabs-transparent">
+        <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
+        {% if breadcrumbs is defined %}
+        {{ breadcrumbs }}
+        {% endif %}
+      </ul>
+      {% if current_user.is_authenticated %}
+      <a class="btn-floating btn-large halfway-fab modal-trigger pink tooltipped waves-effect waves-light" data-tooltip="Roadmap" href="#roadmap-modal"><i class="material-icons">explore</i></a>
+      {% endif %}
+    </div>
+  </nav>
+</div>
+
+<ul class="dropdown-content" id="nav-more-dropdown">
+  {% if current_user.is_authenticated %}
+  <li><a href="{{ url_for('settings.index') }}"><i class="material-icons left">settings</i>Settings</a></li>
+  <li class="divider" tabindex="-1"></li>
+  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
+  {% else %}
+  <li{% if request.path == url_for('auth.register') %} class="active"{% endif %}><a href="{{ url_for('auth.register') }}"><i class="material-icons left">assignment</i>Register</a></li>
+  <li{% if request.path == url_for('auth.login') %} class="active"{% endif %}><a href="{{ url_for('auth.login') }}"><i class="material-icons left">login</i>Log in</a></li>
+  {% endif %}
+</ul>
diff --git a/web/app/templates/_roadmap.html.j2 b/web/app/templates/_roadmap.html.j2
new file mode 100644
index 00000000..56375fb1
--- /dev/null
+++ b/web/app/templates/_roadmap.html.j2
@@ -0,0 +1,34 @@
+<div id="roadmap-modal" class="modal">
+  <div class="modal-content">
+    <h2>Roadmap</h2>
+    <p>The roadmap guides you through nopaque's workflow! If you have the necessary input fie formats, you can directly jump into the corresponding process. If not, you can use the roadmap to jump right to the preceding process.</p>
+    <ul class="tabs tabs-fixed-width">
+      <li class="tab"><a{%if request.path == url_for('services.service', service='file-setup') %} class="active"{% endif %} href="{{ url_for('services.service', service='file-setup') }}" target="_self">File setup</a></li>
+      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+      <li class="tab"><a{%if request.path == url_for('services.service', service='ocr') %} class="active"{% endif %} href="{{ url_for('services.service', service='ocr') }}" target="_self">OCR</a></li>
+      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+      <li class="tab"><a{%if request.path == url_for('services.service', service='nlp') %} class="active"{% endif %} href="{{ url_for('services.service', service='nlp') }}" target="_self">NLP</a></li>
+      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+      <li class="tab"><a{%if request.path == url_for('corpora.add_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.add_corpus') }}" target="_self">Add corpus</a></li>
+      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+      {% if corpus %}
+      <li class="tab"><a{%if request.path == url_for('corpora.add_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.add_corpus_file', corpus_id=corpus.id) }}" target="_self">Add corpus file(s)</a></li>
+      {% else %}
+      <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Add corpus file(s)</a></li>
+      {% endif %}
+      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+      {% if corpus %}
+      {% if corpus.files.all() %}
+      <li class="tab"><a{%if request.path == url_for('corpora.analyse_corpus', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}" target="_self">Corpus analysis</a></li>
+      {% else %}
+      <li class="tab disabled tooltipped" data-tooltip="Add at least one corpus file first"><a>Corpus analysis</a></li>
+      {% endif %}
+      {% else %}
+      <li class="tab disabled tooltipped" data-tooltip="Select a corpus first"><a>Corpus analysis</a></li>
+      {% endif %}
+    </ul>
+  </div>
+  <div class="modal-footer">
+    <a href="#!" class="modal-close waves-effect waves-green btn-flat">Close</a>
+  </div>
+</div>
diff --git a/web/app/templates/_scripts.html.j2 b/web/app/templates/_scripts.html.j2
new file mode 100644
index 00000000..ec485beb
--- /dev/null
+++ b/web/app/templates/_scripts.html.j2
@@ -0,0 +1,37 @@
+{% if current_user.setting_dark_mode %}
+<script src="{{ url_for('static', filename='js/darkreader.js') }}"></script>
+<script>
+  DarkReader.enable({brightness: 150, contrast: 100, sepia: 0});
+</script>
+{% endif %}
+<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
+<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
+<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
+<script src="{{ url_for('static', filename='js/nopaque/main.js') }}"></script>
+{% assets filters='rjsmin', output="js/nopaque/RessourceDisplays.min.compiled.js",
+          "js/nopaque/RessourceDisplays/RessourceDisplay.js",
+          "js/nopaque/RessourceDisplays/CorpusDisplay.js",
+          "js/nopaque/RessourceDisplays/JobDisplay.js" %}
+<script src="{{ ASSET_URL }}"></script>
+{% endassets %}
+{% assets filters='rjsmin', output="js/nopaque/RessourceLists.min.compiled.js",
+          "js/nopaque/RessourceLists/RessourceList.js",
+          "js/nopaque/RessourceLists/CorpusList.js",
+          "js/nopaque/RessourceLists/CorpusFileList.js",
+          "js/nopaque/RessourceLists/JobList.js",
+          "js/nopaque/RessourceLists/JobInputList.js",
+          "js/nopaque/RessourceLists/JobResultList.js",
+          "js/nopaque/RessourceLists/QueryResultList.js",
+          "js/nopaque/RessourceLists/UserList.js" %}
+<script src="{{ ASSET_URL }}"></script>
+{% endassets %}
+<script>
+  // Disable all option elements with no value
+  for (let optionElement of document.querySelectorAll('option[value=""]')) {optionElement.disabled = true;}
+  M.AutoInit();
+  M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
+  M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
+  nopaque.appClient = new AppClient({% if current_user.is_authenticated %}{{ current_user.id }}{% endif %});
+  nopaque.Forms.init();
+  for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {nopaque.appClient.flash(flashedMessage[1], flashedMessage[0]);}
+</script>
diff --git a/web/app/templates/_sidenav.html.j2 b/web/app/templates/_sidenav.html.j2
new file mode 100644
index 00000000..f1f9acb7
--- /dev/null
+++ b/web/app/templates/_sidenav.html.j2
@@ -0,0 +1,30 @@
+<ul class="sidenav sidenav-fixed" id="sidenav">
+  <li>
+    <div class="user-view">
+      <div class="background primary-color"></div>
+      <span class="white-text name">{{ current_user.username }}</span>
+      <span class="white-text email">{{ current_user.email }}</span>
+    </div>
+  </li>
+  <li><a href="{{ url_for('main.index') }}">nopaque</a></li>
+  <li><a href="{{ url_for('main.news') }}"><i class="material-icons left">email</i>News</a></li>
+  <li><a href="#"><i class="material-icons">linear_scale</i>Workflow</a></li>
+  <li><a href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a></li>
+  <li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>My Corpora</a></li>
+  <li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icons">J</i>My Jobs</a></li>
+  <li><div class="divider"></div></li>
+  <li><a class="subheader">Processes & Services</a></li>
+  <li class="service-color service-color-border border-darken" data-service="file-setup" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='file_setup') }}"><i class="nopaque-icons service-icon" data-service="file-setup"></i>File setup</a></li>
+  <li class="service-color service-color-border border-darken" data-service="ocr" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='ocr') }}"><i class="nopaque-icons service-icon" data-service="ocr"></i>OCR</a></li>
+  <li class="service-color service-color-border border-darken" data-service="nlp" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='nlp') }}"><i class="nopaque-icons service-icon" data-service="nlp"></i>NLP</a></li>
+  <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='corpus_analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
+  <li><div class="divider"></div></li>
+  <li><a class="subheader">Account</a></li>
+  <li><a href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a></li>
+  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
+  {% if current_user.is_administrator() %}
+  <li><div class="divider"></div></li>
+  <li><a class="subheader">Administration</a></li>
+  <li><a href="{{ url_for('admin.index') }}"><i class="material-icons">build</i>Administration</a></li>
+  {% endif %}
+</ul>
diff --git a/web/app/templates/_color.html.j2 b/web/app/templates/_styles.html.j2
similarity index 86%
rename from web/app/templates/_color.html.j2
rename to web/app/templates/_styles.html.j2
index e4f2b1bf..069afe5b 100644
--- a/web/app/templates/_color.html.j2
+++ b/web/app/templates/_styles.html.j2
@@ -1,38 +1,10 @@
-{% set colors = {
-  'primary': '#00426f',
-  'primary_variant': '#1A5C89',
-  'secondary': '#00426f',
-  'secondary_variant': '#1A5C89',
-  'background': '#ffffff',
-  'surface': '#ffffff',
-  'error': '#b00020',
-
-  'service_corpus_analysis': '#aa9cc9',
-  'service_corpus_analysis_darken': '#6b3f89',
-  'service_corpus_analysis_lighten': '#ebe8f6',
-  'service_file_setup': '#d5dc95',
-  'service_file_setup_darken': '#a1b300',
-  'service_file_setup_lighten': '#f2f3e1',
-  'service_nlp': '#98acd2',
-  'service_nlp_darken': '#0064a3',
-  'service_nlp_lighten': '#e5e8f5',
-  'service_ocr': '#a9d8c8',
-  'service_ocr_darken': '#00a58b',
-  'service_ocr_lighten': '#e7f4f1',
-
-  'status_unprepared': '#9e9e9e',
-  'status_submitted': '#9e9e9e',
-  'status_queued': '#2196f3',
-  'status_running': '#ffc107',
-  'status_complete': '#4caf50',
-  'status_failed': '#f44336',
-  'status_prepared': '#4caf50',
-  'status_start_analysis': '#2196f3',
-  'status_analysing': '#4caf50',
-  'status_stop_analysis': '#ff5722'
-} %}
-
+{% import "_colors.html.j2" as colors %}
 
+{% if current_user.is_authenticated %}
+<link href="{{ url_for('static', filename='css/sidenav_fixed.css') }}" media="screen,projection" rel="stylesheet">
+{% endif %}
+<link href="{{ url_for('static', filename='css/nopaque_icons.css') }}" media="screen,projection" rel="stylesheet">
+<link href="{{ url_for('static', filename='css/nopaque.css') }}" media="screen,projection" rel="stylesheet">
 <style>
   .primary-color {background-color: {{ colors['primary'] }} !important;}
   .primary-color-border {border-color: {{ colors['primary'] }} !important;}
@@ -60,7 +32,6 @@
   .error-color-border {border-color: {{ colors['error'] }} !important;}
   .error-color-text {color: {{ colors['error'] }} !important;}
 
-  main {background-color: {{ colors['background'] }};}
   main .btn, main .btn-small, main .btn-large, main .btn-floating {background-color: {{ colors['secondary'] }};}
   main .btn:hover, main .btn-large:hover, main .btn-small:hover, main .btn-floating:hover {background-color: {{ colors['secondary_variant'] }};}
   main .pagination li.active {background-color: {{ colors['secondary'] }};}
diff --git a/web/app/templates/admin/_breadcrumbs.html.j2 b/web/app/templates/admin/_breadcrumbs.html.j2
index d27940c3..c4a64046 100644
--- a/web/app/templates/admin/_breadcrumbs.html.j2
+++ b/web/app/templates/admin/_breadcrumbs.html.j2
@@ -1,19 +1,18 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('.index') }}" target="_self">Administration</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.users') %}
-  <li class="tab"><a class="active" href="{{ url_for('.users') }}" target="_self">Users</a></li>
-  {% elif request.path == url_for('.user', user_id=user.id) %}
-  <li class="tab"><a href="{{ url_for('.users') }}" target="_self">Users</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a class="active" href="{{ url_for('.user', user_id=user.id) }}" target="_self">{{ user.username }}</a></li>
-  {% elif request.path == url_for('.edit_user', user_id=user.id) %}
-  <li class="tab"><a href="{{ url_for('.users') }}" target="_self">Users</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('.user', user_id=user.id) }}" target="_self">{{ user.username }}</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a class="active" href="{{ url_for('.edit_user', user_id=user.id) }}" target="_self">Edit</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('.index') }}" target="_self">Administration</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.users') %}
+<li class="tab"><a class="active" href="{{ url_for('.users') }}" target="_self">Users</a></li>
+{% elif request.path == url_for('.user', user_id=user.id) %}
+<li class="tab"><a href="{{ url_for('.users') }}" target="_self">Users</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a class="active" href="{{ url_for('.user', user_id=user.id) }}" target="_self">{{ user.username }}</a></li>
+{% elif request.path == url_for('.edit_user', user_id=user.id) %}
+<li class="tab"><a href="{{ url_for('.users') }}" target="_self">Users</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('.user', user_id=user.id) }}" target="_self">{{ user.username }}</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a class="active" href="{{ url_for('.edit_user', user_id=user.id) }}" target="_self">Edit</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/admin/edit_user.html.j2 b/web/app/templates/admin/edit_user.html.j2
index 5f441cf2..bd75147f 100644
--- a/web/app/templates/admin/edit_user.html.j2
+++ b/web/app/templates/admin/edit_user.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "admin/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'admin/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/admin/user.html.j2 b/web/app/templates/admin/user.html.j2
index 27d09d8c..ada3a55c 100644
--- a/web/app/templates/admin/user.html.j2
+++ b/web/app/templates/admin/user.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'admin/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "admin/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/admin/users.html.j2 b/web/app/templates/admin/users.html.j2
index afe282ff..19991a51 100644
--- a/web/app/templates/admin/users.html.j2
+++ b/web/app/templates/admin/users.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'admin/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "admin/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/auth/_breadcrumbs.html.j2 b/web/app/templates/auth/_breadcrumbs.html.j2
index 45eaca6f..2f46d9dc 100644
--- a/web/app/templates/auth/_breadcrumbs.html.j2
+++ b/web/app/templates/auth/_breadcrumbs.html.j2
@@ -1,15 +1,14 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.login') %}
-  <li class="tab"><a class="active" href="{{ url_for('.login') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.register') %}
-  <li class="tab"><a class="active" href="{{ url_for('.register') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.reset_password', token=token) %}
-  <li class="tab"><a class="active" href="{{ url_for('.reset_password', token=token) }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.reset_password_request') %}
-  <li class="tab"><a class="active" href="{{ url_for('.reset_password_request') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.unconfirmed') %}
-  <li class="tab"><a class="active" href="{{ url_for('.unconfirmed') }}" target="_self">{{ title }}</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.login') %}
+<li class="tab"><a class="active" href="{{ url_for('.login') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.register') %}
+<li class="tab"><a class="active" href="{{ url_for('.register') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.reset_password', token=token) %}
+<li class="tab"><a class="active" href="{{ url_for('.reset_password', token=token) }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.reset_password_request') %}
+<li class="tab"><a class="active" href="{{ url_for('.reset_password_request') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.unconfirmed') %}
+<li class="tab"><a class="active" href="{{ url_for('.unconfirmed') }}" target="_self">{{ title }}</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/auth/login.html.j2 b/web/app/templates/auth/login.html.j2
index c2133616..58fe45b3 100644
--- a/web/app/templates/auth/login.html.j2
+++ b/web/app/templates/auth/login.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "auth/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'auth/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block styles %}
 {{ super() }}
 <style>
diff --git a/web/app/templates/auth/register.html.j2 b/web/app/templates/auth/register.html.j2
index 111b36ad..e1c7186f 100644
--- a/web/app/templates/auth/register.html.j2
+++ b/web/app/templates/auth/register.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "auth/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'auth/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block styles %}
 {{ super() }}
 <style>
diff --git a/web/app/templates/auth/reset_password.html.j2 b/web/app/templates/auth/reset_password.html.j2
index ec9c6b18..376795e1 100644
--- a/web/app/templates/auth/reset_password.html.j2
+++ b/web/app/templates/auth/reset_password.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "auth/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'auth/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/auth/reset_password_request.html.j2 b/web/app/templates/auth/reset_password_request.html.j2
index 1f65091a..66d5776d 100644
--- a/web/app/templates/auth/reset_password_request.html.j2
+++ b/web/app/templates/auth/reset_password_request.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "auth/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'auth/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/auth/unconfirmed.html.j2 b/web/app/templates/auth/unconfirmed.html.j2
index d2b1e359..89d643ac 100644
--- a/web/app/templates/auth/unconfirmed.html.j2
+++ b/web/app/templates/auth/unconfirmed.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'auth/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "auth/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/base.html.j2 b/web/app/templates/base.html.j2
new file mode 100644
index 00000000..bafabe91
--- /dev/null
+++ b/web/app/templates/base.html.j2
@@ -0,0 +1,49 @@
+{% extends "materialize/base.html.j2" %}
+{% import "_colors.html.j2" as colors %}
+
+{% block html_attribs %} lang="en"{% endblock html_attribs %}
+
+{% block head %}
+{{ super() }}
+<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">
+{% endblock head %}
+
+{% block metas %}
+<meta charset="UTF-8">
+{{ super() }}
+{% endblock metas %}
+
+{% block title %}{{ title }}{% endblock title %}
+
+{% block styles %}
+{{ super() }}
+{% include "_styles.html.j2" %}
+{% endblock styles %}
+
+{% block navbar %}
+{% include "_navbar.html.j2" %}
+{% endblock navbar %}
+
+{% block sidenav %}
+{% if current_user.is_authenticated %}
+{% include "_sidenav.html.j2" %}
+{% endif %}
+{% endblock sidenav %}
+
+{% block main_attribs %} class="background-color"{% endblock main_attribs %}
+{% block main %}
+{% block page_content %}{% endblock page_content %}
+{% if current_user.is_authenticated %}
+{% include "_roadmap.html.j2" %}
+{% endif %}
+{% endblock main %}
+
+{% block footer_attribs %} class="page-footer primary-variant-color"{% endblock footer_attribs %}
+{% block footer %}
+{% include "_footer.html.j2" %}
+{% endblock footer %}
+
+{% block scripts %}
+{{ super() }}
+{% include "_scripts.html.j2" %}
+{% endblock scripts %}
diff --git a/web/app/templates/corpora/_breadcrumbs.html.j2 b/web/app/templates/corpora/_breadcrumbs.html.j2
index 04a72018..d91bc8c3 100644
--- a/web/app/templates/corpora/_breadcrumbs.html.j2
+++ b/web/app/templates/corpora/_breadcrumbs.html.j2
@@ -1,29 +1,28 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My corpora</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.add_corpus') %}
-  <li class="tab"><a class="active" href="{{ url_for('.add_corpus') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.import_corpus') %}
-  <li class="tab"><a class="active" href="{{ url_for('.import_corpus') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.corpus', corpus_id=corpus.id) %}
-  <li class="tab"><a class="active" href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
-  {% elif request.path == url_for('.analyse_corpus', corpus_id=corpus.id) %}
-  <li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a class="active" href="{{ url_for('.analyse_corpus', corpus_id=corpus.id) }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.add_corpus_file', corpus_id=corpus.id) %}
-  <li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id, _anchor='files') }}" target="_self">Corpus files</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a class="active" href="{{ url_for('.add_corpus_file', corpus_id=corpus.id) }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) %}
-  <li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id, _anchor='files') }}" target="_self">Corpus files</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a class="active" href="{{ url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) }}" target="_self">{{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My corpora</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.add_corpus') %}
+<li class="tab"><a class="active" href="{{ url_for('.add_corpus') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.import_corpus') %}
+<li class="tab"><a class="active" href="{{ url_for('.import_corpus') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.corpus', corpus_id=corpus.id) %}
+<li class="tab"><a class="active" href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
+{% elif request.path == url_for('.analyse_corpus', corpus_id=corpus.id) %}
+<li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a class="active" href="{{ url_for('.analyse_corpus', corpus_id=corpus.id) }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.add_corpus_file', corpus_id=corpus.id) %}
+<li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id, _anchor='files') }}" target="_self">Corpus files</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a class="active" href="{{ url_for('.add_corpus_file', corpus_id=corpus.id) }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) %}
+<li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id) }}" target="_self">{{ corpus.title }}</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('.corpus', corpus_id=corpus.id, _anchor='files') }}" target="_self">Corpus files</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a class="active" href="{{ url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) }}" target="_self">{{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/corpora/add_corpus.html.j2 b/web/app/templates/corpora/add_corpus.html.j2
index 0785ed11..a44bb990 100644
--- a/web/app/templates/corpora/add_corpus.html.j2
+++ b/web/app/templates/corpora/add_corpus.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/add_corpus_file.html.j2 b/web/app/templates/corpora/add_corpus_file.html.j2
index be3e9f71..da4bb27d 100644
--- a/web/app/templates/corpora/add_corpus_file.html.j2
+++ b/web/app/templates/corpora/add_corpus_file.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2
index 691517b5..7666f550 100644
--- a/web/app/templates/corpora/analyse_corpus.html.j2
+++ b/web/app/templates/corpora/analyse_corpus.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="row">
   <div class="col s12">
diff --git a/web/app/templates/corpora/corpus.html.j2 b/web/app/templates/corpora/corpus.html.j2
index 510f2091..c1ac05a1 100644
--- a/web/app/templates/corpora/corpus.html.j2
+++ b/web/app/templates/corpora/corpus.html.j2
@@ -1,11 +1,8 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/corpus_file.html.j2 b/web/app/templates/corpora/corpus_file.html.j2
index e5dc31eb..df474606 100644
--- a/web/app/templates/corpora/corpus_file.html.j2
+++ b/web/app/templates/corpora/corpus_file.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/import_corpus.html.j2 b/web/app/templates/corpora/import_corpus.html.j2
index 9531e844..68bfc186 100644
--- a/web/app/templates/corpora/import_corpus.html.j2
+++ b/web/app/templates/corpora/import_corpus.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/query_results/_breadcrumbs.html.j2 b/web/app/templates/corpora/query_results/_breadcrumbs.html.j2
index a42d1c6b..9fb0464a 100644
--- a/web/app/templates/corpora/query_results/_breadcrumbs.html.j2
+++ b/web/app/templates/corpora/query_results/_breadcrumbs.html.j2
@@ -1,13 +1,12 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='query-results') }}" target="_self">My query results</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.add_query_result') %}
-  <li class="tab"><a class="active" href="{{ url_for('.add_query_result') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.query_result', query_result_id=query_result.id) %}
-  <li class="tab"><a class="active" href="{{ url_for('.query_result', query_result_id=query_result.id) }}" target="_self">{{ query_result.title }}</a></li>
-  {% elif request.path == url_for('.inspect_query_result', query_result_id=query_result.id) %}
-  <li class="tab"><a class="active" href="{{ url_for('.inspect_query_result', query_result_id=query_result.id) }}" target="_self">{{ title }}</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('main.dashboard', _anchor='query-results') }}" target="_self">My query results</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.add_query_result') %}
+<li class="tab"><a class="active" href="{{ url_for('.add_query_result') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.query_result', query_result_id=query_result.id) %}
+<li class="tab"><a class="active" href="{{ url_for('.query_result', query_result_id=query_result.id) }}" target="_self">{{ query_result.title }}</a></li>
+{% elif request.path == url_for('.inspect_query_result', query_result_id=query_result.id) %}
+<li class="tab"><a class="active" href="{{ url_for('.inspect_query_result', query_result_id=query_result.id) }}" target="_self">{{ title }}</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/corpora/query_results/add_query_result.html.j2 b/web/app/templates/corpora/query_results/add_query_result.html.j2
index 04fbb6ef..61933816 100644
--- a/web/app/templates/corpora/query_results/add_query_result.html.j2
+++ b/web/app/templates/corpora/query_results/add_query_result.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/query_results/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/query_results/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/corpora/query_results/inspect.html.j2 b/web/app/templates/corpora/query_results/inspect.html.j2
index 405b6e2a..afc00f3b 100644
--- a/web/app/templates/corpora/query_results/inspect.html.j2
+++ b/web/app/templates/corpora/query_results/inspect.html.j2
@@ -1,11 +1,8 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/query_results/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/query_results/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="row">
   <div class="col s12">
diff --git a/web/app/templates/corpora/query_results/query_result.html.j2 b/web/app/templates/corpora/query_results/query_result.html.j2
index e3d772bb..51377129 100644
--- a/web/app/templates/corpora/query_results/query_result.html.j2
+++ b/web/app/templates/corpora/query_results/query_result.html.j2
@@ -1,11 +1,8 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "corpora/query_results/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'corpora/query_results/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/errors/403.html.j2 b/web/app/templates/errors/403.html.j2
index 0ebbea86..db286454 100644
--- a/web/app/templates/errors/403.html.j2
+++ b/web/app/templates/errors/403.html.j2
@@ -1,4 +1,4 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/errors/404.html.j2 b/web/app/templates/errors/404.html.j2
index 07692692..62006da9 100644
--- a/web/app/templates/errors/404.html.j2
+++ b/web/app/templates/errors/404.html.j2
@@ -1,4 +1,4 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/errors/413.html.j2 b/web/app/templates/errors/413.html.j2
index b711c42f..6e4e3e7e 100644
--- a/web/app/templates/errors/413.html.j2
+++ b/web/app/templates/errors/413.html.j2
@@ -1,4 +1,4 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/errors/500.html.j2 b/web/app/templates/errors/500.html.j2
index 2db380d9..5e2d3e87 100644
--- a/web/app/templates/errors/500.html.j2
+++ b/web/app/templates/errors/500.html.j2
@@ -1,4 +1,4 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/jobs/_breadcrumbs.html.j2 b/web/app/templates/jobs/_breadcrumbs.html.j2
index 42fcd3b7..5bad7de3 100644
--- a/web/app/templates/jobs/_breadcrumbs.html.j2
+++ b/web/app/templates/jobs/_breadcrumbs.html.j2
@@ -1,9 +1,8 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My jobs</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.job', job_id=job.id) %}
-  <li class="tab"><a class="active" href="{{ url_for('.job', job_id=job.id) }}" target="_self">{{ job.title }}</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My jobs</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.job', job_id=job.id) %}
+<li class="tab"><a class="active" href="{{ url_for('.job', job_id=job.id) }}" target="_self">{{ job.title }}</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/jobs/job.html.j2 b/web/app/templates/jobs/job.html.j2
index 51a2b8ed..2ac48754 100644
--- a/web/app/templates/jobs/job.html.j2
+++ b/web/app/templates/jobs/job.html.j2
@@ -1,11 +1,8 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "jobs/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block main_attribs %} class="service-scheme" data-service="{{ job.service }}"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'jobs/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/main/_breadcrumbs.html.j2 b/web/app/templates/main/_breadcrumbs.html.j2
index df01dda3..cd66b2ff 100644
--- a/web/app/templates/main/_breadcrumbs.html.j2
+++ b/web/app/templates/main/_breadcrumbs.html.j2
@@ -1,15 +1,14 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  {% if not (request.path == url_for('.index')) %}
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% endif %}
-  {% if request.path == url_for('.about_and_faq') %}
-  <li class="tab"><a class="active" href="{{ url_for('.about_and_faq') }}" target="_self">About and faq</a></li>
-  {% elif request.path == url_for('.dashboard') %}
-  <li class="tab"><a class="active" href="{{ url_for('.dashboard') }}" target="_self">Dashboard</a></li>
-  {% elif request.path == url_for('.news') %}
-  <li class="tab"><a class="active" href="{{ url_for('.news') }}" target="_self">News</a></li>
-  {% elif request.path == url_for('.terms_of_use') %}
-  <li class="tab"><a class="active" href="{{ url_for('.terms_of_use') }}" target="_self">Terms of use</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+{% if not (request.path == url_for('.index')) %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% endif %}
+{% if request.path == url_for('.about_and_faq') %}
+<li class="tab"><a class="active" href="{{ url_for('.about_and_faq') }}" target="_self">About and faq</a></li>
+{% elif request.path == url_for('.dashboard') %}
+<li class="tab"><a class="active" href="{{ url_for('.dashboard') }}" target="_self">Dashboard</a></li>
+{% elif request.path == url_for('.news') %}
+<li class="tab"><a class="active" href="{{ url_for('.news') }}" target="_self">News</a></li>
+{% elif request.path == url_for('.terms_of_use') %}
+<li class="tab"><a class="active" href="{{ url_for('.terms_of_use') }}" target="_self">Terms of use</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/main/about_and_faq.html.j2 b/web/app/templates/main/about_and_faq.html.j2
index cdae8dca..caea7e37 100644
--- a/web/app/templates/main/about_and_faq.html.j2
+++ b/web/app/templates/main/about_and_faq.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/main/dashboard.html.j2 b/web/app/templates/main/dashboard.html.j2
index 3ae228b8..f46b1101 100644
--- a/web/app/templates/main/dashboard.html.j2
+++ b/web/app/templates/main/dashboard.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/main/index.html.j2 b/web/app/templates/main/index.html.j2
index 6be76c8e..6165a4b3 100644
--- a/web/app/templates/main/index.html.j2
+++ b/web/app/templates/main/index.html.j2
@@ -1,10 +1,7 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="parallax-container" id="title">
   <div class="parallax">
diff --git a/web/app/templates/main/news.html.j2 b/web/app/templates/main/news.html.j2
index 6e00ed09..ca7f12a8 100644
--- a/web/app/templates/main/news.html.j2
+++ b/web/app/templates/main/news.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/main/privacy_policy.html.j2 b/web/app/templates/main/privacy_policy.html.j2
index dc8052ca..1b7c1c6d 100644
--- a/web/app/templates/main/privacy_policy.html.j2
+++ b/web/app/templates/main/privacy_policy.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/main/terms_of_use.html.j2 b/web/app/templates/main/terms_of_use.html.j2
index dba392a0..f84a2941 100644
--- a/web/app/templates/main/terms_of_use.html.j2
+++ b/web/app/templates/main/terms_of_use.html.j2
@@ -1,8 +1,5 @@
-{% extends "nopaque.html.j2" %}
-
-{% block nav_content %}
-{% include 'main/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
+{% extends "base.html.j2" %}
+{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block page_content %}
 <div class="container">
diff --git a/web/app/templates/nopaque.html.j2 b/web/app/templates/nopaque.html.j2
deleted file mode 100644
index dba9022e..00000000
--- a/web/app/templates/nopaque.html.j2
+++ /dev/null
@@ -1,222 +0,0 @@
-{% extends "materialize/base.html.j2" %}
-
-{% block html_attribs %} lang="en"{% endblock html_attribs %}
-
-{% block head %}
-{{ super() }}
-<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">
-{% endblock head %}
-
-{% block metas %}
-<meta charset="UTF-8">
-{{ super() }}
-{% endblock metas %}
-
-{% block title %}{{ title }}{% endblock title %}
-
-{% block styles %}
-{{ super() }}
-{% if current_user.is_authenticated %}
-<link href="{{ url_for('static', filename='css/sidenav_fixed.css') }}" media="screen,projection" rel="stylesheet">
-{% endif %}
-<link href="{{ url_for('static', filename='css/nopaque_icons.css') }}" media="screen,projection" rel="stylesheet">
-<link href="{{ url_for('static', filename='css/nopaque.css') }}" media="screen,projection" rel="stylesheet">
-{% include "_color.html.j2" %}
-{% endblock styles %}
-
-{% block navbar %}
-<div class="navbar-fixed">
-  <nav class="nav-extended">
-    <div class="nav-wrapper primary-color">
-      {% if current_user.is_authenticated %}
-      <a href="#" data-target="sidenav" class="sidenav-trigger"><i class="material-icons">menu</i></a>
-      {% endif %}
-      <a href="{{ url_for('main.index') }}" class="brand-logo" style="height: 100%; overflow: hidden;">
-        <img class="hide-on-small-only" src="{{ url_for('static', filename='images/nopaque_-_logo_name_slogan.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
-        <img class="hide-on-med-and-up" src="{{ url_for('static', filename='images/nopaque_-_logo.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
-      </a>
-      <ul class="right">
-        <li><a class="dropdown-trigger no-autoinit" data-target="nav-more-dropdown" href="#!" id="nav-more-dropdown-trigger"><i class="material-icons">more_vert</i></a></li>
-      </ul>
-    </div>
-    <div class="nav-content primary-variant-color">
-      {% block nav_content %}{% endblock nav_content %}
-      {% if current_user.is_authenticated %}
-      <a class="btn-floating btn-large halfway-fab modal-trigger pink tooltipped waves-effect waves-light" data-tooltip="Roadmap" href="#roadmap-modal"><i class="material-icons">explore</i></a>
-      {% endif %}
-    </div>
-  </nav>
-</div>
-
-<ul class="dropdown-content" id="nav-more-dropdown">
-  {% if current_user.is_authenticated %}
-  <li><a href="{{ url_for('settings.index') }}"><i class="material-icons left">settings</i>Settings</a></li>
-  <li class="divider" tabindex="-1"></li>
-  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
-  {% else %}
-  <li{% if request.path == url_for('auth.register') %} class="active"{% endif %}><a href="{{ url_for('auth.register') }}"><i class="material-icons left">assignment</i>Register</a></li>
-  <li{% if request.path == url_for('auth.login') %} class="active"{% endif %}><a href="{{ url_for('auth.login') }}"><i class="material-icons left">login</i>Log in</a></li>
-  {% endif %}
-</ul>
-{% endblock navbar %}
-
-{% block sidenav %}
-{% if current_user.is_authenticated %}
-<ul class="sidenav sidenav-fixed" id="sidenav">
-  <li>
-    <div class="user-view">
-      <div class="background primary-color"></div>
-      <span class="white-text name">{{ current_user.username }}</span>
-      <span class="white-text email">{{ current_user.email }}</span>
-    </div>
-  </li>
-  <li><a href="{{ url_for('main.index') }}">nopaque</a></li>
-  <li><a href="{{ url_for('main.news') }}"><i class="material-icons left">email</i>News</a></li>
-  <li><a href="#"><i class="material-icons">linear_scale</i>Workflow</a></li>
-  <li><a href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a></li>
-  <li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>My Corpora</a></li>
-  <li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icons">J</i>My Jobs</a></li>
-  <li><div class="divider"></div></li>
-  <li><a class="subheader">Processes & Services</a></li>
-  <li class="service-color service-color-border border-darken" data-service="file-setup" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='file_setup') }}"><i class="nopaque-icons service-icon" data-service="file-setup"></i>File setup</a></li>
-  <li class="service-color service-color-border border-darken" data-service="ocr" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='ocr') }}"><i class="nopaque-icons service-icon" data-service="ocr"></i>OCR</a></li>
-  <li class="service-color service-color-border border-darken" data-service="nlp" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='nlp') }}"><i class="nopaque-icons service-icon" data-service="nlp"></i>NLP</a></li>
-  <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='corpus_analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
-  <li><div class="divider"></div></li>
-  <li><a class="subheader">Account</a></li>
-  <li><a href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a></li>
-  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
-  {% if current_user.is_administrator() %}
-  <li><div class="divider"></div></li>
-  <li><a class="subheader">Administration</a></li>
-  <li><a href="{{ url_for('admin.index') }}"><i class="material-icons">build</i>Administration</a></li>
-  {% endif %}
-</ul>
-{% endif %}
-{% endblock sidenav %}
-
-{% block main %}
-{% block page_content %}{% endblock page_content %}
-{% if current_user.is_authenticated %}
-<div id="roadmap-modal" class="modal">
-  <div class="modal-content">
-    <h2>Roadmap</h2>
-    <p>The roadmap guides you through nopaque's workflow! If you have the necessary input fie formats, you can directly jump into the corresponding process. If not, you can use the roadmap to jump right to the preceding process.</p>
-    <ul class="tabs tabs-fixed-width">
-      <li class="tab"><a{%if request.path == url_for('services.service', service='file-setup') %} class="active"{% endif %} href="{{ url_for('services.service', service='file-setup') }}" target="_self">File setup</a></li>
-      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-      <li class="tab"><a{%if request.path == url_for('services.service', service='ocr') %} class="active"{% endif %} href="{{ url_for('services.service', service='ocr') }}" target="_self">OCR</a></li>
-      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-      <li class="tab"><a{%if request.path == url_for('services.service', service='nlp') %} class="active"{% endif %} href="{{ url_for('services.service', service='nlp') }}" target="_self">NLP</a></li>
-      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-      <li class="tab"><a{%if request.path == url_for('corpora.add_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.add_corpus') }}" target="_self">Add corpus</a></li>
-      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-      {% if corpus %}
-      <li class="tab"><a{%if request.path == url_for('corpora.add_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.add_corpus_file', corpus_id=corpus.id) }}" target="_self">Add corpus file(s)</a></li>
-      {% else %}
-      <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Add corpus file(s)</a></li>
-      {% endif %}
-      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-      {% if corpus %}
-      {% if corpus.files.all() %}
-      <li class="tab"><a{%if request.path == url_for('corpora.analyse_corpus', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}" target="_self">Corpus analysis</a></li>
-      {% else %}
-      <li class="tab disabled tooltipped" data-tooltip="Add at least one corpus file first"><a>Corpus analysis</a></li>
-      {% endif %}
-      {% else %}
-      <li class="tab disabled tooltipped" data-tooltip="Select a corpus first"><a>Corpus analysis</a></li>
-      {% endif %}
-    </ul>
-  </div>
-  <div class="modal-footer">
-    <a href="#!" class="modal-close waves-effect waves-green btn-flat">Close</a>
-  </div>
-</div>
-{% endif %}
-{% endblock main %}
-
-{% block footer_attribs %} class="page-footer primary-variant-color"{% endblock footer_attribs %}
-
-{% block footer %}
-<div class="container">
-  <div class="row">
-    <div class="col s6 m3">
-      <a href="https://www.dfg.de/">
-        <img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
-      </a>
-    </div>
-    <div class="col s6 m3 offset-m1 center-align">
-      <a href="https://www.uni-bielefeld.de/sfb1288/">
-        <img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_sfb_1288.png') }}">
-      </a>
-    </div>
-    <div class="col s12 m3 offset-m1">
-      <h5 class="white-text">Legal Notice</h5>
-      <ul>
-        <li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
-        <li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
-        <li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
-        <li></li>
-      </ul>
-    </div>
-  </div>
-</div>
-<div class="footer-copyright primary-color">
-  <div class="container">
-    <div class="row" style="margin-bottom: 0;">
-      <div class="col s12 m3">
-        <span>© 2020 Bielefeld University</span>
-      </div>
-      <div class="col s12 m9 right-align">
-        <a class="btn-small blue waves-effect waves-light" href="{{ url_for('main.about_and_faq') }}"><i class="left material-icons">info_outline</i>About and faq</a>
-        {% if config.NOPAQUE_CONTACT %}
-        <a class="btn-small pink waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Contact"><i class="left material-icons">rate_review</i>Contact</a>
-        <a class="btn-small green waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Feedback"><i class="left material-icons">feedback</i>Feedback</a>
-        {% endif %}
-        <a class="btn-small orange waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque"><i class="left material-icons">code</i>GitLab</a>
-      </div>
-    </div>
-  </div>
-</div>
-{% endblock footer %}
-
-{% block scripts %}
-{{ super() }}
-{% if current_user.setting_dark_mode %}
-<script src="{{ url_for('static', filename='js/darkreader.js') }}"></script>
-<script>
-  DarkReader.enable({brightness: 150, contrast: 100, sepia: 0});
-</script>
-{% endif %}
-<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
-<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
-<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
-<script src="{{ url_for('static', filename='js/nopaque/main.js') }}"></script>
-{% assets filters='rjsmin', output="js/nopaque/RessourceDisplays.min.compiled.js",
-          "js/nopaque/RessourceDisplays/RessourceDisplay.js",
-          "js/nopaque/RessourceDisplays/CorpusDisplay.js",
-          "js/nopaque/RessourceDisplays/JobDisplay.js" %}
-<script src="{{ ASSET_URL }}"></script>
-{% endassets %}
-{% assets filters='rjsmin', output="js/nopaque/RessourceLists.min.compiled.js",
-          "js/nopaque/RessourceLists/RessourceList.js",
-          "js/nopaque/RessourceLists/CorpusList.js",
-          "js/nopaque/RessourceLists/CorpusFileList.js",
-          "js/nopaque/RessourceLists/JobList.js",
-          "js/nopaque/RessourceLists/JobInputList.js",
-          "js/nopaque/RessourceLists/JobResultList.js",
-          "js/nopaque/RessourceLists/QueryResultList.js",
-          "js/nopaque/RessourceLists/UserList.js" %}
-<script src="{{ ASSET_URL }}"></script>
-{% endassets %}
-<script>
-  // Disable all option elements with no value
-  for (let optionElement of document.querySelectorAll('option[value=""]')) {optionElement.disabled = true;}
-  M.AutoInit();
-  M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
-  M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
-  nopaque.appClient = new AppClient({% if current_user.is_authenticated %}{{ current_user.id }}{% endif %});
-  nopaque.Forms.init();
-  for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {nopaque.appClient.flash(flashedMessage[1], flashedMessage[0]);}
-</script>
-{% endblock scripts %}
diff --git a/web/app/templates/services/_breadcrumbs.html.j2 b/web/app/templates/services/_breadcrumbs.html.j2
index 01cbbf4c..1496b2c9 100644
--- a/web/app/templates/services/_breadcrumbs.html.j2
+++ b/web/app/templates/services/_breadcrumbs.html.j2
@@ -1,15 +1,14 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a href="{{ url_for('main.index', _anchor='services') }}" target="_self">Processes & Services</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('.service', service='corpus_analysis') %}
-  <li class="tab"><a class="active" href="{{ url_for('.service', service='corpus_analysis') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.service', service='file-setup') %}
-  <li class="tab"><a class="active" href="{{ url_for('.service', service='file-setup') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.service', service='nlp') %}
-  <li class="tab"><a class="active" href="{{ url_for('.service', service='nlp') }}" target="_self">{{ title }}</a></li>
-  {% elif request.path == url_for('.service', service='ocr') %}
-  <li class="tab"><a class="active" href="{{ url_for('.service', service='ocr') }}" target="_self">{{ title }}</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a href="{{ url_for('main.index', _anchor='services') }}" target="_self">Processes & Services</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('.service', service='corpus_analysis') %}
+<li class="tab"><a class="active" href="{{ url_for('.service', service='corpus_analysis') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.service', service='file_setup') %}
+<li class="tab"><a class="active" href="{{ url_for('.service', service='file_setup') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.service', service='nlp') %}
+<li class="tab"><a class="active" href="{{ url_for('.service', service='nlp') }}" target="_self">{{ title }}</a></li>
+{% elif request.path == url_for('.service', service='ocr') %}
+<li class="tab"><a class="active" href="{{ url_for('.service', service='ocr') }}" target="_self">{{ title }}</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/services/corpus_analysis.html.j2 b/web/app/templates/services/corpus_analysis.html.j2
index 9e83a71f..c3c19568 100644
--- a/web/app/templates/services/corpus_analysis.html.j2
+++ b/web/app/templates/services/corpus_analysis.html.j2
@@ -1,11 +1,8 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
 
 {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'services/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/services/file_setup.html.j2 b/web/app/templates/services/file_setup.html.j2
index d6627861..65c633f0 100644
--- a/web/app/templates/services/file_setup.html.j2
+++ b/web/app/templates/services/file_setup.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="file-setup"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'services/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/services/nlp.html.j2 b/web/app/templates/services/nlp.html.j2
index 64d6269f..de95836f 100644
--- a/web/app/templates/services/nlp.html.j2
+++ b/web/app/templates/services/nlp.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="nlp"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'services/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/services/ocr.html.j2 b/web/app/templates/services/ocr.html.j2
index 4b641c09..12051b9c 100644
--- a/web/app/templates/services/ocr.html.j2
+++ b/web/app/templates/services/ocr.html.j2
@@ -1,12 +1,9 @@
-{% extends "nopaque.html.j2" %}
+{% extends "base.html.j2" %}
+{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
 {% block main_attribs %} class="service-scheme" data-service="ocr"{% endblock main_attribs %}
 
-{% block nav_content %}
-{% include 'services/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/settings/_breadcrumbs.html.j2 b/web/app/templates/settings/_breadcrumbs.html.j2
index ddcc4d83..0856463c 100644
--- a/web/app/templates/settings/_breadcrumbs.html.j2
+++ b/web/app/templates/settings/_breadcrumbs.html.j2
@@ -1,13 +1,12 @@
-<ul class="tabs tabs-transparent">
-  <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  <li class="tab"><a{%if request.path == url_for('settings.index') %} class="active"{% endif %} href="{{ url_for('settings.index') }}" target="_self">Settings</a></li>
-  <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
-  {% if request.path == url_for('settings.change_password') %}
-  <li class="tab"><a class="active" href="{{ url_for('settings.change_password') }}" target="_self">Change password</a></li>
-  {% elif request.path == url_for('settings.edit_general_settings') %}
-  <li class="tab"><a class="active" href="{{ url_for('settings.edit_general_settings') }}" target="_self">Edit general settings</a></li>
-  {% elif request.path == url_for('settings.edit_notification_settings') %}
-  <li class="tab"><a class="active" href="{{ url_for('settings.edit_notification_settings') }}" target="_self">Edit notification settings</a></li>
-  {% endif %}
-</ul>
+{% set breadcrumbs %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+<li class="tab"><a{%if request.path == url_for('settings.index') %} class="active"{% endif %} href="{{ url_for('settings.index') }}" target="_self">Settings</a></li>
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
+{% if request.path == url_for('settings.change_password') %}
+<li class="tab"><a class="active" href="{{ url_for('settings.change_password') }}" target="_self">Change password</a></li>
+{% elif request.path == url_for('settings.edit_general_settings') %}
+<li class="tab"><a class="active" href="{{ url_for('settings.edit_general_settings') }}" target="_self">Edit general settings</a></li>
+{% elif request.path == url_for('settings.edit_notification_settings') %}
+<li class="tab"><a class="active" href="{{ url_for('settings.edit_notification_settings') }}" target="_self">Edit notification settings</a></li>
+{% endif %}
+{% endset %}
diff --git a/web/app/templates/settings/change_password.html.j2 b/web/app/templates/settings/change_password.html.j2
index c4f76cbd..da71ec3f 100644
--- a/web/app/templates/settings/change_password.html.j2
+++ b/web/app/templates/settings/change_password.html.j2
@@ -1,10 +1,7 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
+{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'settings/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/settings/edit_general_settings.html.j2 b/web/app/templates/settings/edit_general_settings.html.j2
index f0e7953d..19f487e7 100644
--- a/web/app/templates/settings/edit_general_settings.html.j2
+++ b/web/app/templates/settings/edit_general_settings.html.j2
@@ -1,10 +1,7 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
+{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'settings/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
diff --git a/web/app/templates/settings/edit_notification_settings.html.j2 b/web/app/templates/settings/edit_notification_settings.html.j2
index 64a695c3..74d98ba9 100644
--- a/web/app/templates/settings/edit_notification_settings.html.j2
+++ b/web/app/templates/settings/edit_notification_settings.html.j2
@@ -1,10 +1,7 @@
-{% extends 'nopaque.html.j2' %}
+{% extends "base.html.j2" %}
+{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
 {% import "materialize/wtf.html.j2" as wtf %}
 
-{% block nav_content %}
-{% include 'settings/_breadcrumbs.html.j2' %}
-{% endblock nav_content %}
-
 {% block page_content %}
 <div class="container">
   <div class="row">
-- 
GitLab