diff --git a/SciPost_v1/settings/local_jorran.py b/SciPost_v1/settings/local_jorran.py
index 6b4b12e24cea7c7d0747cf4a948c0a5d03091146..6db72e8b3e4cc6d3d5f0393f45fc99ab32049d97 100644
--- a/SciPost_v1/settings/local_jorran.py
+++ b/SciPost_v1/settings/local_jorran.py
@@ -13,10 +13,10 @@ MIDDLEWARE += (
 INTERNAL_IPS = ['127.0.0.1', '::1']
 
 # Static and media
-STATIC_ROOT = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/static/'
-MEDIA_ROOT = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/media/'
+STATIC_ROOT = '/Users/jorrandewit/Documents/SciPost/codebase/local_files/static/'
+MEDIA_ROOT = '/Users/jorrandewit/Documents/SciPost/codebase/local_files/media/'
 WEBPACK_LOADER['DEFAULT']['BUNDLE_DIR_NAME'] =\
-    '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/local_files/static/bundles/'
+    '/Users/jorrandewit/Documents/SciPost/codebase/local_files/static/bundles/'
 
 MAILCHIMP_API_USER = get_secret("MAILCHIMP_API_USER")
 MAILCHIMP_API_KEY = get_secret("MAILCHIMP_API_KEY")
@@ -27,8 +27,8 @@ ITHENTICATE_USERNAME = get_secret('ITHENTICATE_USERNAME')
 ITHENTICATE_PASSWORD = get_secret('ITHENTICATE_PASSWORD')
 
 # Logging
-LOGGING['handlers']['scipost_file_arxiv']['filename'] = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/logs/arxiv.log'
-LOGGING['handlers']['scipost_file_doi']['filename'] = '/Users/jorrandewit/Documents/Develop/SciPost/scipost_v1/logs/doi.log'
+LOGGING['handlers']['scipost_file_arxiv']['filename'] = '/Users/jorrandewit/Documents/SciPost/codebase/logs/arxiv.log'
+LOGGING['handlers']['scipost_file_doi']['filename'] = '/Users/jorrandewit/Documents/SciPost/codebase/logs/doi.log'
 
 # Other
 CROSSREF_LOGIN_ID = get_secret("CROSSREF_LOGIN_ID")
diff --git a/journals/behaviors.py b/journals/behaviors.py
index dd9e5f253b3391d7c19672fbbe9b4978a2c7ae27..ce61585abd8761eea51ea03f31e3d9393cdf3910 100644
--- a/journals/behaviors.py
+++ b/journals/behaviors.py
@@ -7,14 +7,13 @@ from django.core.validators import RegexValidator
 from .constants import PUBLICATION_DOI_VALIDATION_REGEX
 
 
-doi_journal_validator = RegexValidator(r'^[a-zA-Z]+$',
-                                       'Only valid DOI expressions are allowed ([a-zA-Z]+).')
-doi_volume_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+$',
-                                      'Only valid DOI expressions are allowed ([a-zA-Z]+.[0-9]+).')
-doi_issue_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+.[0-9]+$',
-                                     ('Only valid DOI expressions are allowed '
-                                      '([a-zA-Z]+.[0-9]+.[0-9]+).'))
+doi_journal_validator = RegexValidator(
+    r'^[a-zA-Z]+$', 'Only valid DOI expressions are allowed ([a-zA-Z]+).')
+doi_volume_validator = RegexValidator(
+    r'^[a-zA-Z]+.[0-9]+$', 'Only valid DOI expressions are allowed ([a-zA-Z]+.[0-9]+).')
+doi_issue_validator = RegexValidator(
+    r'^[a-zA-Z]+.\w+(.[0-9]+)?$',
+    'Only valid DOI expressions are allowed ([a-zA-Z]+.\w+(.[0-9]+)?)')
 doi_publication_validator = RegexValidator(
     r'^{regex}$'.format(regex=PUBLICATION_DOI_VALIDATION_REGEX),
-    ('Only valid DOI expressions are allowed '
-     '(`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)'))
+    'Only valid DOI expressions are allowed: `[a-zA-Z]+(.\w+(.[0-9]+(.[0-9]{3,})?)?)?`')
diff --git a/journals/constants.py b/journals/constants.py
index 529c204ef05a223473c915cee4cca47717b6fc6f..d8d394a5c1c70eaba9b0dd14311e18d26a40f07a 100644
--- a/journals/constants.py
+++ b/journals/constants.py
@@ -33,8 +33,12 @@ REGEX_CHOICES = '|'.join([
 # Regex used for URLs of specific Publications and for
 # doi validation during the publication process.
 PUBLICATION_DOI_REGEX = '({})'.format(REGEX_CHOICES)
-PUBLICATION_DOI_REGEX += '.[0-9]+(.[0-9]+.[0-9]{3,})?'
+PUBLICATION_DOI_REGEX += '(.\w+(.[0-9]+(.[0-9]{3,})?)?)?'
 PUBLICATION_DOI_VALIDATION_REGEX = PUBLICATION_DOI_REGEX
+DOI_DISPATCH_REGEX = '(?P<journal_tag>{})'.format(REGEX_CHOICES)
+DOI_DISPATCH_REGEX += '(.(?P<part_1>\w+)(.(?P<part_2>[0-9]+)(.(?P<part_3>[0-9]{3,}))?)?)?'
+
+DOI_ISSUE_REGEX = '(?P<doi_label>({}).\w+(.[0-9]+)?)'.format(REGEX_CHOICES)
 
 SCIPOST_JOURNALS_DOMAINS = (
     ('E', 'Experimental'),
@@ -90,9 +94,9 @@ CC_LICENSES_URI = (
 
 ISSUES_AND_VOLUMES = 'IV'
 ISSUES_ONLY = 'IO'
-INDIVIDUAL_PUBLCATIONS = 'IP'
+INDIVIDUAL_PUBLICATIONS = 'IP'
 JOURNAL_STRUCTURE = (
     (ISSUES_AND_VOLUMES, 'Issues and Volumes'),
-    # (ISSUES_ONLY, 'Issues only'),  # This option complies with Crossref's rules, but is not implemented (yet).
-    (INDIVIDUAL_PUBLCATIONS, 'Individual Publications'),
+    (ISSUES_ONLY, 'Issues only'),
+    (INDIVIDUAL_PUBLICATIONS, 'Individual Publications'),
 )
diff --git a/journals/exceptions.py b/journals/exceptions.py
index 202f40aca57188a04f558297094c390266900b1c..d7d1b2642e5818ea410c6deeeba4f25e90e76955 100644
--- a/journals/exceptions.py
+++ b/journals/exceptions.py
@@ -24,3 +24,11 @@ class PaperNumberingError(Exception):
 
     def __str__(self):
         return self.nr
+
+
+class InvalidDOIError(Exception):
+    def __init__(self, name):
+        self.name = name
+
+    def __str__(self):
+        return self.name
diff --git a/journals/factories.py b/journals/factories.py
index 3bb8bc26fca3c41ecdab65b873c95bea583b9b44..5f74ef19e736a38089e47086058f4e9bfafbab81 100644
--- a/journals/factories.py
+++ b/journals/factories.py
@@ -9,7 +9,7 @@ import random
 
 from common.helpers import random_digits, random_external_doi, random_external_journal_abbrev
 from journals.constants import SCIPOST_JOURNALS, SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES,\
-    ISSUES_AND_VOLUMES, INDIVIDUAL_PUBLCATIONS, PUBLICATION_PUBLISHED
+    ISSUES_AND_VOLUMES, INDIVIDUAL_PUBLICATIONS, PUBLICATION_PUBLISHED
 from submissions.factories import PublishedSubmissionFactory
 
 from .models import Journal, Volume, Issue, Publication, Reference
@@ -48,7 +48,7 @@ class JournalFactory(factory.django.DjangoModelFactory):
     @factory.lazy_attribute
     def structure(self):
         if self.name == SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES:
-            return INDIVIDUAL_PUBLCATIONS
+            return INDIVIDUAL_PUBLICATIONS
         return ISSUES_AND_VOLUMES
 
 
diff --git a/journals/forms.py b/journals/forms.py
index 58d59866ef25cbd9da0d8dc4b7133b046cf936ff..ab03f21092d8215c77826480521cacf721057723 100644
--- a/journals/forms.py
+++ b/journals/forms.py
@@ -497,44 +497,61 @@ class DraftPublicationForm(forms.ModelForm):
             self.fields['acceptance_date'].initial = self.submission.acceptance_date
             self.fields['publication_date'].initial = timezone.now()
 
-        # Fill data that may be derived from the issue data
-        issue = None
+        # Fill data for Publications grouped by Issues (or Issue+Volume).
         if hasattr(self.instance, 'in_issue') and self.instance.in_issue:
-            issue = self.instance.in_issue
-        elif self.issue:
-            issue = self.issue
-        if issue:
-            self.prefill_with_issue(issue)
-
-        # Fill data that may be derived from the issue data
-        journal = None
+            self.issue = self.instance.in_issue
+        if self.issue:
+            self.prefill_with_issue(self.issue)
+
+        # Fill data for Publications ungrouped; directly linked to a Journal.
         if hasattr(self.instance, 'in_journal') and self.instance.in_journal:
-            journal = self.instance.in_issue
-        elif self.to_journal:
-            journal = self.to_journal
-        if journal:
-            self.prefill_with_journal(journal)
+            self.to_journal = self.instance.in_issue
+        if self.to_journal:
+            self.prefill_with_journal(self.to_journal)
 
     def prefill_with_issue(self, issue):
         # Determine next available paper number:
-        paper_nr = Publication.objects.filter(in_issue__in_volume=issue.in_volume).count() + 1
+        if issue.in_volume:
+            # Issue/Volume
+            paper_nr = Publication.objects.filter(in_issue__in_volume=issue.in_volume).count() + 1
+        elif issue.in_journal:
+            # Issue only
+            paper_nr = Publication.objects.filter(in_issue=issue).count() + 1
         if paper_nr > 999:
             raise PaperNumberingError(paper_nr)
+
         self.fields['paper_nr'].initial = str(paper_nr)
-        doi_label = '{journal}.{vol}.{issue}.{paper}'.format(
-            journal=issue.in_volume.in_journal.name,
-            vol=issue.in_volume.number,
-            issue=issue.number,
-            paper=str(paper_nr).rjust(3, '0'))
+        if issue.in_volume:
+            doi_label = '{journal}.{vol}.{issue}.{paper}'.format(
+                journal=issue.in_volume.in_journal.name,
+                vol=issue.in_volume.number,
+                issue=issue.number,
+                paper=str(paper_nr).rjust(3, '0'))
+        elif issue.in_journal:
+            doi_label = '{journal}.{issue}.{paper}'.format(
+                journal=issue.in_journal.name,
+                issue=issue.number,
+                paper=str(paper_nr).rjust(3, '0'))
         self.fields['doi_label'].initial = doi_label
-
         doi_string = '10.21468/{doi}'.format(doi=doi_label)
+
+        # Initiate a BibTex entry
         bibtex_entry = (
             '@Article{%s,\n'
-            '\ttitle={{%s},\n'
+            '\ttitle={{%s}},\n'
             '\tauthor={%s},\n'
-            '\tjournal={%s},\n'
-            '\tvolume={%i},\n'
+        ) % (
+            doi_string,
+            self.submission.title,
+            self.submission.author_list.replace(',', ' and'))
+
+        if issue.in_volume:
+            bibtex_entry += '\tjournal={%s},\n\tvolume={%i},\n' % (
+                issue.in_volume.in_journal.abbreviation_citation, issue.in_volume.number)
+        elif issue.in_journal:
+            bibtex_entry += '\tjournal={%s},\n' % (issue.in_journal.abbreviation_citation)
+
+        bibtex_entry += (
             '\tissue={%i},\n'
             '\tpages={%i},\n'
             '\tyear={%s},\n'
@@ -543,16 +560,12 @@ class DraftPublicationForm(forms.ModelForm):
             '\turl={https://scipost.org/%s},\n'
             '}'
         ) % (
-            doi_string,
-            self.submission.title,
-            self.submission.author_list.replace(',', ' and'),
-            issue.in_volume.in_journal.abbreviation_citation,
-            issue.in_volume.number,
             issue.number,
             paper_nr,
             issue.until_date.strftime('%Y'),
             doi_string,
             doi_string)
+
         self.fields['BiBTeX_entry'].initial = bibtex_entry
         if not self.instance.BiBTeX_entry:
             self.instance.BiBTeX_entry = bibtex_entry
@@ -569,7 +582,7 @@ class DraftPublicationForm(forms.ModelForm):
         doi_string = '10.21468/{doi}'.format(doi=doi_label)
         bibtex_entry = (
             '@Article{%s,\n'
-            '\ttitle={{%s},\n'
+            '\ttitle={{%s}},\n'
             '\tauthor={%s},\n'
             '\tjournal={%s},\n'
             '\tpages={%i},\n'
diff --git a/journals/managers.py b/journals/managers.py
index ca106578a5a8f02eba9fa2e271d7bac68209473d..f4ead6cac827c5a23cf13705fe8bc674b1dd5c2c 100644
--- a/journals/managers.py
+++ b/journals/managers.py
@@ -6,7 +6,7 @@ from django.db import models
 from django.utils import timezone
 
 from .constants import STATUS_PUBLISHED, STATUS_DRAFT, PUBLICATION_PUBLISHED, ISSUES_AND_VOLUMES,\
-    ISSUES_ONLY, INDIVIDUAL_PUBLCATIONS
+    ISSUES_ONLY, INDIVIDUAL_PUBLICATIONS
 
 
 class JournalQuerySet(models.QuerySet):
@@ -17,7 +17,7 @@ class JournalQuerySet(models.QuerySet):
         return self.filter(structure__in=(ISSUES_AND_VOLUMES, ISSUES_ONLY))
 
     def has_individual_publications(self):
-        return self.filter(structure=INDIVIDUAL_PUBLCATIONS)
+        return self.filter(structure=INDIVIDUAL_PUBLICATIONS)
 
 
 class IssueQuerySet(models.QuerySet):
diff --git a/journals/migrations/0042_auto_20180923_2130.py b/journals/migrations/0042_auto_20180923_2130.py
new file mode 100644
index 0000000000000000000000000000000000000000..70e6f49568399481e69c1b15c9dd0fab5addaa31
--- /dev/null
+++ b/journals/migrations/0042_auto_20180923_2130.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2018-09-23 19:30
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('journals', '0041_auto_20180922_1609'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='journal',
+            name='structure',
+            field=models.CharField(choices=[('IV', 'Issues and Volumes'), ('IO', 'Issues only'), ('IP', 'Individual Publications')], default='IV', max_length=2),
+        ),
+        migrations.AlterField(
+            model_name='publication',
+            name='doi_label',
+            field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(SciPostPhysProc|SciPostPhysSel|SciPostPhysLectNotes|SciPostPhys).[0-9]+((.[0-9]+)?.[0-9]{3,})?$', 'Only valid DOI expressions are allowed (`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')]),
+        ),
+    ]
diff --git a/journals/migrations/0043_auto_20180927_2014.py b/journals/migrations/0043_auto_20180927_2014.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d2f820e30ebfac90d7e8f30c897372310682d45
--- /dev/null
+++ b/journals/migrations/0043_auto_20180927_2014.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2018-09-27 18:14
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('journals', '0042_auto_20180923_2130'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='issue',
+            name='slug',
+            field=models.SlugField(null=True),
+        ),
+        migrations.AlterField(
+            model_name='issue',
+            name='doi_label',
+            field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^[a-zA-Z]+(.[0-9]+)?.[0-9]+$', 'Only valid DOI expressions are allowed ([a-zA-Z]+.[0-9]+.[0-9]+).')]),
+        ),
+    ]
diff --git a/journals/migrations/0044_auto_20180927_2050.py b/journals/migrations/0044_auto_20180927_2050.py
new file mode 100644
index 0000000000000000000000000000000000000000..480657815b90971c7b5880d1f1ab676201106be1
--- /dev/null
+++ b/journals/migrations/0044_auto_20180927_2050.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2018-09-27 18:50
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def update_issue_slugs(apps, schema_editor):
+    """Fill all slug fields for existing Issue instances."""
+    Issue = apps.get_model('journals', 'Issue')
+
+    for issue in Issue.objects.all():
+        Issue.objects.filter(id=issue.id).update(slug=issue.number)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('journals', '0043_auto_20180927_2014'),
+    ]
+
+    operations = [
+        migrations.RunPython(update_issue_slugs),
+    ]
diff --git a/journals/migrations/0045_auto_20180927_2055.py b/journals/migrations/0045_auto_20180927_2055.py
new file mode 100644
index 0000000000000000000000000000000000000000..75588da9e0c49ff393176555c70afbb3289215f6
--- /dev/null
+++ b/journals/migrations/0045_auto_20180927_2055.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2018-09-27 18:55
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('journals', '0044_auto_20180927_2050'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='issue',
+            name='doi_label',
+            field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^[a-zA-Z]+.\\w+(.[0-9]+)?$', 'Only valid DOI expressions are allowed ([a-zA-Z]+.\\w+(.[0-9]+)?)')]),
+        ),
+        migrations.AlterField(
+            model_name='issue',
+            name='slug',
+            field=models.SlugField(),
+        ),
+        migrations.AlterField(
+            model_name='publication',
+            name='doi_label',
+            field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(SciPostPhysProc|SciPostPhysSel|SciPostPhysLectNotes|SciPostPhys)(.\\w+(.[0-9]+(.[0-9]{3,})?)?)?$', 'Only valid DOI expressions are allowed: `[a-zA-Z]+(.\\w+(.[0-9]+(.[0-9]{3,})?)?)?`')]),
+        ),
+    ]
diff --git a/journals/models.py b/journals/models.py
index 95ffaac4bc4ad643d53d1c9f5d0e3b1929d5e44b..2e13ad41a8761f4dff24cb2243357312fb1ee8fd 100644
--- a/journals/models.py
+++ b/journals/models.py
@@ -256,6 +256,7 @@ class Issue(models.Model):
         'journals.Volume', on_delete=models.CASCADE, null=True, blank=True,
         help_text='Assign either an Volume or Journal to the Issue')
     number = models.PositiveSmallIntegerField()
+    slug = models.SlugField()
     start_date = models.DateField(default=timezone.now)
     until_date = models.DateField(default=timezone.now)
     status = models.CharField(max_length=20, choices=ISSUE_STATUSES, default=STATUS_PUBLISHED)
@@ -308,11 +309,15 @@ class Issue(models.Model):
 
     @property
     def issue_number(self):
-        return '%s issue %s' % (self.in_volume, self.number)
+        if self.in_volume:
+            return '%s issue %s' % (self.in_volume, self.number)
+        return '%s issue %s' % (self.in_journal, self.number)
 
     @property
     def short_str(self):
-        return 'Vol. %s issue %s' % (self.in_volume.number, self.number)
+        if self.in_volume:
+            return 'Vol. %s issue %s' % (self.in_volume.number, self.number)
+        return 'Issue %s' % self.number
 
     @property
     def period_as_string(self):
@@ -321,8 +326,8 @@ class Issue(models.Model):
         return '%s - %s' % (self.start_date.strftime('%B'), self.until_date.strftime('%B %Y'))
 
     def is_current(self):
-        return self.start_date <= timezone.now().date() and\
-               self.until_date >= timezone.now().date()
+        today = timezone.now().date()
+        return self.start_date <= today and self.until_date >= today
 
     def nr_publications(self, tier=None):
         publications = Publication.objects.filter(in_issue=self)
@@ -463,14 +468,14 @@ class Publication(models.Model):
                 'in_issue': ValidationError(
                     'Either assign only a Journal or Issue to this Publication', code='invalid'),
             })
-        if self.in_issue and not self.in_issue.in_volume.in_journal.has_issues:
+        if self.in_issue and not self.get_journal().has_issues:
             # Assigning both a Journal and an Issue will screw up the database
             raise ValidationError({
                 'in_issue': ValidationError(
                     'This journal does not allow the use of Issues',
                     code='invalid'),
             })
-        if self.in_journal and self.in_journal.has_issues:
+        if self.in_journal and self.get_journal().has_issues:
             # Assigning both a Journal and an Issue will screw up the database
             raise ValidationError({
                 'in_journal': ValidationError(
@@ -548,12 +553,18 @@ class Publication(models.Model):
         """
         Return Publication name in the preferred citation format.
         """
-        if self.in_issue:
+        if self.in_issue and self.in_issue.in_volume:
             return '{journal} {volume}, {paper_nr} ({year})'.format(
                 journal=self.in_issue.in_volume.in_journal.abbreviation_citation,
                 volume=self.in_issue.in_volume.number,
                 paper_nr=self.get_paper_nr(),
                 year=self.publication_date.strftime('%Y'))
+        elif self.in_issue and self.in_issue.in_journal:
+            return '{journal} {issue}, {paper_nr} ({year})'.format(
+                journal=self.in_issue.in_journal.abbreviation_citation,
+                issue=self.in_issue.number,
+                paper_nr=self.get_paper_nr(),
+                year=self.publication_date.strftime('%Y'))
         elif self.in_journal:
             return '{journal} {paper_nr} ({year})'.format(
                 journal=self.in_journal.abbreviation_citation,
@@ -564,7 +575,11 @@ class Publication(models.Model):
             year=self.publication_date.strftime('%Y'))
 
     def get_journal(self):
-        return self.in_journal or self.in_issue.in_volume.in_journal
+        if self.in_journal:
+            return self.in_journal
+        elif self.in_issue.in_journal:
+            return self.in_issue.in_journal
+        return self.in_issue.in_volume.in_journal
 
     def journal_issn(self):
         return self.get_journal().issn
@@ -581,7 +596,7 @@ class Publication(models.Model):
         if self.citedby and self.latest_citedby_update:
             ncites = len(self.citedby)
             deltat = (self.latest_citedby_update.date() - self.publication_date).days
-            return (ncites * 365.25/deltat)
+            return (ncites * 365.25 / deltat)
         else:
             return 0
 
diff --git a/journals/templates/xml/publication_crossref.html b/journals/templates/xml/publication_crossref.html
index 10f70218493076a53c3ac4683a8f67678b2cec0d..26e49c9c2dadf0e8289493cc40de4dfe8bee10b6 100644
--- a/journals/templates/xml/publication_crossref.html
+++ b/journals/templates/xml/publication_crossref.html
@@ -16,7 +16,7 @@
                 <journal_metadata>
                     <full_title>{{ publication.in_issue.in_volume.in_journal.get_name_display }}</full_title>
                     <abbrev_title>{{ publication.in_issue.in_volume.in_journal.abbreviation_citation }}</abbrev_title>
-                    <issn media_type='electronic'>{{ publication.in_issue.in_volume.in_journal.issn }}</issn>
+                    {% if publication.in_journal.issn %}<issn media_type='electronic'>{{ publication.in_journal.issn }}</issn>{% endif %}
                     <doi_data>
                         <doi>{{ publication.in_issue.in_volume.in_journal.doi_string }}</doi>
                         <resource>https://scipost.org/{{ publication.in_issue.in_volume.in_journal.doi_string }}</resource>
diff --git a/journals/views.py b/journals/views.py
index d4a59d242916b45a98b900e59cedcd3f66ff4706..e660f9e2830cefe73995f8c4743f84d126f49ead 100644
--- a/journals/views.py
+++ b/journals/views.py
@@ -24,13 +24,13 @@ from django.db.models import Q
 from django.http import Http404, HttpResponse
 from django.utils import timezone
 from django.utils.decorators import method_decorator
-from django.views.generic.base import TemplateView
 from django.views.generic.detail import DetailView
 from django.views.generic.edit import UpdateView
 from django.views.generic.list import ListView
 from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
 
-from .constants import STATUS_DRAFT, PUBLICATION_PREPUBLISHED
+from .constants import STATUS_DRAFT, ISSUES_AND_VOLUMES, ISSUES_ONLY, INDIVIDUAL_PUBLICATIONS
+from .exceptions import InvalidDOIError
 from .models import Journal, Issue, Publication, Deposit, DOAJDeposit,\
                     GenericDOIDeposit, PublicationAuthorsTable, OrgPubFraction
 from .forms import AbstractJATSForm, FundingInfoForm,\
@@ -57,12 +57,59 @@ from scipost.mixins import PermissionsMixin, RequestViewMixin, PaginationMixin
 from guardian.decorators import permission_required
 
 
+def doi_dispatch(request, journal_tag, part_1=None, part_2=None, part_3=None):
+    """
+    Dispatch given DOI route to the appropriate view according to the Journal's settings.
+
+    journal_tag -- Part of the DOI right before the first period.
+    part_1 (optional) -- Part of the DOI after the first period.
+    part_2 (optional) -- Part of the DOI after the second period.
+    part_3 (optional) -- Part of the DOI after the third period.
+    """
+    journal = get_object_or_404(Journal, doi_label=journal_tag)
+    if part_1 is None:
+        # This DOI refers to a Journal landing page.
+        return landing_page(request, journal_tag)
+    elif part_2 is None:
+        doi_label = '{0}.{1}'.format(journal_tag, part_1)
+
+        if journal.structure == INDIVIDUAL_PUBLICATIONS:
+            # Publication DOI for invidivual publication Journals.
+            return publication_detail(request, doi_label)
+        elif journal.structure == ISSUES_ONLY:
+            # Issue DOI for Issue only Journals.
+            return issue_detail(request, doi_label)
+
+        # The third option: a `Issue and Volume Journal DOI` would lead to a "volume detail page",
+        # but that does not exist. Redirect to the Journal landing page instead.
+        return landing_page(request, journal_tag)
+    elif part_3 is None:
+        doi_label = '{0}.{1}.{2}'.format(journal_tag, part_1, part_2)
+
+        if journal.structure == ISSUES_AND_VOLUMES:
+            # Issue DOI for Issue+Volumes Journals.
+            return issue_detail(request, doi_label)
+        elif journal.structure == ISSUES_ONLY:
+            # Publication DOI for Issue only Journals.
+            return publication_detail(request, doi_label)
+    else:
+        doi_label = '{0}.{1}.{2}.{3}'.format(journal_tag, part_1, part_2, part_3)
+        return publication_detail(request, doi_label)
+
+    # Invalid db configure
+    raise InvalidDOIError({
+        'journal_tag': journal_tag,
+        'part_1': part_1,
+        'part_2': part_2,
+        'part_3': part_3})
+
+
 ############
 # Journals
 ############
 
 def journals(request):
-    '''Main landing page for Journals application.'''
+    """Main landing page for Journals application."""
     context = {'journals': Journal.objects.order_by('name')}
     return render(request, 'journals/journals.html', context)
 
@@ -149,15 +196,15 @@ def about(request, doi_label):
 
 def issue_detail(request, doi_label):
     issue = get_object_or_404(Issue.objects.published(), doi_label=doi_label)
-    journal = issue.in_volume.in_journal
+    journal = issue.in_journal or issue.in_volume.in_journal
 
     papers = issue.publications.published().order_by('paper_nr')
-    next_issue = Issue.objects.published().filter(
-        in_volume__in_journal=journal, start_date__gt=issue.start_date
-        ).order_by('start_date').first()
-    prev_issue = Issue.objects.published().filter(
-        in_volume__in_journal=journal, start_date__lt=issue.start_date
-        ).order_by('start_date').last()
+    next_issue = Issue.objects.published().filter(start_date__gt=issue.start_date).filter(
+        Q(in_volume__in_journal=journal) | Q(in_journal=journal),
+        ).distinct().order_by('start_date').first()
+    prev_issue = Issue.objects.published().filter(start_date__lt=issue.start_date).filter(
+        Q(in_volume__in_journal=journal) | Q(in_journal=journal)
+        ).distinct().order_by('start_date').last()
 
     context = {
         'issue': issue,
@@ -365,24 +412,6 @@ class AuthorAffiliationView(PublicationMixin, PermissionsMixin, DetailView):
         context['add_affiliation_form'] = AuthorsTableOrganizationSelectForm()
         return context
 
-# NOT WORKING, using the FBV below instead
-# class AddAffiliationView(PermissionsMixin, UpdateView):
-#     """
-#     Add an affiliation to a PublicationAuthorsTable instance.
-#     """
-#     permission_required = 'scipost.can_draft_publication'
-#     model = PublicationAuthorsTable
-#     form_class = AuthorsTableOrganizationSelectForm
-#     template_name = 'journals/author_affiliations.html'
-
-#     def form_valid(self, form):
-#         self.object.affiliations.add(form.cleaned_data['organization'])
-#         return super().form_valid(form)
-
-#     def get_success_url(self):
-#         return reverse_lazy('journals:author_affiliations',
-#                             kwargs={'doi_label': self.kwargs['.doi_label']})
-
 
 @permission_required('scipost.can_draft_publication', return_403=True)
 @transaction.atomic
@@ -755,7 +784,7 @@ def allocate_orgpubfractions(request, doi_label):
     elif not (request.user == publication.accepted_submission.submitted_by.user or
               request.user.has_perm('scipost.can_publish_accepted_submission')):
         raise Http404
-    initial = []
+
     if not publication.pubfractions.all().exists():
         # Create new OrgPubFraction objects from existing data, spreading weight evenly
         for org in publication.get_organizations():
@@ -1232,20 +1261,12 @@ def publication_detail(request, doi_label):
     visible for Production Supervisors and Administrators if in draft.
     """
     publication = get_object_or_404(Publication, doi_label=doi_label)
-    if not publication.is_published  and not publication.status == PUBLICATION_PREPUBLISHED:
-        if not request.user.has_perm('scipost.can_draft_publication'):
-            raise Http404('Publication is not publicly visible')
-
-    if publication.in_issue:
-        journal = publication.in_issue.in_volume.in_journal
-    elif publication.in_journal:
-        journal = publication.in_journal
-    else:
-        raise Http404('Publication configuration is valid')
+    if not publication.is_published and not request.user.has_perm('scipost.can_draft_publication'):
+        raise Http404('Publication is not publicly visible')
 
     context = {
         'publication': publication,
-        'journal': journal
+        'journal': publication.get_journal(),
     }
     return render(request, 'journals/publication_detail.html', context)
 
diff --git a/package.json b/package.json
index f2c64c62424bf4e46e3382a94e50c3c2f4ea1266..d1b19598bf1e9ce5f936f6408157a529a47d3e6b 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,7 @@
   "homepage": "https://www.scipost.org",
   "devDependencies": {
     "ajv": "^5.2.2",
-    "bootstrap": "^4.1.0",
-    "bootstrap-loader": "^2.1.0",
+    "bootstrap": "^4.1.3",
     "clean-webpack-plugin": "^0.1.15",
     "css-loader": "^0.28.4",
     "enhanced-resolve": "^3.4.1",
@@ -41,13 +40,15 @@
     "style-loader": "^0.13.1",
     "tapable": "^0.2.8",
     "tether": "^1.4.0",
-    "url-loader": "^0.5.7",
+    "url-loader": "^1.1.1",
     "webpack": "^3.5.4",
-    "webpack-bundle-tracker": "^0.2.0",
+    "webpack-bundle-tracker": "^0.3.0",
     "webpack-glob-entry": "^2.1.1"
   },
   "dependencies": {
-    "npm": "^5.10.0",
+    "bootstrap-loader": "^2.2.0",
+    "npm": "^6.4.1",
+    "npm-install-peers": "^1.2.1",
     "schema-utils": "^0.3.0"
   }
 }
diff --git a/proceedings/models.py b/proceedings/models.py
index abff3df6b1c8749c04ae2845c884b7fcf10ee919..cf5d45b61e420def2a9a74868e7b89063b16e46c 100644
--- a/proceedings/models.py
+++ b/proceedings/models.py
@@ -20,7 +20,7 @@ class Proceedings(TimeStampedModel):
     # Link to the actual Journal platform
     issue = models.OneToOneField(
         'journals.Issue', related_name='proceedings',
-        limit_choices_to={'in_volume__in_journal__name': 'SciPostPhysProc'})
+        limit_choices_to=models.Q(in_volume__in_journal__name='SciPostPhysProc') | models.Q(in_journal__name='SciPostPhysProc'))
     minimum_referees = models.PositiveSmallIntegerField(
         help_text='Require an explicit minimum number of referees for the default ref cycle.',
         blank=True, null=True)
diff --git a/requirements.txt b/requirements.txt
index 176e10765b2e5252f3879571ba27bc11e07e7cb3..388e33d5cfc3018c8b67c0976ab9684b9fcceb5d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,7 +16,7 @@ django-debug-toolbar==1.8
 django-extensions==1.7.6
 django-filter==1.0.4
 django-guardian==1.4.9
-django-mathjax==0.0.5  # This thing looks dead as well
+django-mathjax==0.0.8
 django-mptt==0.8.6  # Dead
 django-sphinxdoc==1.5.1
 django-silk==2.0.0
@@ -34,7 +34,7 @@ sphinx-rtd-theme==0.1.9  # Sphinx theme
 
 # Testing
 factory-boy==2.10.0
-Faker==0.8.12
+Faker==0.8.1
 
 
 # Django Utils
diff --git a/scipost/urls.py b/scipost/urls.py
index e6e4183ba01daeb61f30a4510e5a6186a8c7f0fc..0aeb24a4e3b11cef3d315d86f86a96ee006d5c05 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -11,7 +11,7 @@ from .feeds import LatestNewsFeedRSS, LatestNewsFeedAtom, LatestCommentsFeedRSS,
                    LatestPublicationsFeedRSS, LatestPublicationsFeedAtom
 
 from journals import views as journals_views
-from journals.constants import REGEX_CHOICES, PUBLICATION_DOI_REGEX
+from journals.constants import REGEX_CHOICES, PUBLICATION_DOI_REGEX, DOI_DISPATCH_REGEX, DOI_ISSUE_REGEX
 from submissions import views as submission_views
 
 JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES
@@ -193,6 +193,10 @@ urlpatterns = [
         name='author_reply_detail'),
 
     # Publication detail (+pdf)
+    url(r'^10.21468/{regex}$'.format(regex=DOI_DISPATCH_REGEX),
+        journals_views.doi_dispatch, name='doi_dispatch'),
+    url(r'^{regex}$'.format(regex=DOI_DISPATCH_REGEX),
+        journals_views.doi_dispatch, name='doi_dispatch'),
     url(r'^10.21468/(?P<doi_label>{regex})$'.format(regex=PUBLICATION_DOI_REGEX),
         journals_views.publication_detail,
         name='publication_detail'),
@@ -207,9 +211,9 @@ urlpatterns = [
         name='publication_pdf'),
 
     # Journal issue
-    url(r'^10.21468/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9])$',
+    url(r'^10.21468/{regex}$'.format(regex=DOI_ISSUE_REGEX),
         journals_views.issue_detail, name='issue_detail'),
-    url(r'^(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9])$',
+    url(r'^{regex}$'.format(regex=DOI_ISSUE_REGEX),
         journals_views.issue_detail, name='issue_detail'),
 
     # Journal landing page