diff --git a/datasette/views/table.py b/datasette/views/table.py
index 0a908c69..8d35a4d4 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -721,6 +721,8 @@ async def display_columns_and_rows(
# Unless we are a view, the first column is a link - either to the rowid
# or to the simple or compound primary key
if link_column:
+ # NULL keys may be duplicated and encode like a real string key.
+ has_null_pk = bool(pks) and any(row[pk] is None for pk in pks)
is_special_link_column = len(pks) != 1
pk_path = path_from_row_pks(row, pks, not pks, False)
row_path = path_from_row_pks(row, pks, not pks)
@@ -729,9 +731,9 @@ async def display_columns_and_rows(
if row_label and row_label != pk_path:
row_action_label = f"{pk_path} {row_label}"
table_path = datasette.urls.table(database_name, table_name)
- row_link = (
- f'{markupsafe.escape(pk_path)!s}'
- )
+ row_link = markupsafe.escape(pk_path)
+ if not has_null_pk:
+ row_link = f'{markupsafe.escape(pk_path)!s}'
edit_icon = (
'