diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0967ef424bce6791893e9a57bb952f80fd536e93..c2a8567eeb3aa10c172abd83c00915f76c89762a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1 +1,22 @@
-{}
+{
+    "editor.rulers": [79],
+    "files.insertFinalNewline": true,
+    "[css]": {
+        "editor.tabSize": 2
+    },
+    "[scss]": {
+        "editor.tabSize": 2
+    },
+    "[html]": {
+        "editor.tabSize": 2
+    },
+    "[javascript]": {
+        "editor.tabSize": 2
+    },
+    "[jinja-html]": {
+        "editor.tabSize": 2
+    },
+    "[jinja-js]": {
+        "editor.tabSize": 2
+    },
+}
diff --git a/app/__init__.py b/app/__init__.py
index cc747a89846a837f77acfba87bafd127b999c25f..dcb58e47b9943b4efcc77f9bc8f5ba060f9b0d78 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -54,6 +54,9 @@ def create_app(config: Config = Config) -> Flask:
     scheduler.init_app(app)
     socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])  # noqa
 
+    from .errors import init_app as init_error_handlers
+    init_error_handlers(app)
+
     from .admin import bp as admin_blueprint
     app.register_blueprint(admin_blueprint, url_prefix='/admin')
 
@@ -69,9 +72,6 @@ def create_app(config: Config = Config) -> Flask:
     from .corpora import bp as corpora_blueprint
     app.register_blueprint(corpora_blueprint, url_prefix='/corpora')
 
-    from .errors import bp as errors_blueprint
-    app.register_blueprint(errors_blueprint)
-
     from .jobs import bp as jobs_blueprint
     app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
 
diff --git a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py
index 705fbdb46bc2fb3ce5a080b2e5ed46dbf742b54f..f4c4ca799bec6e42ca651f8f740b685c8c0b9bc5 100644
--- a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py
+++ b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py
@@ -39,8 +39,6 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
 @permission_required('CONTRIBUTE')
 @content_negotiation(consumes='application/json', produces='application/json')
 def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id):
-    # body: jsonify({'is_public': True})
-    # body: jsonify(False)
     is_public = request.json
     if not isinstance(is_public, bool):
         abort(400)
diff --git a/app/corpora/__init__.py b/app/corpora/__init__.py
index 83cecec56186293b6c43ed147cb358c99224acca..f39ca22bcb8acf6528737e8cc66cd8864ac98b44 100644
--- a/app/corpora/__init__.py
+++ b/app/corpora/__init__.py
@@ -2,4 +2,10 @@ from flask import Blueprint
 
 
 bp = Blueprint('corpora', __name__)
-from . import cqi_over_socketio, routes  # noqa
+from . import cqi_over_socketio, routes, json_routes  # noqa
+
+from .files import bp as files_bp
+bp.register_blueprint(files_bp, url_prefix='<hashid:corpus_id>/files')
+
+from .followers import bp as followers_bp
+bp.register_blueprint(followers_bp, url_prefix='<hashid:corpus_id>/followers')
diff --git a/app/corpora/files/__init__.py b/app/corpora/files/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f47a450e713842199c04457a3110a87ab6ac384
--- /dev/null
+++ b/app/corpora/files/__init__.py
@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+
+TEMPLATE_FOLDER = 'corpora/files'
+
+
+bp = Blueprint('files', __name__)
+from . import routes, json_routes
diff --git a/app/corpora/files/forms.py b/app/corpora/files/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5b0f0c86492471840b76826897c131584723de1
--- /dev/null
+++ b/app/corpora/files/forms.py
@@ -0,0 +1,54 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileRequired
+from wtforms import (
+    StringField,
+    SubmitField,
+    ValidationError,
+    IntegerField
+)
+from wtforms.validators import InputRequired, Length
+
+
+class CorpusFileBaseForm(FlaskForm):
+    author = StringField(
+        'Author',
+        validators=[InputRequired(), Length(max=255)]
+    )
+    publishing_year = IntegerField(
+        'Publishing year',
+        validators=[InputRequired()]
+    )
+    title = StringField(
+        'Title',
+        validators=[InputRequired(), Length(max=255)]
+    )
+    address = StringField('Adress', validators=[Length(max=255)])
+    booktitle = StringField('Booktitle', validators=[Length(max=255)])
+    chapter = StringField('Chapter', validators=[Length(max=255)])
+    editor = StringField('Editor', validators=[Length(max=255)])
+    institution = StringField('Institution', validators=[Length(max=255)])
+    journal = StringField('Journal', validators=[Length(max=255)])
+    pages = StringField('Pages', validators=[Length(max=255)])
+    publisher = StringField('Publisher', validators=[Length(max=255)])
+    school = StringField('School', validators=[Length(max=255)])
+    submit = SubmitField()
+
+
+class CreateCorpusFileForm(CorpusFileBaseForm):
+    vrt = FileField('File', validators=[FileRequired()])
+
+    def __init__(self, *args, **kwargs):
+        if 'prefix' not in kwargs:
+            kwargs['prefix'] = 'create-corpus-file-form'
+        super().__init__(*args, **kwargs)
+
+    def validate_vrt(self, field):
+        if not field.data.filename.lower().endswith('.vrt'):
+            raise ValidationError('VRT files only!')
+
+
+class UpdateCorpusFileForm(CorpusFileBaseForm):
+    def __init__(self, *args, **kwargs):
+        if 'prefix' not in kwargs:
+            kwargs['prefix'] = 'update-corpus-file-form'
+        super().__init__(*args, **kwargs)
diff --git a/app/corpora/files/json_routes.py b/app/corpora/files/json_routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..cddf4642ac9d901a4d80e76566190d222781c77e
--- /dev/null
+++ b/app/corpora/files/json_routes.py
@@ -0,0 +1,42 @@
+from flask import current_app, jsonify
+from flask_login import login_required
+from threading import Thread
+from app import db
+from app.decorators import content_negotiation
+from app.models import CorpusFile
+from ..decorators import corpus_follower_permission_required
+from . import bp
+
+
+##############################################################################
+# IMPORTANT NOTE: These routes are prefixed by the blueprint                 #
+#                 Prefix: <hashid:corpus_id>/files                           #
+#                 This implies that the corpus_id is always in the kwargs of #
+#                 a route that is registered to this blueprint.              #
+##############################################################################
+
+
+@bp.route('/<hashid:corpus_file_id>', methods=['DELETE'])
+@login_required
+@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
+@content_negotiation(produces='application/json')
+def delete_corpus_file(corpus_id, corpus_file_id):
+    def _delete_corpus_file(app, corpus_file_id):
+        with app.app_context():
+            corpus_file = CorpusFile.query.get(corpus_file_id)
+            corpus_file.delete()
+            db.session.commit()
+
+    corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
+    thread = Thread(
+        target=_delete_corpus_file,
+        args=(current_app._get_current_object(), corpus_file.id)
+    )
+    thread.start()
+    response_data = {
+        'message': f'Corpus File "{corpus_file.title}" marked for deletion',
+        'category': 'corpus'
+    }
+    response = jsonify(response_data)
+    response.status_code = 202
+    return response
diff --git a/app/corpora/files/routes.py b/app/corpora/files/routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..7715f4174d7905783fdcdf605153b1d4585ecb47
--- /dev/null
+++ b/app/corpora/files/routes.py
@@ -0,0 +1,101 @@
+from flask import (
+    abort,
+    flash,
+    Markup,
+    redirect,
+    render_template,
+    send_from_directory
+)
+from flask_login import current_user, login_required
+import os
+from app import db
+from app.models import Corpus, CorpusFile, CorpusStatus
+from ..decorators import corpus_follower_permission_required
+from . import bp, TEMPLATE_FOLDER
+from .forms import CreateCorpusFileForm, UpdateCorpusFileForm
+
+
+##############################################################################
+# IMPORTANT NOTE: These routes are prefixed by the blueprint                 #
+#                 Prefix: <hashid:corpus_id>/files                           #
+#                 This implies that the corpus_id is always in the kwargs of #
+#                 a route that is registered to this blueprint.              #
+##############################################################################
+
+
+@bp.route('/create', methods=['GET', 'POST'])
+@login_required
+@corpus_follower_permission_required('ADD_CORPUS_FILE')
+def create_corpus_file(corpus_id):
+    corpus = Corpus.query.get_or_404(corpus_id)
+    form = CreateCorpusFileForm()
+    if form.is_submitted():
+        if not form.validate():
+            response = {'errors': form.errors}
+            return response, 400
+        try:
+            corpus_file = CorpusFile.create(
+                form.vrt.data,
+                address=form.address.data,
+                author=form.author.data,
+                booktitle=form.booktitle.data,
+                chapter=form.chapter.data,
+                editor=form.editor.data,
+                institution=form.institution.data,
+                journal=form.journal.data,
+                pages=form.pages.data,
+                publisher=form.publisher.data,
+                publishing_year=form.publishing_year.data,
+                school=form.school.data,
+                title=form.title.data,
+                mimetype='application/vrt+xml',
+                corpus=corpus
+            )
+        except (AttributeError, OSError):
+            abort(500)
+        corpus.status = CorpusStatus.UNPREPARED
+        db.session.commit()
+        flash(f'Corpus File "{corpus_file.filename}" added', category='corpus')
+        return '', 201, {'Location': corpus.url}
+    return render_template(
+        f'{TEMPLATE_FOLDER}/create_corpus_file.html.j2',
+        corpus=corpus,
+        form=form,
+        title='Add corpus file'
+    )
+
+
+@bp.route('/<hashid:corpus_file_id>', methods=['GET', 'POST'])
+@login_required
+@corpus_follower_permission_required('UPDATE_CORPUS_FILE')
+def corpus_file(corpus_id, corpus_file_id):
+    corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
+    form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
+    if form.validate_on_submit():
+        form.populate_obj(corpus_file)
+        if db.session.is_modified(corpus_file):
+            corpus_file.corpus.status = CorpusStatus.UNPREPARED
+            db.session.commit()
+            flash(f'Corpus file "{corpus_file.filename}" updated', category='corpus')
+        return redirect(corpus_file.corpus.url)
+    return render_template(
+        f'{TEMPLATE_FOLDER}/corpus_file.html.j2',
+        corpus=corpus_file.corpus,
+        corpus_file=corpus_file,
+        form=form,
+        title='Edit corpus file'
+    )
+
+
+@bp.route('/<hashid:corpus_file_id>/download')
+@login_required
+@corpus_follower_permission_required('VIEW')
+def download_corpus_file(corpus_id, corpus_file_id):
+    corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
+    return send_from_directory(
+        os.path.dirname(corpus_file.path),
+        os.path.basename(corpus_file.path),
+        as_attachment=True,
+        attachment_filename=corpus_file.filename,
+        mimetype=corpus_file.mimetype
+    )
diff --git a/app/corpora/followers/__init__.py b/app/corpora/followers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1e6d14e80602669b7762857ff9d1fd3ee5bc15d
--- /dev/null
+++ b/app/corpora/followers/__init__.py
@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+
+bp = Blueprint('followers', __name__)
+from . import json_routes
diff --git a/app/corpora/followers/json_routes.py b/app/corpora/followers/json_routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..990717313f02ef02249f7c9599380e53289e381e
--- /dev/null
+++ b/app/corpora/followers/json_routes.py
@@ -0,0 +1,87 @@
+from flask import abort, jsonify, request
+from flask_login import current_user, login_required
+from app import db
+from app.decorators import content_negotiation
+from app.models import (
+    Corpus,
+    CorpusFollowerAssociation,
+    CorpusFollowerRole,
+    User
+)
+from ..decorators import corpus_owner_or_admin_required
+from . import bp
+
+
+##############################################################################
+# IMPORTANT NOTE: These routes are prefixed by the blueprint                 #
+#                 Prefix: <hashid:corpus_id>/followers                       #
+#                 This implies that the corpus_id is always in the kwargs of #
+#                 a route that is registered to this blueprint.              #
+##############################################################################
+
+
+@bp.route('', methods=['POST'])
+@login_required
+@corpus_owner_or_admin_required
+@content_negotiation(consumes='application/json', produces='application/json')
+def create_corpus_followers(corpus_id):
+    usernames = request.json
+    if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
+        abort(400)
+    corpus = Corpus.query.get_or_404(corpus_id)
+    for username in usernames:
+        user = User.query.filter_by(username=username, is_public=True).first_or_404()
+        user.follow_corpus(corpus)
+    db.session.commit()
+    resonse_data = {
+        'message': f'Users are now following "{corpus.title}"',
+        'category': 'corpus'
+    }
+    response = jsonify(resonse_data)
+    response.status_code = 200
+    return response
+
+
+@bp.route('/<hashid:follower_id>/role', methods=['PUT'])
+@login_required
+@corpus_owner_or_admin_required
+@content_negotiation(consumes='application/json', produces='application/json')
+def update_corpus_follower_role(corpus_id, follower_id):
+    role_name = request.json
+    if not isinstance(role_name, str):
+        abort(400)
+    cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
+    if cfr is None:
+        abort(400)
+    cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
+    cfa.role = cfr
+    db.session.commit()
+    resonse_data = {
+        'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
+        'category': 'corpus'
+    }
+    response = jsonify(resonse_data)
+    response.status_code = 200
+    return response
+
+
+@bp.route('/<hashid:follower_id>', methods=['DELETE'])
+@login_required
+@content_negotiation(produces='application/json')
+def delete_corpus_follower(corpus_id, follower_id):
+    corpus = Corpus.query.get_or_404(corpus_id)
+    follower = User.query.get_or_404(follower_id)
+    if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
+        abort(403)
+    if not follower.is_following_corpus(corpus):
+        abort(409)
+    follower.unfollow_corpus(corpus)
+    db.session.commit()
+    response_data = {
+        'message': \
+            f'"{follower.username}" is not following "{corpus.title}" anymore',
+        'category': 'corpus'
+    }
+    response = jsonify(response_data)
+    response.status_code = 200
+    return response
diff --git a/app/corpora/forms.py b/app/corpora/forms.py
index 8403e621932deac8779a6f1b4fc27f9322984a29..36ee34c8ccc43c754f46b8d5d98bba53e1fd139e 100644
--- a/app/corpora/forms.py
+++ b/app/corpora/forms.py
@@ -1,13 +1,5 @@
 from flask_wtf import FlaskForm
