diff --git a/CHANGES.rst b/CHANGES.rst
index f9f1ea038b8bcb05beea1d1f3b65c828f2f628b4..df50fc8225c3e65c223c57c8c36ff47524d7b0ea 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,7 +1,19 @@
+v38.3.1
+-------
+
+* #1231: Removed warning when PYTHONDONTWRITEBYTECODE is enabled.
+
+v38.3.0
+-------
+
+* #1210: Add support for PEP 345 Project-URL metadata.
+* #1207: Add support for ``long_description_type`` to setup.cfg
+  declarative config as intended and documented.
+
 v38.2.5
 -------
 
-* Removed warning when PYTHONDONTWRITEBYTECODE is enabled
+* #1232: Fix trailing slash handling in ``pkg_resources.ZipProvider``.
 
 v38.2.4
 -------
diff --git a/docs/setuptools.txt b/docs/setuptools.txt
index c2822c4f9e8cf164f2128fed6b91eef2f46cd321..bea8018195910801fdd4cc1efb3e4af583014c0e 100644
--- a/docs/setuptools.txt
+++ b/docs/setuptools.txt
@@ -145,6 +145,11 @@ dependencies, and perhaps some data files and scripts::
         license="PSF",
         keywords="hello world example examples",
         url="http://example.com/HelloWorld/",   # project home page, if any
+        project_urls={
+            "Bug Tracker": "https://bugs.example.com/HelloWorld/",
+            "Documentation": "https://docs.example.com/HelloWorld/",
+            "Source Code": "https://code.example.com/HelloWorld/",
+        }
 
         # could also include long_description, download_url, classifiers, etc.
     )
@@ -408,6 +413,11 @@ unless you need the associated ``setuptools`` feature.
     A list of modules to search for additional fixers to be used during
     the 2to3 conversion. See :doc:`python3` for more details.
 
+``project_urls``
+    An arbitrary map of URL names to hyperlinks, allowing more extensible
+    documentation of where various resources can be found than the simple
+    ``url`` and ``download_url`` options provide.
+
 
 Using ``find_packages()``
 -------------------------
@@ -2406,6 +2416,7 @@ name                                               str
 version                                            attr:, str
 url                             home-page          str
 download_url                    download-url       str
+project_urls                                       dict
 author                                             str
 author_email                    author-email       str
 maintainer                                         str
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index 73334641b5675510711b30d26ab7834b7990fd1a..08f9bbe7ef55e38f910e7a79af2d6da290488f9b 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -1693,6 +1693,9 @@ class ZipProvider(EggProvider):
     def _zipinfo_name(self, fspath):
         # Convert a virtual filename (full path to file) into a zipfile subpath
         # usable with the zipimport directory cache for our target archive
