Skip to content

Commit ef093e0

Browse files
committed
feat: add robots-meta-content
1 parent 0ef331b commit ef093e0

4 files changed

Lines changed: 189 additions & 1 deletion

File tree

ckanext/switzerland/helpers.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,3 +714,93 @@ def uri_to_iri(uri):
714714
return iri
715715
except Exception as e:
716716
raise ValueError(f"Provided URI can't be converted to IRI: {e}")
717+
718+
719+
# Indexable curated list / detail pages (dataset, showcase, group).
720+
# Filtered URLs (any query string) and organizations / resources / harvest are noindex.
721+
_ROBOTS_INDEXABLE_LIST_ENDPOINTS = frozenset(
722+
{
723+
"dataset.search",
724+
"ogdch_home.search",
725+
"group.index",
726+
"showcase_blueprint.index",
727+
}
728+
)
729+
_ROBOTS_INDEXABLE_DETAIL_ENDPOINTS = frozenset(
730+
{
731+
"dataset.read",
732+
"group.read",
733+
"showcase_blueprint.read",
734+
}
735+
)
736+
_ROBOTS_RESOURCE_ENDPOINTS = frozenset(
737+
{
738+
"dataset_resource.read",
739+
"resource.read",
740+
}
741+
)
742+
_ROBOTS_NOINDEX_ENDPOINT_PREFIXES = ("organization.", "harvest.")
743+
_ROBOTS_NOINDEX = "noindex, follow"
744+
_ROBOTS_INDEX = "index, follow"
745+
746+
747+
def get_robots_meta_content():
748+
"""
749+
Return the robots meta content for the current request.
750+
751+
- Dataset / showcase / group list and detail (no query): index, follow
752+
- Same pages with any query parameters (filters, search, pagination): noindex, follow
753+
- Resource detail: noindex, follow
754+
- Organization pages: noindex, follow
755+
- Harvest pages: noindex, follow
756+
- Everything else: index, follow
757+
"""
758+
try:
759+
endpoint = tk.request.endpoint
760+
args = tk.request.args
761+
path = tk.request.path or ""
762+
except RuntimeError:
763+
# Outside a request context (e.g. some CLI / test helpers)
764+
return _ROBOTS_INDEX
765+
766+
if endpoint in _ROBOTS_RESOURCE_ENDPOINTS or _is_resource_path(path):
767+
return _ROBOTS_NOINDEX
768+
769+
if _is_noindex_admin_path(path) or (
770+
endpoint
771+
and endpoint.startswith(_ROBOTS_NOINDEX_ENDPOINT_PREFIXES)
772+
):
773+
return _ROBOTS_NOINDEX
774+
775+
has_query = bool(args)
776+
777+
if endpoint in _ROBOTS_INDEXABLE_LIST_ENDPOINTS:
778+
return _ROBOTS_NOINDEX if has_query else _ROBOTS_INDEX
779+
780+
if endpoint in _ROBOTS_INDEXABLE_DETAIL_ENDPOINTS:
781+
# Group detail can still have dataset filter query params
782+
return _ROBOTS_NOINDEX if has_query else _ROBOTS_INDEX
783+
784+
return _ROBOTS_INDEX
785+
786+
787+
def _is_noindex_admin_path(path):
788+
"""True for organization and harvest UI paths."""
789+
if not path:
790+
return False
791+
cleaned = path if path.startswith("/") else f"/{path}"
792+
return cleaned.startswith("/organization") or cleaned.startswith("/harvest")
793+
794+
795+
def _is_resource_path(path):
796+
"""True for /dataset/<id>/resource/<resource_id> HTML pages."""
797+
if not path:
798+
return False
799+
parts = [p for p in path.strip("/").split("/") if p]
800+
# dataset / <id> / resource / <resource_id> [/optional subpath]
801+
return (
802+
len(parts) >= 4
803+
and parts[0] == "dataset"
804+
and parts[2] == "resource"
805+
and parts[3] != "new"
806+
)

ckanext/switzerland/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def get_helpers(self):
9898
"ogdch_publisher_form_helper": sh.ogdch_publisher_form_helper,
9999
"ogdch_get_media_type_choices": sh.ogdch_get_media_type_choices,
100100
"ogdch_get_default_terms_of_use": sh.ogdch_get_default_terms_of_use,
101+
"get_robots_meta_content": sh.get_robots_meta_content,
101102
}
102103

103104
def i18n_directory(self):

ckanext/switzerland/templates/base.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{% ckan_extends %}
22