-from flask_wtf.file import FileField, FileRequired
-from wtforms import (
-    BooleanField,
-    StringField,
-    SubmitField,
-    TextAreaField,
-    ValidationError,
-    IntegerField
-)
+from wtforms import StringField, SubmitField, TextAreaField
 from wtforms.validators import InputRequired, Length
 
 
@@ -34,50 +26,5 @@ class UpdateCorpusForm(CorpusBaseForm):
         super().__init__(*args, **kwargs)
 
 
-class CorpusFileBaseForm(FlaskForm):
-    author = StringField(
-        'Author',
-        validators=[InputRequired(), Length(max=255)]
-    )
-    publishing_year = IntegerField(
-        'Publishing year',
-        validators=[InputRequired()]
-    )
-    title = StringField(
-        'Title',
-        validators=[InputRequired(), Length(max=255)]
-    )
-    address = StringField('Adress', validators=[Length(max=255)])
-    booktitle = StringField('Booktitle', validators=[Length(max=255)])
-    chapter = StringField('Chapter', validators=[Length(max=255)])
-    editor = StringField('Editor', validators=[Length(max=255)])
-    institution = StringField('Institution', validators=[Length(max=255)])
-    journal = StringField('Journal', validators=[Length(max=255)])
-    pages = StringField('Pages', validators=[Length(max=255)])
-    publisher = StringField('Publisher', validators=[Length(max=255)])
-    school = StringField('School', validators=[Length(max=255)])
-    submit = SubmitField()
-
-
-class CreateCorpusFileForm(CorpusFileBaseForm):
-    vrt = FileField('File', validators=[FileRequired()])
-
-    def __init__(self, *args, **kwargs):
-        if 'prefix' not in kwargs:
-            kwargs['prefix'] = 'create-corpus-file-form'
-        super().__init__(*args, **kwargs)
-
-    def validate_vrt(self, field):
-        if not field.data.filename.lower().endswith('.vrt'):
-            raise ValidationError('VRT files only!')
-
-
-class UpdateCorpusFileForm(CorpusFileBaseForm):
-    def __init__(self, *args, **kwargs):
-        if 'prefix' not in kwargs:
-            kwargs['prefix'] = 'update-corpus-file-form'
-        super().__init__(*args, **kwargs)
-
-
 class ImportCorpusForm(FlaskForm):
     pass
