diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py
index 1141ca75..5a8439b4 100644
--- a/datasette/hookspecs.py
+++ b/datasette/hookspecs.py
@@ -155,6 +155,11 @@ def database_actions(datasette, actor, database, request):
"""Links for the database actions menu"""
+@hookspec
+def homepage_actions(datasette, actor, request):
+ """Links for the homepage actions menu"""
+
+
@hookspec
def skip_csrf(datasette, scope):
"""Mechanism for skipping CSRF checks for certain requests"""
diff --git a/datasette/templates/database.html b/datasette/templates/database.html
index 02e6fb3d..b5a0edc4 100644
--- a/datasette/templates/database.html
+++ b/datasette/templates/database.html
@@ -44,7 +44,6 @@
{% endif %}
-
{{ top_database() }}
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
diff --git a/datasette/templates/index.html b/datasette/templates/index.html
index 203abca8..d08cdb10 100644
--- a/datasette/templates/index.html
+++ b/datasette/templates/index.html
@@ -7,6 +7,37 @@
{% block content %}
{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}
+{% set links = homepage_actions %}{% if links %}
+
+{% endif %}
+
{{ top_homepage() }}
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
diff --git a/datasette/views/index.py b/datasette/views/index.py
index 2cb18b1c..6546b7ae 100644
--- a/datasette/views/index.py
+++ b/datasette/views/index.py
@@ -1,6 +1,12 @@
import json
-from datasette.utils import add_cors_headers, make_slot_function, CustomJSONEncoder
+from datasette.plugins import pm
+from datasette.utils import (
+ add_cors_headers,
+ await_me_maybe,
+ make_slot_function,
+ CustomJSONEncoder,
+)
from datasette.utils.asgi import Response
from datasette.version import __version__
@@ -131,6 +137,15 @@ class IndexView(BaseView):
headers=headers,
)
else:
+ homepage_actions = []
+ for hook in pm.hook.homepage_actions(
+ datasette=self.ds,
+ actor=request.actor,
+ request=request,
+ ):
+ extra_links = await await_me_maybe(hook)
+ if extra_links:
+ homepage_actions.extend(extra_links)
return await self.render(
["index.html"],
request=request,
@@ -144,5 +159,6 @@ class IndexView(BaseView):
"top_homepage": make_slot_function(
"top_homepage", self.ds, request
),
+ "homepage_actions": homepage_actions,
},
)
diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst
index 91db80f8..4ac391ef 100644
--- a/docs/plugin_hooks.rst
+++ b/docs/plugin_hooks.rst
@@ -1629,6 +1629,39 @@ This example adds a new database action for creating a table, if the user has th
Example: `datasette-graphql `_, `datasette-edit-schema `_
+.. _plugin_hook_homepage_actions:
+
+homepage_actions(datasette, actor, request)
+-------------------------------------------
+
+``datasette`` - :ref:`internals_datasette`
+ You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.
+
+``actor`` - dictionary or None
+ The currently authenticated :ref:`actor `.
+
+``request`` - :ref:`internals_request`
+ The current HTTP request.
+
+This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the index page of the Datasette instance.
+
+This example adds a link an imagined tool for editing the homepage, only for signed in users:
+
+.. code-block:: python
+
+ from datasette import hookimpl
+
+
+ @hookimpl
+ def homepage_actions(datasette, actor):
+ if actor:
+ return [
+ {
+ "href": datasette.urls.path("/-/customize-homepage"),
+ "label": "Customize homepage",
+ }
+ ]
+
.. _plugin_hook_skip_csrf:
skip_csrf(datasette, scope)
diff --git a/tests/fixtures.py b/tests/fixtures.py
index c3c77fce..61d240da 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -42,6 +42,7 @@ EXPECTED_PLUGINS = [
"extra_js_urls",
"extra_template_vars",
"forbidden",
+ "homepage_actions",
"menu_links",
"permission_allowed",
"prepare_connection",
diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py
index 5f000537..94267f04 100644
--- a/tests/plugins/my_plugin.py
+++ b/tests/plugins/my_plugin.py
@@ -425,6 +425,18 @@ def database_actions(datasette, database, actor, request):
]
+@hookimpl
+def homepage_actions(datasette, actor, request):
+ if actor:
+ label = f"Custom homepage for: {actor['id']}"
+ return [
+ {
+ "href": datasette.urls.path("/-/custom-homepage"),
+ "label": label,
+ }
+ ]
+
+
@hookimpl
def skip_csrf(scope):
return scope["path"] == "/skip-csrf"
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 9f69e4fa..dcc5de20 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -995,6 +995,25 @@ async def test_hook_database_actions(ds_client):
]
+@pytest.mark.asyncio
+async def test_hook_homepage_actions(ds_client):
+ response = await ds_client.get("/")
+ # No button for anonymous users
+ assert "Homepage actions" not in response.text
+ # Signed in user gets an action
+ response2 = await ds_client.get(
+ "/", cookies={"ds_actor": ds_client.actor_cookie({"id": "troy"})}
+ )
+ assert "Homepage actions" in response2.text
+ assert get_actions_links(response2.text) == [
+ {
+ "label": "Custom homepage for: troy",
+ "href": "/-/custom-homepage",
+ "description": None,
+ },
+ ]
+
+
def test_hook_skip_csrf(app_client):
cookie = app_client.actor_cookie({"id": "test"})
csrf_response = app_client.post(