Align the project hero with the showcase design: owner cover, logo, repo link

The hero now matches the showcase reference: the title block, tagline
(first description line), type/platform chips, author row and started
date render OVERLAID on the cover banner behind a bottom scrim, with
the optional project logo as a framed tile beside them and the Visit
Website CTA on the right. The section tab bar switches to the underline
style with Overview active.

Owners control the missing pieces from the create and edit modals,
which are now multipart: cover_image and logo_image file uploads
(stored as bare uploaded filenames via save_inline_image, the
posts.image pattern; a new upload replaces the previous one) plus a
repo_url sibling of website_url (same normalization/validation). The
cover feeds og:image and the schema image, the logo becomes schema
thumbnailUrl, and sameAs now carries website + repository. repo_url
rides ProjectOut, the Devii create/edit actions and the API docs;
tests cover repo normalization, the multipart upload round-trip and
the hero render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 22:12:25 +02:00
co-authored by Claude Fable 5
parent 3e9475fb61
commit 8a2e0e6e70
14 changed files with 311 additions and 77 deletions
+42
View File
@@ -355,6 +355,48 @@ def test_project_page_cover_and_screenshots_from_image_attachments(app_server):
assert '"screenshot"' in html, "Expected screenshot urls in the JSON-LD"
def test_owner_uploaded_cover_and_logo_render_in_hero(app_server):
"""Multipart cover_image/logo_image uploads land on the row and in the hero."""
import io
from PIL import Image
session, _ = _member()
def png(color):
buf = io.BytesIO()
Image.new("RGB", (12, 6), color).save(buf, "PNG")
return buf.getvalue()
r = session.post(
f"{BASE_URL}/projects/create",
headers=JSON,
data={
"title": _unique("dlhero"),
"description": "Hero art test project",
"project_type": "game",
"status": "In Development",
"platforms": "PC",
},
files={
"cover_image": ("cover.png", png((10, 20, 90)), "image/png"),
"logo_image": ("logo.png", png((90, 20, 10)), "image/png"),
},
)
assert r.status_code == 200, r.text[:300]
slug = r.json()["data"]["slug"]
refresh_snapshot()
row = get_table("projects").find_one(slug=slug)
assert row["cover_image"], "cover_image filename expected on the row"
assert row["logo_image"], "logo_image filename expected on the row"
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert f"/static/uploads/{row['cover_image']}" in html
assert f"/static/uploads/{row['logo_image']}" in html
assert 'class="project-logo"' in html
assert '"thumbnailUrl"' in html, "Expected the logo as schema thumbnailUrl"
def test_project_page_json_ld_rating_from_stars(app_server):
"""Stars surface as an aggregateRating; zero stars emit none."""
session, _ = _member()
+2
View File
@@ -115,12 +115,14 @@ def test_owner_can_set_and_clear_website_url(app_server):
"title": "Website Via Api",
"description": "has a website now",
"website_url": "myproject.dev/docs",
"repo_url": "github.com/me/website-via-api",
},
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"
assert row["repo_url"] == "https://github.com/me/website-via-api"
html = requests.get(f"{BASE_URL}/projects/{slug}").text
assert "Visit Website" in html