+        fspath = fspath.rstrip(os.sep)
+        if fspath == self.loader.archive:
+            return ''
         if fspath.startswith(self.zip_pre):
             return fspath[len(self.zip_pre):]
         raise AssertionError(
diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py
index c6a7ac97e93467b60dbd8e419bf53142881c8cc8..f2c00b297b6ebe66b7ee0df44b0f562b75cbdc0d 100644
--- a/pkg_resources/tests/test_pkg_resources.py
+++ b/pkg_resources/tests/test_pkg_resources.py
@@ -62,10 +62,21 @@ class TestZipProvider(object):
         zip_info.filename = 'data.dat'
         zip_info.date_time = cls.ref_time.timetuple()
         zip_egg.writestr(zip_info, 'hello, world!')
+        zip_info = zipfile.ZipInfo()
+        zip_info.filename = 'subdir/mod2.py'
+        zip_info.date_time = cls.ref_time.timetuple()
+        zip_egg.writestr(zip_info, 'x = 6\n')
+        zip_info = zipfile.ZipInfo()
+        zip_info.filename = 'subdir/data2.dat'
+        zip_info.date_time = cls.ref_time.timetuple()
+        zip_egg.writestr(zip_info, 'goodbye, world!')
         zip_egg.close()
         egg.close()
 
         sys.path.append(egg.name)
+        subdir = os.path.join(egg.name, 'subdir')
+        sys.path.append(subdir)
+        cls.finalizers.append(EggRemover(subdir))
         cls.finalizers.append(EggRemover(egg.name))
 
     @classmethod
@@ -73,6 +84,30 @@ class TestZipProvider(object):
         for finalizer in cls.finalizers:
             finalizer()
 
+    def test_resource_listdir(self):
+        import mod
+        zp = pkg_resources.ZipProvider(mod)
+
+        expected_root = ['data.dat', 'mod.py', 'subdir']
+        assert sorted(zp.resource_listdir('')) == expected_root
+        assert sorted(zp.resource_listdir('/')) == expected_root
+
+        expected_subdir = ['data2.dat', 'mod2.py']
+        assert sorted(zp.resource_listdir('subdir')) == expected_subdir
+        assert sorted(zp.resource_listdir('subdir/')) == expected_subdir
+
+        assert zp.resource_listdir('nonexistent') == []
+        assert zp.resource_listdir('nonexistent/') == []
+
+        import mod2
+        zp2 = pkg_resources.ZipProvider(mod2)
+
+        assert sorted(zp2.resource_listdir('')) == expected_subdir
+        assert sorted(zp2.resource_listdir('/')) == expected_subdir
+
+        assert zp2.resource_listdir('subdir') == []
+        assert zp2.resource_listdir('subdir/') == []
+
     def test_resource_filename_rewrites_on_change(self):
         """
         If a previous call to get_resource_filename has saved the file, but
diff --git a/setup.cfg b/setup.cfg
index d6f1a195b539a529c078f92c03bff70f6da30fc3..13b520a898cff5521a71506cda0e6d0e4b887e7a 100755
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 38.2.4
+current_version = 38.3.0
 commit = True
 tag = True
 
diff --git a/setup.py b/setup.py
index 0e3e42c5367b91d25c9313b7d99a8b71f6d3ad4f..e2b5dc90a22e765adce127df7fcc1677cd13816e 100755
--- a/setup.py
+++ b/setup.py
@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
 
 setup_params = dict(
     name="setuptools",
-    version="38.2.4",
+    version="38.3.0",
     description="Easily download, build, install, upgrade, and uninstall "
         "Python packages",
     author="Python Packaging Authority",
@@ -98,6 +98,9 @@ setup_params = dict(
     long_description_content_type='text/x-rst; charset=UTF-8',
     keywords="CPAN PyPI distutils eggs package management",
     url="https://github.com/pypa/setuptools",
+    project_urls={
+        "Documentation": "https://setuptools.readthedocs.io/",
+    },
     src_root=None,
     packages=setuptools.find_packages(exclude=['*.tests']),
     package_data=package_data,
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index 103c5f20b97b032ba8ca5bca21733358858cb535..befa09043a9fd73bd7a6bdfecef2b076e6402883 100755
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -597,10 +597,7 @@ def write_pkg_info(cmd, basename, filename):
         metadata = cmd.distribution.metadata
         metadata.version, oldver = cmd.egg_version, metadata.version
         metadata.name, oldname = cmd.egg_name, metadata.name
-        metadata.long_description_content_type = getattr(
-            cmd.distribution,
-            'long_description_content_type'
-        )
+
         try:
             # write unescaped data to PKG-INFO, so older pkg_resources
             # can still parse it
diff --git a/setuptools/config.py b/setuptools/config.py
index 5382844769eb5e25ff9a0afe0d5c49ba7dfcbd7f..a70794a42ef0068685c9788e02aa70d12e6a14e5 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -404,6 +404,7 @@ class ConfigMetadataHandler(ConfigHandler):
         """Metadata item name to parser function mapping."""
         parse_list = self._parse_list
         parse_file = self._parse_file
+        parse_dict = self._parse_dict
 
         return {
             'platforms': parse_list,
@@ -416,6 +417,7 @@ class ConfigMetadataHandler(ConfigHandler):
             'description': parse_file,
             'long_description': parse_file,
             'version': self._parse_version,
+            'project_urls': parse_dict,
         }
 
     def _parse_version(self, value):
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 477f93ddbaade73c69b64b578d08903d5142b3d7..c2bfdbc7c66090d096d0515b5ca7454f7c2cf72c 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -44,7 +44,7 @@ def write_pkg_file(self, file):
             self.classifiers or self.download_url):
         version = '1.1'
     # Setuptools specific for PEP 345
-    if hasattr(self, 'python_requires'):
+    if hasattr(self, 'python_requires') or self.project_urls:
         version = '1.2'
 
     file.write('Metadata-Version: %s\n' % version)
@@ -57,12 +57,11 @@ def write_pkg_file(self, file):
     file.write('License: %s\n' % self.get_license())
     if self.download_url:
         file.write('Download-URL: %s\n' % self.download_url)
+    for project_url in self.project_urls.items():
+        file.write('Project-URL: %s, %s\n' % project_url)
 
-    long_desc_content_type = getattr(
-        self,
-        'long_description_content_type',
-        None
-    ) or 'UNKNOWN'
+    long_desc_content_type = \
+        self.long_description_content_type or 'UNKNOWN'
     file.write('Description-Content-Type: %s\n' % long_desc_content_type)
 
     long_desc = rfc822_escape(self.get_long_description())
@@ -326,14 +325,21 @@ class Distribution(Distribution_parse_config_files, _Distribution):
         self.dist_files = []
         self.src_root = attrs.pop("src_root", None)
         self.patch_missing_pkg_info(attrs)
-        self.long_description_content_type = attrs.get(
-            'long_description_content_type'
-        )
+        self.project_urls = attrs.get('project_urls', {})
         self.dependency_links = attrs.pop('dependency_links', [])
         self.setup_requires = attrs.pop('setup_requires', [])
         for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
             vars(self).setdefault(ep.name, None)
         _Distribution.__init__(self, attrs)
+
+        # The project_urls attribute may not be supported in distutils, so
+        # prime it here from our value if not automatically set
+        self.metadata.project_urls = getattr(
+            self.metadata, 'project_urls', self.project_urls)
+        self.metadata.long_description_content_type = attrs.get(
+            'long_description_content_type'
+        )
+
         if isinstance(self.metadata.version, numbers.Number):
             # Some people apparently take "version number" too literally :)
             self.metadata.version = str(self.metadata.version)
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
index cdfa5af43bec84ad484b85110a071360f7985bf5..383e0d3071c579b8ddc6c2cfec5eef4c3bf004a1 100644
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -110,6 +110,7 @@ class TestMetadata:
             '[metadata]\n'
             'version = 10.1.1\n'
             'description = Some description\n'
+            'long_description_content_type = text/something\n'
             'long_description = file: README\n'
             'name = fake_name\n'
             'keywords = one, two\n'
@@ -131,6 +132,7 @@ class TestMetadata:
 
             assert metadata.version == '10.1.1'
             assert metadata.description == 'Some description'
+            assert metadata.long_description_content_type == 'text/something'
             assert metadata.long_description == 'readme contents\nline2'
             assert metadata.provides == ['package', 'package.sub']
             assert metadata.license == 'BSD 3-Clause License'
@@ -215,6 +217,22 @@ class TestMetadata:
                 'Programming Language :: Python :: 3.5',
             ]
 
+    def test_dict(self, tmpdir):
+
+        fake_env(
+            tmpdir,
+            '[metadata]\n'
+            'project_urls =\n'
+            '  Link One = https://example.com/one/\n'
+            '  Link Two = https://example.com/two/\n'
+        )
+        with get_dist(tmpdir) as dist:
+            metadata = dist.metadata
+            assert metadata.project_urls == {
+                'Link One': 'https://example.com/one/',
+                'Link Two': 'https://example.com/two/',
+            }
+
     def test_version(self, tmpdir):
 
         _, config = fake_env(
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index 66ca9164d5ee8d9fb8fd7c2b5547b98fe0455dd2..7d12434e1f0d6b1dccb3e03a4e3f980bd5881635 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -445,6 +445,36 @@ class TestEggInfo(object):
         expected_line = 'Description-Content-Type: text/markdown'
         assert expected_line in pkg_info_lines
 
+    def test_project_urls(self, tmpdir_cwd, env):
+        # Test that specifying a `project_urls` dict to the `setup`
+        # function results in writing multiple `Project-URL` lines to
+        # the `PKG-INFO` file in the `<distribution>.egg-info`
+        # directory.
+        # `Project-URL` is described at https://packaging.python.org
+        #     /specifications/core-metadata/#project-url-multiple-use
+
+        self._setup_script_with_requires(
+            """project_urls={
+                'Link One': 'https://example.com/one/',
+                'Link Two': 'https://example.com/two/',
+                },""")
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        code, data = environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
+            pkg_info_lines = pkginfo_file.read().split('\n')
+        expected_line = 'Project-URL: Link One, https://example.com/one/'
+        assert expected_line in pkg_info_lines
+        expected_line = 'Project-URL: Link Two, https://example.com/two/'
+        assert expected_line in pkg_info_lines
+
     def test_python_requires_egg_info(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
             """python_requires='>=2.7.12',""")