diff --git a/app/corpora/json_routes.py b/app/corpora/json_routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..0494e1e5e81a3888b47441010bfe84f620a369d8
--- /dev/null
+++ b/app/corpora/json_routes.py
@@ -0,0 +1,130 @@
+from datetime import datetime
+from flask import (
+    abort,
+    current_app,
+    jsonify,
+    request,
+    url_for
+)
+from flask_login import current_user, login_required
+from threading import Thread
+from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
+from app import db, hashids
+from app.decorators import content_negotiation
+from app.models import Corpus, CorpusFollowerRole
+from . import bp
+
+
+@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
+@login_required
+@corpus_owner_or_admin_required
+@content_negotiation(produces='application/json')
+def delete_corpus(corpus_id):
+    def _delete_corpus(app, corpus_id):
+        with app.app_context():
+            corpus = Corpus.query.get(corpus_id)
+            corpus.delete()
+            db.session.commit()
+
+    corpus = Corpus.query.get_or_404(corpus_id)
+    thread = Thread(
+        target=_delete_corpus,
+        args=(current_app._get_current_object(), corpus.id)
+    )
+    thread.start()
+    response_data = {
+        'message': f'Corpus "{corpus.title}" marked for deletion',
+        'category': 'corpus'
+    }
+    response = jsonify(response_data)
+    response.status_code = 200
+    return response
+
+
+@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
+@login_required
+@corpus_owner_or_admin_required
+@content_negotiation(produces='application/json')
+def build_corpus(corpus_id):
+    def _build_corpus(app, corpus_id):
+        with app.app_context():
+            corpus = Corpus.query.get(corpus_id)
+            corpus.build()
+            db.session.commit()
+
+    print(corpus_id)
+    corpus = Corpus.query.get_or_404(corpus_id)
+    if len(corpus.files.all()) == 0:
+        abort(409)
+    thread = Thread(
+        target=_build_corpus,
+        args=(current_app._get_current_object(), corpus_id)
+    )
+    thread.start()
+    response_data = {
+        'message': f'Corpus "{corpus.title}" marked for building',
+        'category': 'corpus'
+    }
+    response = jsonify(response_data)
+    response.status_code = 202
+    return response
+
+
+@bp.route('/<hashid:corpus_id>/generate-share-link', methods=['POST'])
+@login_required
+@corpus_follower_permission_required('GENERATE_SHARE_LINK')
+@content_negotiation(consumes='application/json', produces='application/json')
+def generate_corpus_share_link(corpus_id):
+    corpus_hashid = hashids.encode(corpus_id)
+    data = request.json
+    if not isinstance(data, dict):
+        abort(400)
+    expiration = data.get('expiration')
+    if not isinstance(expiration, str):
+        abort(400)
+    role_name = data.get('role')
+    if not isinstance(role_name, str):
+        abort(400)
+    expiration_date = datetime.strptime(expiration, '%b %d, %Y')
+    cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
+    if cfr is None:
+        abort(400)
+    token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date)
+    corpus_share_link = url_for(
+        'corpora.follow_corpus',
+        corpus_id=corpus_id,
+        token=token,
+        _external=True
+    )
+    response_data = {
+        'message': 'Corpus share link generated',
+        'category': 'corpus',
+        'corpusShareLink': corpus_share_link
+    }
+    response = jsonify(response_data)
+    response.status_code = 200
+    return response
+    
+
+
+@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
+@login_required
+@corpus_owner_or_admin_required
+@content_negotiation(consumes='application/json', produces='application/json')
+def update_corpus_is_public(corpus_id):
+    is_public = request.json
+    if not isinstance(is_public, bool):
+        abort(400)
+    corpus = Corpus.query.get_or_404(corpus_id)
+    corpus.is_public = is_public
+    db.session.commit()
+    response_data = {
+        'message': (
+            f'Corpus "{corpus.title}" is now'
+            f' {"public" if is_public else "private"}'
+        ),
+        'category': 'corpus'
+    }
+    response = jsonify(response_data)
+    response.status_code = 200
+    return response
diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index 148f5775aa40401ceb272d7d519bf22a43e3b51a..5484fb50af31a6a80984eeffa475aa1e02cc4953 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -1,36 +1,21 @@
-from datetime import datetime
 from flask import (
     abort,
-    current_app,
     flash,
-    jsonify,
     Markup,
     redirect,
     render_template,
-    request,
-    send_from_directory,
     url_for
 )
 from flask_login import current_user, login_required
-from threading import Thread
-import os
-from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
-from app import db, hashids
-from app.decorators import content_negotiation
+from .decorators import corpus_follower_permission_required
+from app import db
 from app.models import (
     Corpus,
-    CorpusFile,
     CorpusFollowerAssociation,
     CorpusFollowerRole,
-    CorpusStatus,
-    User
 )
 from . import bp
-from .forms import (
-    CreateCorpusFileForm,
-    CreateCorpusForm,
-    UpdateCorpusFileForm
-)
+from .forms import CreateCorpusForm
 
 
 @bp.route('/create', methods=['GET', 'POST'])
@@ -47,10 +32,7 @@ def create_corpus():
         except OSError:
             abort(500)
         db.session.commit()
-        message = Markup(
-            f'Corpus "<a href="{corpus.url}">{corpus.title}</a>" created'
-        )
-        flash(message, 'corpus')
+        flash(f'Corpus "{corpus.title}" created', 'corpus')
         return redirect(corpus.url)
     return render_template(
         'corpora/create_corpus.html.j2',
@@ -59,29 +41,12 @@ def create_corpus():
     )
 
 
