Dedicate the project page to the project: hero, tabs, screenshots, sidebar

The project detail page becomes a full project showcase on the site's
content measure. The hero card opens with a cover banner from the
project's first image attachment (brand-gradient band as fallback),
then title + status chip, type/platform chips, dates and forked-from
meta, the author row with an owner-set Visit Website CTA, and the
unchanged action row. A sticky anchor tab bar (Overview, Devlog,
Screenshots when images exist, Comments, Files) navigates the page
with plain server-rendered anchors so crawlers index one complete
document. The two-column body keeps About (description + non-image
attachments), the Devlog timeline and the comment thread in the main
column, adds a Screenshots gallery built from image attachments
(lightbox-wired thumbnails), and a sidebar with Links (website, files,
fork source), the Stats card with a last-update line, and the Author
card.

New optional projects.website_url rides the whole stack: normalized
and validated in models (scheme-less input gets https://, non-http(s)
rejected), settable in the create and edit modals, on ProjectOut, in
the Devii create/edit actions and the API docs, rendered as the hero
CTA and Links entry with rel noopener nofollow, and emitted as
schema.org sameAs. The app schema also gains screenshot urls from the
image attachments. The e2e project comment/files tests scope their
locators (.comment-form textarea, .project-detail-actions a) per the
documented dual-control idiom - the composer modal made the bare
selectors ambiguous - and new unit/api tests cover URL normalization,
sameAs/screenshot schema output, the cover, and the gallery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 21:56:50 +02:00
co-authored by Claude Fable 5
parent 2af5110399
commit 3e9475fb61
17 changed files with 548 additions and 152 deletions
+36
View File
@@ -319,6 +319,42 @@ def test_project_page_emits_typed_json_ld(app_server):
assert '"gamePlatform"' in html, "Expected gamePlatform from the platforms field"
def test_project_page_cover_and_screenshots_from_image_attachments(app_server):
"""An image attachment becomes the hero cover, the Screenshots section, and schema screenshots."""
import io
from PIL import Image
session, _ = _member()
buf = io.BytesIO()
Image.new("RGB", (8, 8), (30, 60, 120)).save(buf, "PNG")
r = session.post(
f"{BASE_URL}/uploads/upload",
files={"file": ("shot.png", buf.getvalue(), "image/png")},
)
assert r.status_code == 201, r.text[:300]
attachment_uid = r.json()["uid"]
r = session.post(
f"{BASE_URL}/projects/create",
headers=JSON,
data={
"title": _unique("dlshot"),
"description": "Cover test project",
"project_type": "game",
"status": "In Development",
"platforms": "PC",
"attachment_uids": attachment_uid,
},
)
assert r.status_code == 200, r.text[:300]
slug = r.json()["data"]["slug"]
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert 'class="project-cover"' in html, "Expected the image attachment as hero cover"
assert "project-screenshot-grid" in html, "Expected the Screenshots section"
assert '"screenshot"' in html, "Expected screenshot urls in the JSON-LD"
def test_project_page_json_ld_rating_from_stars(app_server):
"""Stars surface as an aggregateRating; zero stars emit none."""
session, _ = _member()
+36
View File
@@ -105,6 +105,42 @@ def test_owner_can_edit_project(app_server):
assert row["platforms"] == "Linux,Web"
def test_owner_can_set_and_clear_website_url(app_server):
_, _, key = _signup_project_visibility()
slug = _create_project_project_visibility(key, "Website Via Api")["slug"]
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(key),
data={
"title": "Website Via Api",
"description": "has a website now",
"website_url": "myproject.dev/docs",
},
allow_redirects=False,
)
assert r.status_code == 200 and r.json()["ok"] is True
row = get_table("projects").find_one(slug=slug)
assert row["website_url"] == "https://myproject.dev/docs"
html = requests.get(f"{BASE_URL}/projects/{slug}").text
assert "Visit Website" in html
assert '"sameAs"' in html
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(key),
data={
"title": "Website Via Api",
"description": "website removed",
"website_url": "",
},
allow_redirects=False,
)
assert r.status_code == 200
assert get_table("projects").find_one(slug=slug)["website_url"] is None
assert "Visit Website" not in requests.get(f"{BASE_URL}/projects/{slug}").text
def test_non_owner_cannot_edit_project(app_server):
_, _, owner_key = _signup_project_visibility()
slug = _create_project_project_visibility(owner_key, "Owner Edit Guard")["slug"]