diff --git a/datasette/app.py b/datasette/app.py index 2a5d569e..2d02e705 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -17,6 +17,7 @@ from markupsafe import Markup import jinja2 from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader, escape from jinja2.environment import Template +from jinja2.exceptions import TemplateNotFound import uvicorn from .views.base import DatasetteError, ureg, AsgiRouter @@ -745,7 +746,7 @@ class DatasetteRouter(AsgiRouter): path = "/" + path[len(base_url) :] return await super().route_path(scope, receive, send, path) - async def handle_404(self, scope, receive, send): + async def handle_404(self, scope, receive, send, exception=None): # If URL has a trailing slash, redirect to URL without it path = scope.get("raw_path", scope["path"].encode("utf8")) if path.endswith(b"/"): @@ -754,7 +755,20 @@ class DatasetteRouter(AsgiRouter): path += b"?" + scope["query_string"] await asgi_send_redirect(send, path.decode("latin1")) else: - await super().handle_404(scope, receive, send) + # Is there a pages/* template matching this path? + template_path = os.path.join("pages", *scope["path"].split("/")) + ".html" + try: + template = self.ds.jinja_env.select_template([template_path]) + except TemplateNotFound: + template = None + if template: + await asgi_send_html( + send, await self.ds.render_template(template), status=200 + ) + else: + await self.handle_500( + scope, receive, send, exception or NotFound("404") + ) async def handle_500(self, scope, receive, send, exception): title = None diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 817c7dd8..ed3138d0 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -119,11 +119,13 @@ class AsgiRouter: new_scope = dict(scope, url_route={"kwargs": match.groupdict()}) try: return await view(new_scope, receive, send) + except NotFound as exception: + return await self.handle_404(scope, receive, send, exception) except Exception as exception: return await self.handle_500(scope, receive, send, exception) return await self.handle_404(scope, receive, send) - async def handle_404(self, scope, receive, send): + async def handle_404(self, scope, receive, send, exception=None): await send( { "type": "http.response.start", diff --git a/datasette/views/base.py b/datasette/views/base.py index 2478bd84..c81af938 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -28,6 +28,7 @@ from datasette.utils.asgi import ( AsgiRouter, AsgiView, NotFound, + NotFound, Response, ) diff --git a/tests/test_html.py b/tests/test_html.py index 1b675d3c..785d2d97 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1241,3 +1241,20 @@ def test_base_url_config(base_url, path): "href_or_src": href, "element_parent": str(el.parent), } + + +def test_custom_template_page(tmpdir): + template_dir = tmpdir.mkdir("page-templates") + pages_dir = template_dir.mkdir("pages") + (pages_dir / "about.html").write_text("ABOUT!", "utf-8") + nested_dir = pages_dir.mkdir("nested") + (nested_dir / "nest.html").write_text("Nest!", "utf-8") + for client in make_app_client(template_dir=str(template_dir)): + response = client.get("/about") + assert 200 == response.status + assert "ABOUT!" == response.text + response = client.get("/nested/nest") + assert 200 == response.status + assert "Nest!" == response.text + response = client.get("/nested/nest2") + assert 404 == response.status