-@bp.route('/public')
-@login_required
-def public_corpora():
-    corpora = [
-        c.to_json_serializeable()
-        for c in Corpus.query.filter(Corpus.is_public == True).all()
-    ]
-    return render_template(
-        'corpora/public_corpora.html.j2',
-        corpora=corpora,
-        title='Corpora'
-    )
-
-
-##############################################################################
-# Corpus                                                                     #
-##############################################################################
-#region corpus
 @bp.route('/<hashid:corpus_id>')
 @login_required
 def corpus(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
     corpus_follower_roles = CorpusFollowerRole.query.all()
+    # TODO: Add URL query option to toggle view
     if corpus.user == current_user or current_user.is_administrator():
         return render_template(
             'corpora/corpus.html.j2',
@@ -122,7 +87,7 @@ def follow_corpus(corpus_id, token):
     corpus = Corpus.query.get_or_404(corpus_id)
     if current_user.follow_corpus_by_token(token):
         db.session.commit()
-        flash(f'You are following {corpus.title} now', category='corpus')
+        flash(f'You are following "{corpus.title}" now', category='corpus')
         return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
     abort(403)
 
@@ -137,309 +102,3 @@ def import_corpus():
 @login_required
 def export_corpus(corpus_id):
     abort(503)
-
-
-#region json-routes
-@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
-@login_required
-@corpus_owner_or_admin_required
-@content_negotiation(produces='application/json')
-def delete_corpus(corpus_id):
-    def _delete_corpus(app, corpus_id):
-        with app.app_context():
-            corpus = Corpus.query.get(corpus_id)
-            corpus.delete()
-            db.session.commit()
-
-    corpus = Corpus.query.get_or_404(corpus_id)
-    thread = Thread(
-        target=_delete_corpus,
-        args=(current_app._get_current_object(), corpus.id)
-    )
-    thread.start()
-    response_data = {
-        'message': f'Corpus "{corpus.title}" marked for deletion',
-        'category': 'corpus'
-    }
-    response = jsonify(response_data)
-    response.status_code = 200
-    return response
-
-
-@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
-@login_required
-@corpus_owner_or_admin_required
-@content_negotiation(produces='application/json')
-def build_corpus(corpus_id):
-    def _build_corpus(app, corpus_id):
-        with app.app_context():
-            corpus = Corpus.query.get(corpus_id)
-            corpus.build()
-            db.session.commit()
-
-    corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.user == current_user or current_user.is_administrator()):
-        abort(403)
-    if len(corpus.files.all()) == 0:
-        abort(409)
-    thread = Thread(
-        target=_build_corpus,
-        args=(current_app._get_current_object(), corpus_id)
-    )
-    thread.start()
-    response_data = {
-        'message': f'Corpus "{corpus.title}" marked for building',
-        'category': 'corpus'
-    }
-    response = jsonify(response_data)
-    response.status_code = 202
-    return response
-
-
-@bp.route('/<hashid:corpus_id>/generate-corpus-share-link', methods=['POST'])
-@login_required
-@corpus_follower_permission_required('GENERATE_SHARE_LINK')
-@content_negotiation(consumes='application/json', produces='application/json')
-def generate_corpus_share_link(corpus_id):
-    corpus_hashid = hashids.encode(corpus_id)
-    data = request.json
-    if not isinstance(data, dict):
-        abort(400)
-    expiration = data.get('expiration')
-    if not isinstance(expiration, str):
-        abort(400)
-    role_name = data.get('role')
-    if not isinstance(role_name, str):
-        abort(400)
-    expiration_date = datetime.strptime(expiration, '%b %d, %Y')
-    cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
-    if cfr is None:
-        abort(400)
-    token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date)
-    corpus_share_link = url_for(
-        'corpora.follow_corpus',
-        corpus_id=corpus_id,
-        token=token,
-        _external=True
-    )
-    response_data = {
-        'message': 'Corpus share link generated',
-        'category': 'corpus',
-        'corpusShareLink': corpus_share_link
-    }
-    response = jsonify(response_data)
-    response.status_code = 200
-    return response
-    
-
-
-@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
-@login_required
-@corpus_owner_or_admin_required
-@content_negotiation(consumes='application/json', produces='application/json')
-def update_corpus_is_public(corpus_id):
-    is_public = request.json
-    if not isinstance(is_public, bool):
-        abort(400)
-    corpus = Corpus.query.get_or_404(corpus_id)
-    corpus.is_public = is_public
-    db.session.commit()
-    response_data = {
-        'message': (
-            f'Corpus "{corpus.title}" is now'
-            f' {"public" if is_public else "private"}'
-        ),
-        'category': 'corpus'
-    }
-    response = jsonify(response_data)
-    response.status_code = 200
-    return response
-#endregion json-routes
-#endregion corpus
-
-
-##############################################################################
-# Corpus/Files                                                               #
-##############################################################################
-#region files
-@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
-@login_required
-@corpus_follower_permission_required('ADD_CORPUS_FILE')
-def create_corpus_file(corpus_id):
-    corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.user == current_user or current_user.is_administrator()):
-        abort(403)
-    form = CreateCorpusFileForm()
-    if form.is_submitted():
-        if not form.validate():
-            response = {'errors': form.errors}
-            return response, 400
-        try:
-            corpus_file = CorpusFile.create(
-                form.vrt.data,
-                address=form.address.data,
-                author=form.author.data,
-                booktitle=form.booktitle.data,
-                chapter=form.chapter.data,
-                editor=form.editor.data,
-                institution=form.institution.data,
-                journal=form.journal.data,
-                pages=form.pages.data,
-                publisher=form.publisher.data,
-                publishing_year=form.publishing_year.data,
-                school=form.school.data,
-                title=form.title.data,
-                mimetype='application/vrt+xml',
-                corpus=corpus
-            )
-        except (AttributeError, OSError):
-            abort(500)
-        corpus.status = CorpusStatus.UNPREPARED
-        db.session.commit()
-        message = Markup(
-            'Corpus file'
-            f'"<a href="{corpus_file.url}">{corpus_file.filename}</a>" added'
-        )
-        flash(message, category='corpus')
-        return {}, 201, {'Location': corpus.url}
-    return render_template(
-        'corpora/create_corpus_file.html.j2',
-        corpus=corpus,
-        form=form,
-        title='Add corpus file'
-    )
-
-
-@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
-@login_required
-@corpus_follower_permission_required('UPDATE_CORPUS_FILE')
-def corpus_file(corpus_id, corpus_file_id):
-    corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
-    form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
-    if form.validate_on_submit():
-        form.populate_obj(corpus_file)
-        if db.session.is_modified(corpus_file):
-            corpus_file.corpus.status = CorpusStatus.UNPREPARED
-            db.session.commit()
-            message = Markup(f'Corpus file "<a href="{corpus_file.url}">{corpus_file.filename}</a>" updated')
-            flash(message, category='corpus')
-        return redirect(corpus_file.corpus.url)
-    return render_template(
-        'corpora/corpus_file.html.j2',
-        corpus=corpus_file.corpus,
-        corpus_file=corpus_file,
-        form=form,
-        title='Edit corpus file'
-    )
-
-
-@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
-@login_required
-@corpus_follower_permission_required('VIEW')
-def download_corpus_file(corpus_id, corpus_file_id):
-    corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
-    if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):
-        abort(403)
-    return send_from_directory(
-        os.path.dirname(corpus_file.path),
-        os.path.basename(corpus_file.path),
-        as_attachment=True,
-        attachment_filename=corpus_file.filename,
-        mimetype=corpus_file.mimetype
-    )
-
-
-#region json-routes
-@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
-@login_required
-@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
-@content_negotiation(produces='application/json')
-def delete_corpus_file(corpus_id, corpus_file_id):
-    def _delete_corpus_file(app, corpus_file_id):
-        with app.app_context():
-            corpus_file = CorpusFile.query.get(corpus_file_id)
-            corpus_file.delete()
-            db.session.commit()
-
-    corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
-    thread = Thread(
-        target=_delete_corpus_file,
-        args=(current_app._get_current_object(), corpus_file.id)
-    )
-    thread.start()
-    return {}, 202
-#endregion json-routes
-#endregion files
-
-##############################################################################
-# Corpus/Followers                                                           #
-##############################################################################
-#region followers
-#region json-routes
-@bp.route('/<hashid:corpus_id>/followers', methods=['POST'])
-@login_required
-@corpus_owner_or_admin_required
-@content_negotiation(consumes='application/json', produces='application/json')
-def add_corpus_followers(corpus_id):
-    usernames = request.json
-    if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
-        abort(400)
-    corpus = Corpus.query.get_or_404(corpus_id)
-    for username in usernames:
-        user = User.query.filter_by(username=username, is_public=True).first_or_404()
-        user.follow_corpus(corpus)
-    db.session.commit()
-    resonse_data = {
-        'message': f'Users are now following "{corpus.title}"',
-        'category': 'corpus'
-    }
-    response = jsonify(resonse_data)
-    response.status_code = 200
-    return response
-
-
-@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE'])
-@login_required
-@content_negotiation(produces='application/json')
-def unfollow_corpus(corpus_id, follower_id):
-    corpus = Corpus.query.get_or_404(corpus_id)
-    follower = User.query.get_or_404(follower_id)
-    if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
-        abort(403)
-    if not follower.is_following_corpus(corpus):
-        abort(409)  # 'User is not following the corpus'
-    follower.unfollow_corpus(corpus)
-    db.session.commit()
-    response_data = {
-        'message': \
-            f'"{follower.username}" is not following "{corpus.title}" anymore',
-        'category': 'corpus'
-    }
-    response = jsonify(response_data)
-    response.status_code = 200
-    return response
-
-
-@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT'])
-@login_required
-@corpus_owner_or_admin_required
-@content_negotiation(consumes='application/json', produces='application/json')
-def add_permission(corpus_id, follower_id):
-    role_name = request.json
-    if not isinstance(role_name, str):
-        abort(400)
-    cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
-    if cfr is None:
-        abort(400)
-    cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
-    cfa.role = cfr
-    db.session.commit()
-    resonse_data = {
-        'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
-        'category': 'corpus'
-    }
-    response = jsonify(resonse_data)
-    response.status_code = 200
-    return response
-#endregion json-routes
-#endregion followers
diff --git a/app/errors/__init__.py b/app/errors/__init__.py
index 0d79af4857891d0c86c260b95416c3ddac5f80b3..1f0480b4d12b7ddb76d17a898fcd2a2a15f0f716 100644
--- a/app/errors/__init__.py
+++ b/app/errors/__init__.py
@@ -1,5 +1,6 @@
-from flask import Blueprint
+from werkzeug.exceptions import HTTPException
+from .handlers import generic_error_handler
 
 
-bp = Blueprint('errors', __name__)
-from . import handlers
+def init_app(app):
+    app.register_error_handler(HTTPException, generic_error_handler)
diff --git a/app/errors/handlers.py b/app/errors/handlers.py
index cc6c926869536362764b0cab2ac8d75eed1dc0ea..5a6c413d3b2205cbed1e377b8c5fd7f7a8648fae 100644
--- a/app/errors/handlers.py
+++ b/app/errors/handlers.py
@@ -1,11 +1,6 @@
-from flask import render_template, request
-from werkzeug.exceptions import HTTPException
-from . import bp
+from flask import render_template
 
 
-@bp.errorhandler(HTTPException)
 def generic_error_handler(e):
-    if (request.accept_mimetypes.accept_json
-            and not request.accept_mimetypes.accept_html):
-        return {'errors': {'message': e.description}}, e.code
+    print('test')
     return render_template('errors/error.html.j2', error=e), e.code
diff --git a/app/main/routes.py b/app/main/routes.py
index 287a04f5b162c13d80d18b001508f90c61f213a5..918f7414d2415f510dbf28ca0a92aa63878b6fc7 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -30,12 +30,6 @@ def dashboard():
     return render_template('main/dashboard.html.j2', title='Dashboard')
 
 
-@bp.route('/dashboard2')
-@login_required
-def dashboard2():
-    return render_template('main/dashboard2.html.j2', title='Dashboard')
-
-
 @bp.route('/user_manual')
 def user_manual():
     return render_template('main/user_manual.html.j2', title='User manual')
@@ -55,6 +49,7 @@ def privacy_policy():
 def terms_of_use():
     return render_template('main/terms_of_use.html.j2', title='Terms of Use')
 