3+
{% block meta %}
4+
{{ super() }}
5+
<meta name="robots" content="{{ h.get_robots_meta_content() }}">
6+
{% endblock %}
7+
38
{% block links %}
49
<!-- Data layer initialization -->
510
<script>

ckanext/switzerland/tests/test_helpers.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from ckanext.switzerland.helpers import map_to_valid_format
3+
from ckanext.switzerland.helpers import get_robots_meta_content, map_to_valid_format
44

55
CSV_URI = "http://publications.europa.eu/resource/authority/file-type/CSV"
66
GEOJSON_URI = "http://publications.europa.eu/resource/authority/file-type/GEOJSON"
@@ -61,3 +61,95 @@
6161
)
6262
def test_map_to_valid_format_known_values(input_format, expected):
6363
assert map_to_valid_format(input_format) == expected
64+
65+
66+
INDEX = "index, follow"
67+
NOINDEX = "noindex, follow"
68+
69+
70+
class _FakeArgs(dict):
71+
"""Minimal stand-in for Flask request.args (truthy when non-empty)."""
72+
73+
def __bool__(self):
74+
return bool(dict(self))
75+
76+
77+
class _FakeRequest:
78+
def __init__(self, endpoint, path="/", args=None):
79+
self.endpoint = endpoint
80+
self.path = path
81+
self.args = _FakeArgs(args or {})
82+
83+
84+
@pytest.mark.parametrize(
85+
"endpoint,path,args,expected",
86+
[
87+
("dataset.search", "/dataset/", None, INDEX),
88+
("dataset.search", "/dataset/", {"q": "fahrplan"}, NOINDEX),
89+
("dataset.search", "/dataset/", {"groups": "timetables"}, NOINDEX),
90+
("dataset.search", "/dataset/", {"page": "2"}, NOINDEX),
91+
("ogdch_home.search", "/", None, INDEX),
92+
("ogdch_home.search", "/", {"organization": "oevch"}, NOINDEX),
93+
("dataset.read", "/dataset/bike-and-car-parking", None, INDEX),
94+
("showcase_blueprint.index", "/showcase/", None, INDEX),
95+
(
96+
"showcase_blueprint.index",
97+
"/showcase/",
98+
{"q": "viz"},
99+
NOINDEX,
100+
),
101+
(
102+
"showcase_blueprint.read",
103+
"/showcase/visualisierung-der-oev-tagesentwicklungen",
104+
None,
105+
INDEX,
106+
),
107+
("group.index", "/group/", None, INDEX),
108+
("group.read", "/group/accessibilitydata", None, INDEX),
109+
(
110+
"group.read",
111+
"/group/accessibilitydata",
112+
{"q": "test"},
113+
NOINDEX,
114+
),
115+
("organization.index", "/organization/", None, NOINDEX),
116+
("organization.read", "/organization/oevch", None, NOINDEX),
117+
(
118+
"organization.read",
119+
"/organization/oevch",
120+
{"q": "x"},
121+
NOINDEX,
122+
),
123+
("harvest.search", "/harvest/", None, NOINDEX),
124+
("harvest.read", "/harvest/some-source", None, NOINDEX),
125+
# Path fallback when endpoint is missing/unknown
126+
(None, "/harvest/", None, NOINDEX),
127+
(None, "/organization/oevch", None, NOINDEX),
128+
(
129+
"dataset_resource.read",
130+
"/dataset/bike-and-car-parking/resource/eb892409-d24f-4484-a19b-2d6e26108d9a",
131+
None,
132+
NOINDEX,
133+
),
134+
(
135+
"resource.read",
136+
"/dataset/bike-and-car-parking/resource/eb892409-d24f-4484-a19b-2d6e26108d9a",
137+
None,
138+
NOINDEX,
139+
),
140+
# Path fallback when endpoint is missing/unknown but URL is a resource
141+
(
142+
None,
143+
"/dataset/bike-and-car-parking/resource/eb892409-d24f-4484-a19b-2d6e26108d9a",
144+
None,
145+
NOINDEX,
146+
),
147+
("user.read", "/user/admin", None, INDEX),
148+
],
149+
)
150+
def test_get_robots_meta_content(monkeypatch, endpoint, path, args, expected):
151+
monkeypatch.setattr(
152+
"ckanext.switzerland.helpers.tk.request",
153+
_FakeRequest(endpoint, path, args),
154+
)
155+
assert get_robots_meta_content() == expected

0 commit comments

Comments
 (0)