+
 @bp.route('/social-area')
 def social_area():
     users = [
diff --git a/app/static/js/Requests/Corpora.js b/app/static/js/Requests/Corpora.js
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/app/static/js/Requests/corpora/corpora.js b/app/static/js/Requests/corpora/corpora.js
index 34593d952b5d8c274e5902ed8984c79cef010487..b8031e6e8c665b4c1409470a7e3a30dbb2329294 100644
--- a/app/static/js/Requests/corpora/corpora.js
+++ b/app/static/js/Requests/corpora/corpora.js
@@ -33,11 +33,11 @@ Requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
 
 Requests.corpora.entity.isPublic = {};
 
-Requests.corpora.entity.isPublic.update = (corpusId, value) => {
+Requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
   let input = `/corpora/${corpusId}/is_public`;
   let init = {
     method: 'PUT',
-    body: JSON.stringify(value)
+    body: JSON.stringify(isPublic)
   };
   return Requests.JSONfetch(input, init);
 };
diff --git a/app/static/js/ResourceLists/CorpusFileList.js b/app/static/js/ResourceLists/CorpusFileList.js
index c052b2eaf529467ac659e3f83815cd99fbe23260..0ce96e2dbaf1703c66ecef5860b243961e7e4faf 100644
--- a/app/static/js/ResourceLists/CorpusFileList.js
+++ b/app/static/js/ResourceLists/CorpusFileList.js
@@ -100,7 +100,37 @@ class CorpusFileList extends ResourceList {
     let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
     switch (listAction) {
       case 'delete': {
-        Utils.deleteCorpusFileRequest(this.userId, this.corpusId, itemId);
+        let values = this.listjs.get('id', itemId)[0].values();
+        let modalElement = Utils.HTMLToElement(
+          `
+            <div class="modal">
+              <div class="modal-content">
+                <h4>Confirm Corpus File deletion</h4>
+                <p>Do you really want to delete the Corpus File <b>${values.title}</b>? All files will be permanently deleted!</p>
+              </div>
+              <div class="modal-footer">
+                <a class="btn modal-close waves-effect waves-light">Cancel</a>
+                <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
+              </div>
+            </div>
+          `
+        );
+        document.querySelector('#modals').appendChild(modalElement);
+        let modal = M.Modal.init(
+          modalElement,
+          {
+            dismissible: false,
+            onCloseEnd: () => {
+              modal.destroy();
+              modalElement.remove();
+            }
+          }
+        );
+        let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
+        confirmElement.addEventListener('click', (event) => {
+          Requests.corpora.entity.files.ent.delete(this.corpusId, itemId);
+        });
+        modal.open();
         break;
       }
       case 'download': {
diff --git a/app/static/js/ResourceLists/CorpusList.js b/app/static/js/ResourceLists/CorpusList.js
index 7d43167380c04ee67d08b4d6f7a9727793243b69..955bb424b205be309c7115c48c8b2e01dc51cc33 100644
--- a/app/static/js/ResourceLists/CorpusList.js
+++ b/app/static/js/ResourceLists/CorpusList.js
@@ -95,7 +95,37 @@ class CorpusList extends ResourceList {
     let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
     switch (listAction) {
       case 'delete-request': {
-        Requests.corpora.entity.delete(this.userId, itemId);
+        let values = this.listjs.get('id', itemId)[0].values();
+        let modalElement = Utils.HTMLToElement(
+          `
+            <div class="modal">
+              <div class="modal-content">
+                <h4>Confirm Corpus deletion</h4>
+                <p>Do you really want to delete the Corpus <b>${values.title}</b>? All files will be permanently deleted!</p>
+              </div>
+              <div class="modal-footer">
+                <a class="btn modal-close waves-effect waves-light">Cancel</a>
+                <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
+              </div>
+            </div>
+          `
+        );
+        document.querySelector('#modals').appendChild(modalElement);
+        let modal = M.Modal.init(
+          modalElement,
+          {
+            dismissible: false,
+            onCloseEnd: () => {
+              modal.destroy();
+              modalElement.remove();
+            }
+          }
+        );
+        let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
+        confirmElement.addEventListener('click', (event) => {
+          Requests.corpora.entity.delete(itemId);
+        });
+        modal.open();
         break;
       }
       case 'view': {
diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js
index e7c5404e66f82722ef81bbc8dcf984264c98ea2c..822ab7757c03e55acd7b7590b0631a89f60a8ea1 100644
--- a/app/static/js/Utils.js
+++ b/app/static/js/Utils.js
@@ -69,118 +69,6 @@ class Utils {
     return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
   }
 
-  static deleteCorpusRequest(userId, corpusId) {
-    return new Promise((resolve, reject) => {
-      let corpus;
-      try {
-        corpus = app.data.users[userId].corpora[corpusId];
-      } catch (error) {
-        corpus = {};
-      }
-
-      let modalElement = Utils.HTMLToElement(
-        `
-          <div class="modal">
-            <div class="modal-content">
-              <h4>Confirm Corpus deletion</h4>
-              <p>Do you really want to delete the Corpus <b>${corpus?.title}</b>? All files will be permanently deleted!</p>
-            </div>
-            <div class="modal-footer">
-              <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
-              <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
-            </div>
-          </div>
-        `
-      );
-      document.querySelector('#modals').appendChild(modalElement);
-      let modal = M.Modal.init(
-        modalElement,
-        {
-          dismissible: false,
-          onCloseEnd: () => {
-            modal.destroy();
-            modalElement.remove();
-          }
-        }
-      );
-      
-      let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
-      confirmElement.addEventListener('click', (event) => {
-        let corpusTitle = corpus?.title;
-        fetch(`/corpora/${corpusId}`, {method: 'DELETE', headers: {Accept: 'application/json'}})
-          .then(
-            (response) => {
-              if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
-              if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
-              app.flash(`Corpus "${corpusTitle}" marked for deletion`, 'corpus');
-              resolve(response);
-            },
-            (response) => {
-              app.flash('Something went wrong', 'error');
-              reject(response);
-            }
-          );
-      });
-      modal.open();
-    });
-  }
-
-  static deleteCorpusFileRequest(userId, corpusId, corpusFileId) {
-    return new Promise((resolve, reject) => {
-      let corpusFile;
-      try {
-        corpusFile = app.data.users[userId].corpora[corpusId].files[corpusFileId];
-      } catch (error) {
-        corpusFile = {};
-      }
-
-      let modalElement = Utils.HTMLToElement(
-        `
-          <div class="modal">
-            <div class="modal-content">
-              <h4>Confirm Corpus File deletion</h4>
-              <p>Do you really want to delete the Corpus File <b>${corpusFile?.title}</b>? All files will be permanently deleted!</p>
-            </div>
-            <div class="modal-footer">
-              <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
-              <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
-            </div>
-          </div>
-        `
-      );
-      document.querySelector('#modals').appendChild(modalElement);
-      let modal = M.Modal.init(
-        modalElement,
-        {
-          dismissible: false,
-          onCloseEnd: () => {
-            modal.destroy();
-            modalElement.remove();
-          }
-        }
-      );
-
-      let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
-      confirmElement.addEventListener('click', (event) => {
-        let corpusFileTitle = corpusFile?.title;
-        fetch(`/corpora/${corpusId}/files/${corpusFileId}`, {method: 'DELETE', headers: {Accept: 'application/json'}})
-          .then(
-            (response) => {
-              if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
-              if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
-              app.flash(`Corpus File "${corpusFileTitle}" deleted`, 'corpus');
-              resolve(response);
-            },
-            (response) => {
-              app.flash('Something went wrong', 'error');
-              reject(response);
-            }
-          );
-      });
-      modal.open();
-    });
-  }
-
   static deleteProfileAvatarRequest(userId) {
     return new Promise((resolve, reject) => {
       let modalElement = Utils.HTMLToElement(
diff --git a/app/templates/_roadmap.html.j2 b/app/templates/_roadmap.html.j2
index 50cc18cd7387a4992b66591a6334103a727dced2..3b8ea308f51354b2b9b54470b958082a3f24f46d 100644
--- a/app/templates/_roadmap.html.j2
+++ b/app/templates/_roadmap.html.j2
@@ -14,7 +14,7 @@
       <li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create 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.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li>
+      <li class="tab"><a{%if request.path == url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li>
       {% else %}
       <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li>
       {% endif %}
diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2
index dbecd35e1e76419baff35b973af85bf4beb9b2bb..c9189e13cfb83f9bc865560a7c50f828487a1864 100644
--- a/app/templates/base.html.j2
+++ b/app/templates/base.html.j2
@@ -49,4 +49,8 @@
 {% block scripts %}
 {{ super() }}
 {% include "_scripts.html.j2" %}
+{% set page_script = self._TemplateReference__context.name|replace('.html.j2', '.js.j2') %}
+<script>
+{% include page_script ignore missing %}
+</script>
 {% endblock scripts %}
diff --git a/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2
index 43ad0a136437deaae1ac299c0b13a2f8ca64b2df..84eda745a0534e99fcc1b22eb81231d4de06ec21 100644
--- a/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2
+++ b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2
@@ -79,37 +79,3 @@
   </div>
 </div>
 {% endblock page_content %}
-
-{% block modals %}
-{{ super() }}
-<div id="models-modal" class="modal">
-  <div class="modal-content">
-    <h4>Tesseract OCR Pipeline models</h4>
-    <table>
-      <thead>
-        <tr>
-          <th>Title</th>
-          <th>Description</th>
-          <th>Biblio</th>
-        </tr>
-      </thead>
-      <tbody>
-        {% for m in tesseract_ocr_pipeline_models %}
-        <tr id="tesseract-ocr-pipeline-model-{{ m.hashid }}">
-          <td>{{ m.title }}</td>
-          {% if m.description == '' %}
-          <td>Description is not available.</td>
-          {% else %}
-          <td>{{ m.description }}</td>
-          {% endif %}
-          <td><a href="{{ m.publisher_url }}">{{ m.publisher }}</a> ({{ m.publishing_year }}), {{ m.title }} {{ m.version}}, <a href="{{ m.publishing_url }}">{{ m.publishing_url }}</a></td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
-  </div>
-  <div class="modal-footer">
-    <a href="#!" class="modal-close waves-effect waves-light btn">Close</a>
-  </div>
-</div>
-{% endblock modals %}
diff --git a/app/templates/corpora/_breadcrumbs.html.j2 b/app/templates/corpora/_breadcrumbs.html.j2
index af6d2b78eec36b366fe81854fc64c3ae89945888..bdbe0f3449c9a514c920e01055f728aba56d1419 100644
--- a/app/templates/corpora/_breadcrumbs.html.j2
+++ b/app/templates/corpora/_breadcrumbs.html.j2
@@ -1,4 +1,4 @@
-{% set breadcrumbs %}
+{# {% 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>
@@ -25,4 +25,4 @@
 <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 %}
+{% endset %} #}
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index d8577907d3ed93e84651236d7131c0600d082c53..053dbdc0a775997a7c0b89158d729796dcf28b7f 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -91,7 +91,7 @@
           <div class="corpus-file-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div>
         </div>
         <div class="card-action right-align">
-          <a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a>
+          <a href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a>
         </div>
       </div>
     </div>
@@ -215,132 +215,3 @@
   </div>
 </div>
 {% endblock modals %}
-
-{% block scripts %}
-{{ super() }}
-<script>
-  let corpusId = {{ corpus.hashid|tojson }};
-  let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
-
-  // #region publishing_modal_js
-  let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
-  publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
-    let newIsPublic = publishingModalIsPublicSwitchElement.checked;
-    Requests.corpora.entity.isPublic.update(corpusId, newIsPublic)
-      .catch((response) => {
-        publishingModalIsPublicSwitchElement.checked = !newIsPublic;
-      });
-  });
-  // #endregion publishing_modal_js
-
-  // #region delete_modal_js
-  let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
-  deleteModalDeleteButtonElement.addEventListener('click', (event) => {
-    Requests.corpora.entity.delete(corpusId)
-      .then((response) => {window.location.href = '/dashboard';});
-  });
-  // #endregion delete_modal_js
-
-  // #region invite_user_modal_js
-  let inviteUserModalElement = document.querySelector('#invite-user-modal');
-  let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
-  let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
-
-  let inviteUserModalSearch = M.Chips.init(
-    inviteUserModalSearchElement,
-    {
-      autocompleteOptions: {
-        data: {
-          'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar',
-          'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar',
-          'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar'
-        }
-      },
-      limit: 3,
-      onChipAdd: (a, chipElement) => {
-        if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) {
-          chipElement.firstElementChild.click();
-        }
-      },
-      placeholder: 'Enter a username',
-      secondaryPlaceholder: 'Add more users'
-    }
-  );
-
-  M.Modal.init(
-    inviteUserModalElement,
-    {
-      onOpenStart: (modalElement, modalTriggerElement) => {
-        while (inviteUserModalSearch.chipsData.length > 0) {
-          inviteUserModalSearch.deleteChip(0);
-        }
-      }
-    }
-  )
-
-  inviteUserModalInviteButtonElement.addEventListener('click', (event) => {
-    let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag);
-    Requests.corpora.entity.followers.add(corpusId, usernames);
-  });
-  // #endregion invite_user_modal_js
-
-  // #region share_link_modal_js
-  let shareLinkModalElement = document.querySelector('#share-link-modal');
-  let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select');
-  let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker');
-  let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button');
-  let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container');
-  let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field');
-  let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button');
-
-  let today = new Date();
-  let tomorrow = new Date();
-  tomorrow.setDate(today.getDate() + 1);
-  let oneWeekLater = new Date();
-  oneWeekLater.setDate(today.getDate() + 7);
-  let fourWeeksLater = new Date();
-  fourWeeksLater.setDate(today.getDate() + 28);
-
-  M.Datepicker.init(
-    shareLinkModalExpirationDateDatepickerElement,
-    {
-      container: document.querySelector('main'),
-      defaultDate: oneWeekLater,
-      setDefaultDate: true,
-      minDate: tomorrow,
-      maxDate: fourWeeksLater
-    }
-  );
-
-  M.Modal.init(
-    shareLinkModalElement,
-    {
-      onOpenStart: (modalElement, modalTriggerElement) => {
-        shareLinkModalOutputFieldElement.value = '';
-        shareLinkModalOutputContainerElement.classList.add('hide');
-      }
-    }
-  )
-
-  shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
-    Requests.corpora.entity.generateShareLink(corpusId, shareLinkModalCorpusFollowerRoleSelectElement.value, shareLinkModalExpirationDateDatepickerElement.value)
-      .then((response) => {
-        response.json()
-          .then((json) => {
-            shareLinkModalOutputContainerElement.classList.remove('hide');
-            shareLinkModalOutputFieldElement.value = json.corpusShareLink;
-          });
-      });
-  });
-
-  shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
-    navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
-      .then(
-        () => {app.flash('Copied!');},
-        () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
-      );
-    
-  });
-  // #endregion share_link_modal_js
-</script>
-{% endblock scripts %}
diff --git a/app/templates/corpora/corpus.js.j2 b/app/templates/corpora/corpus.js.j2
new file mode 100644
index 0000000000000000000000000000000000000000..7e815cf373cf6e63999bb9d745eb01a50d4d2395
--- /dev/null
+++ b/app/templates/corpora/corpus.js.j2
@@ -0,0 +1,127 @@
+let corpusId = {{ corpus.hashid|tojson }};
+let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
+
+// #region Publishing
+let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
+publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
+  let newIsPublic = publishingModalIsPublicSwitchElement.checked;
+  Requests.corpora.entity.isPublic.update(corpusId, newIsPublic)
+    .catch((response) => {
+      publishingModalIsPublicSwitchElement.checked = !newIsPublic;
+    });
+});
+// #endregion Publishing
+
+// #region Delete
+let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
+deleteModalDeleteButtonElement.addEventListener('click', (event) => {
+  Requests.corpora.entity.delete(corpusId)
+    .then((response) => {
+      window.location.href = {{ url_for('main.dashboard')|tojson }};
+    });
+});
+// #endregion Delete
+
+// #region Invite users
+let inviteUserModalElement = document.querySelector('#invite-user-modal');
+let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
+let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
+
+let inviteUserModalSearch = M.Chips.init(
+  inviteUserModalSearchElement,
+  {
+    autocompleteOptions: {
+      data: {
+        'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar',
+        'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar',
+        'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar'
+      }
+    },
+    limit: 3,
+    onChipAdd: (a, chipElement) => {
+      if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) {
+        chipElement.firstElementChild.click();
+      }
+    },
+    placeholder: 'Enter a username',
+    secondaryPlaceholder: 'Add more users'
+  }
+);
+
+M.Modal.init(
+  inviteUserModalElement,
+  {
+    onOpenStart: (modalElement, modalTriggerElement) => {
+      while (inviteUserModalSearch.chipsData.length > 0) {
+        inviteUserModalSearch.deleteChip(0);
+      }
+    }
+  }
+)
+
+inviteUserModalInviteButtonElement.addEventListener('click', (event) => {
+  let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag);
+  Requests.corpora.entity.followers.add(corpusId, usernames);
+});
+// #endregion Invite users
+
+// #region Share link
+let shareLinkModalElement = document.querySelector('#share-link-modal');
+let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select');
+let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker');
+let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button');
+let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container');
+let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field');
+let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button');
+
+let today = new Date();
+let tomorrow = new Date();
+tomorrow.setDate(today.getDate() + 1);
+let oneWeekLater = new Date();
+oneWeekLater.setDate(today.getDate() + 7);
+let fourWeeksLater = new Date();
+fourWeeksLater.setDate(today.getDate() + 28);
+
+M.Datepicker.init(
+  shareLinkModalExpirationDateDatepickerElement,
+  {
+    container: document.querySelector('main'),
+    defaultDate: oneWeekLater,
+    setDefaultDate: true,
+    minDate: tomorrow,
+    maxDate: fourWeeksLater
+  }
+);
+
+M.Modal.init(
+  shareLinkModalElement,
+  {
+    onOpenStart: (modalElement, modalTriggerElement) => {
+      shareLinkModalOutputFieldElement.value = '';
+      shareLinkModalOutputContainerElement.classList.add('hide');
+    }
+  }
+)
+
+shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
+  let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
+  let expiration = shareLinkModalExpirationDateDatepickerElement.value
+  Requests.corpora.entity.generateShareLink(corpusId, role, expiration)
+    .then((response) => {
+      response.json()
+        .then((json) => {
+          shareLinkModalOutputContainerElement.classList.remove('hide');
+          shareLinkModalOutputFieldElement.value = json.corpusShareLink;
+        });
+    });
+});
+
+shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
+  navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
+    .then(
+      () => {app.flash('Copied!');},
+      () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
+    );
+  
+});
+// #endregion Share link
diff --git a/app/templates/corpora/corpus_file.html.j2 b/app/templates/corpora/files/corpus_file.html.j2
similarity index 100%
rename from app/templates/corpora/corpus_file.html.j2
rename to app/templates/corpora/files/corpus_file.html.j2
diff --git a/app/templates/corpora/create_corpus_file.html.j2 b/app/templates/corpora/files/create_corpus_file.html.j2
similarity index 95%
rename from app/templates/corpora/create_corpus_file.html.j2
rename to app/templates/corpora/files/create_corpus_file.html.j2
index 44e9ff82ff51adbc332350675bc9473113843dcd..8cff13a61803d50a68efa89ffa954f10cb44af0e 100644
--- a/app/templates/corpora/create_corpus_file.html.j2
+++ b/app/templates/corpora/files/create_corpus_file.html.j2
@@ -36,7 +36,7 @@
             </div>
           </div>
           <div class="card-action right-align">
-            <a class="waves-effect waves-light btn red" href="{{ url_for('.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a>
+            <a class="waves-effect waves-light btn red" href="{{ url_for('corpora.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a>
             {{ wtf.render_field(form.submit, material_icon='send') }}
           </div>
         </div>
diff --git a/app/templates/corpora/public_corpora.html.j2 b/app/templates/corpora/public_corpora.html.j2
deleted file mode 100644
index 9be377f32a9fc7b1444f376b89d57a72242629dc..0000000000000000000000000000000000000000
--- a/app/templates/corpora/public_corpora.html.j2
+++ /dev/null
@@ -1,72 +0,0 @@
-{% extends "base.html.j2" %}
-
-{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
-
-{% block page_content %}
-<div class="corpus-list no-autoinit" id="corpus-list">
-  <div class="parallax-container">
-    <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
-    <div style="position: absolute; bottom: 0; width: 100%;">
-      <div class="container">
-        <div class="white-text">
-          <h1 id="title"><i class="nopaque-icons" style="font-size: inherit;">I</i>Corpora</h1>
-        </div>
-        <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
-          <div class="input-field">
-            <i class="material-icons prefix">search</i>
-            <input class="search" id="corpus-list-search" type="text">
-            <label for="corpus-list-search">Search corpus</label>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-
-  <div class="row">
-    <div class="col s12" id="corpora">
-      <div class="card">
-        <div class="card-content">
-          <div>
-            <table>
-              <thead>
-                <tr>
-                  <th></th>
-                  <th>Title and Description</th>
-                  <th>Status</th>
-                  <th></th>
-                </tr>
-              </thead>
-              <tbody class="list"></tbody>
-            </table>
-            <ul class="pagination"></ul>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-{% endblock page_content %}
-
-
-{% block scripts %}
-{{ super() }}
-<script>
-  let corpusListElement = document.querySelector('#corpus-list');
-  let corpusListOptions = {
-    initialHtmlGenerator: null,
-    item: `
-      <tr class="clickable hoverable">
-        <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
-        <td><b class="title"></b><br><i class="description"></i></td>
-        <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
-        <td class="right-align">
-          <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
-        </td>
-      </tr>
-    `.trim(),
-  };
-  let corpusList = new CorpusList(corpusListElement, corpusListOptions);
-  corpusList._init({{ corpora|tojson }});
-</script>
-{% endblock scripts %}
diff --git a/app/templates/corpora/public_corpus.html.j2 b/app/templates/corpora/public_corpus.html.j2
index 41f5798baca90f8fe5cd756f45e8113e27e251ed..ff445e38cd2829d7f3d54bbc228f5b6dc9e6adcb 100644
--- a/app/templates/corpora/public_corpus.html.j2
+++ b/app/templates/corpora/public_corpus.html.j2
@@ -101,30 +101,3 @@
   </div>
 </div>
 {% endblock page_content %}
-
-{% block scripts %}
-{{ super() }}
-<script>
-  let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list'));
-  corpusFileList.add({{ corpus_files|tojson }});
-
-  let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
-  unfollowRequestElement.addEventListener('click', () => {
-    return new Promise((resolve, reject) => {
-      fetch('{{ url_for("corpora.unfollow_corpus", corpus_id=corpus.id, follower_id=current_user.id) }}', {method: 'POST', headers: {Accept: 'application/json'}})
-        .then(
-          (response) => {
-            if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
-            if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
-            resolve(response);
-            window.location.href = '{{ url_for("main.dashboard") }}';
-          },
-          (response) => {
-            app.flash('Something went wrong', 'error');
-            reject(response);
-          }
-        );
-    });
-  });
-</script>
-{% endblock scripts %}
diff --git a/app/templates/corpora/public_corpus.js.j2 b/app/templates/corpora/public_corpus.js.j2
new file mode 100644
index 0000000000000000000000000000000000000000..0b8205f42be1699481b58a27478a81af09b1a8a6
--- /dev/null
+++ b/app/templates/corpora/public_corpus.js.j2
@@ -0,0 +1,11 @@
+let corpusId = {{ corpus.hashid|tojson }};
+let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list'));
+corpusFileList.add({{ corpus_files|tojson }});
+
+let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
+unfollowRequestElement.addEventListener('click', () => {
+  Requests.corpora.entity.followers.entity.delete(corpusId, currentUserId)
+    .then((response) => {
+      window.location.href = {{ url_for('main.dashboard')|tojson }};
+    });
+});
diff --git a/app/templates/main/dashboard2.html.j2 b/app/templates/main/dashboard2.html.j2
deleted file mode 100644
index d45aaa041d55a2b123aaeb3dc63f82ab3858c61a..0000000000000000000000000000000000000000
--- a/app/templates/main/dashboard2.html.j2
+++ /dev/null
@@ -1,274 +0,0 @@
-{% extends "base.html.j2" %}
-{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
-
-{% block page_content %}
-<div class="section scrollspy" id="dashboard">
-  <div class="row">
-    <div class="col s1"></div>
-    <div class="col s11">
-      <h1 id="title">Dashboard</h1>
-    </div>
-    <div class="col s1"></div>
-    <div class="col s3">
-      <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
-    </div>
-    <div class="col s8">
-      <a class="btn waves-effect waves-light" href="#my-corpora"><i class="nopaque-icons left">I</i>My Corpora</a>
-      <a class="btn waves-effect waves-light" href="#my-jobs"><i class="nopaque-icons left">J</i>My Jobs</a>
-      <a class="btn waves-effect waves-light" href="#my-groups"><i class="material-icons left">groups</i>My Groups</a>
-    </div>
-  </div>
-</div>
-
-
-<div class="corpus-list no-autoinit" id="corpus-list" data-user-id="{{ current_user.hashid }}">
-  <div class="parallax-container">
-    <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
-    <div style="position: absolute; bottom: 0; width: 100%;">
-      <div class="container">
-        <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
-          <div class="input-field">
-            <i class="material-icons prefix">search</i>
-            <input class="search" id="corpus-list-search" type="text">
-            <label for="corpus-list-search">Search Corpus</label>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="section scrollspy" id="my-corpora">
-    <div class="row">
-      <div class="col s1"></div>
-      <div class="col s2">
-        <h2>My Corpora</h2>
-        <p>Create a corpus to interactively perform linguistic analysis.</p>
-        <p>Or browse our users public corpora.<span class="new badge"></span></p>
-      </div>
-      <div class="col s6">
-        <div class="card">
-          <div class="card-content">
-            <div>
-              <table>
-                <thead>
-                  <tr>
-                    <th></th>
-                    <th>Title and Description</th>
-                    <th>Status</th>
-                    <th></th>
-                  </tr>
-                </thead>
-                <tbody class="list"></tbody>
-              </table>
-              <ul class="pagination"></ul>
-            </div>
-          </div>
-          <div class="card-action right-align">
-            <a class="btn disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}">Import Corpus<i class="material-icons right">import_export</i></a>
-            <a class="btn waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
-          </div>
-        </div>
-      </div>
-      <div class="col s1"></div>
-      <div class="col s2">
-        <ul class="section table-of-contents">
-          <li><a href="#dashboard">Dashboard</a></li>
-          <li><a href="#my-corpora">My Corpora</a></li>
-          <li><a href="#my-jobs">My Jobs</a></li>
-          <li><a href="#my-groups">My Groups</a></li>
-        </ul>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="job-list no-autoinit" id="job-list" data-user-id="{{ current_user.hashid }}">
-  <div class="parallax-container">
-    <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
-    <div style="position: absolute; bottom: 0; width: 100%;">
-      <div class="container">
-        <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
-          <div class="input-field">
-            <i class="material-icons prefix">search</i>
-            <input class="search" id="job-list-search" type="text">
-            <label for="job-list-search">Search Job</label>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="section scrollspy" id="my-jobs">
-    <div class="row">
-      <div class="col s1"></div>
-      <div class="col s2">
-        <h2>My Jobs</h2>
-        <p>
-          A job is the execution of a service provided by nopaque. You can
-          create any number of jobs and let them be processed simultaneously. We
-          <b>strongly recommend</b> that you create a folder on your computer where you
-          save the various files that nopaque provides you with after each
-          pre-processing step. You will need the result of each step for the
-          next step.
-        </p>
-        <p><b>Where is my Job data?</b> Don't worry, please read <a href="{{ url_for('main.news', _anchor='april-2022-update') }}">this news</a> entry</p>
-      </div>
-      <div class="col s6">
-        <div class="card">
-          <div class="card-content">
-            <div>
-              <table>
-                <thead>
-                  <tr>
-                    <th></th>
-                    <th>Title and Description</th>
-                    <th>Status</th>
-                    <th></th>
-                  </tr>
-                </thead>
-                <tbody class="list"></tbody>
-              </table>
-              <ul class="pagination"></ul>
-            </div>
-          </div>
-          <div class="card-action right-align">
-            <a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal"><i class="material-icons left">add</i>Create job</a>
-          </div>
-        </div>
-      </div>
-      <div class="col s1"></div>
-      <div class="col s2">
-        <ul class="section table-of-contents">
-          <li><a href="#dashboard">Dashboard</a></li>
-          <li><a href="#my-corpora">My Corpora</a></li>
-          <li><a href="#my-jobs">My Jobs</a></li>
-          <li><a href="#my-groups">My Groups</a></li>
-        </ul>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="group-list no-autoinit" id="group-list" data-user-id="{{ current_user.hashid }}">
-  <div class="parallax-container">
-    <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
-    <div style="position: absolute; bottom: 0; width: 100%;">
-      <div class="container">
-        <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
-          <div class="input-field">
-            <i class="material-icons prefix">search</i>
-            <input class="search" id="group-list-search" type="text">
-            <label for="group-list-search">Search Group</label>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="section scrollspy" id="my-groups">
-    <div class="row">
-      <div class="col s1"></div>
-      <div class="col s2">
-        <h2>My Groups</h2>
-        <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
-      </div>
-      <div class="col s6">
-        <div class="card">
-          <div class="card-content">
-            <div>
-              <table>
-                <thead>
-                  <tr>
-                    <th></th>
-                    <th>Title and Description</th>
-                    <th>Status</th>
-                    <th></th>
-                  </tr>
-                </thead>
-                <tbody class="list"></tbody>
-              </table>
-              <ul class="pagination"></ul>
-            </div>
-          </div>
-          <div class="card-action right-align">
-            <a class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create group</a>
-          </div>
-        </div>
-      </div>
-      <div class="col s1"></div>
-      <div class="col s2">
-        <ul class="section table-of-contents">
-          <li><a href="#dashboard">Dashboard</a></li>
-          <li><a href="#my-corpora">My Corpora</a></li>
-          <li><a href="#my-jobs">My Jobs</a></li>
-          <li><a href="#my-groups">My Groups</a></li>
-        </ul>
-      </div>
-    </div>
-  </div>
-</div>
-{% endblock page_content %}
-
-{% block modals %}
-{{ super() }}
-<div id="create-job-modal" class="modal">
-  <div class="modal-content">
-    <h4>Select a service</h4>
-    <p>&nbsp;</p>
-    <div class="row">
-      <div class="col s12 m4">
-          <div class="card-panel center-align hoverable">
-            <br>
-            <a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
-            </a>
-            <br><br>
-            <p class="service-color-text darken" data-service="file-setup-pipeline"><b>File setup</b></p>
-            <p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing.</p>
-            <a href="{{ url_for('services.file_setup_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="file-setup-pipeline">Create Job</a>
-          </div>
-      </div>
-      <div class="col s12 m4">
-          <div class="card-panel center-align hoverable">
-            <br>
-            <a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
-            </a>
-            <br><br>
-            <p class="service-color-text darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
-            <p class="light">nopaque converts your image data – like photos or scans – into text data through a process called OCR. This step enables you to proceed with further computational analysis of your documents.</p>
-            <a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="tesseract-ocr-pipeline">Create Job</a>
-          </div>
-      </div>
-      <div class="col s12 m4">
-          <div class="card-panel center-align hoverable">
-            <br>
-            <a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
-              <i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
-            </a>
-            <br><br>
-            <p class="service-color-text darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
-            <p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
-            <a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="spacy-nlp-pipeline">Create Job</a>
-          </div>
-      </div>
-    </div>
-  </div>
-  <div class="modal-footer">
-    <a class="btn-flat modal-close waves-effect waves-light">Close</a>
-  </div>
-</div>
-{% endblock modals %}
-
-
-{% block scripts %}
-{{ super() }}
-<script>
-  let corpusListElement = document.querySelector('#corpus-list');
-  let corpusListOptions = {initialHtmlGenerator: null};
-  let corpusList = new CorpusList(corpusListElement, corpusListOptions);
-  let jobListElement = document.querySelector('#job-list');
-  let jobListOptions = {initialHtmlGenerator: null};
-  let jobList = new JobList(jobListElement, jobListOptions);
-</script>
-{% endblock scripts %}
diff --git a/app/templates/services/spacy_nlp_pipeline.html.j2 b/app/templates/services/spacy_nlp_pipeline.html.j2
index 030ea163456407eb186bced0a079c4dcba6da513..8f466e3dc9431583ecef8e641dd9740d572daa2b 100644
--- a/app/templates/services/spacy_nlp_pipeline.html.j2
+++ b/app/templates/services/spacy_nlp_pipeline.html.j2
@@ -77,7 +77,7 @@
                   {{ form.model.label }}
                   <span class="helper-text">
                     <a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#0064A3;">help_outline</i></a>
-                    <a class="tooltipped" href="{{ url_for('contributions.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a>
+                    <a class="tooltipped" href="{{ url_for('contributions.spacy_nlp_pipeline_models.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a>
                   </span>
                 </div>
               </div>
diff --git a/app/templates/services/tesseract_ocr_pipeline.html.j2 b/app/templates/services/tesseract_ocr_pipeline.html.j2
index ff4fd38b779260ab86f53ad87a5bfa115f1904a5..11d65c6439850bfb2f18787de2f911bb56efde3a 100644
--- a/app/templates/services/tesseract_ocr_pipeline.html.j2
+++ b/app/templates/services/tesseract_ocr_pipeline.html.j2
@@ -59,7 +59,7 @@
                   {{ form.model.label }}
                   <span class="helper-text">
                     <a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#00A58B;">help_outline</i></a>
-                    <a class="tooltipped" href="{{ url_for('contributions.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a>
+                    <a class="tooltipped" href="{{ url_for('contributions.tesseract_ocr_pipeline_models.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a>
                   </span>
                   {% for error in form.model.errors %}
                   <span class="helper-text error-color-text">{{ error }}</span>