Make the project page an SEO-optimized overview with a devlog environment

The project detail page becomes a professional project overview: hero
with status/type/dates/forked-from, a stats strip (stars, updates,
comments, files, forks with #devlog/#comments anchors), an About
section, platforms, and the Devlog timeline under a proper h2 - now
rendered with feed.css loaded so the post cards are actually styled.
The owner posts updates from the page itself: a Post update button
opens the shared create-post composer preset to the devlog topic and
this project. The composer form is extracted into
_post_composer_form.html and reused by feed.html - one form, two
surfaces.

SEO: software_application_schema is type-aware via project_schema_type
(game -> VideoGame with gamePlatform, website -> WebApplication,
software/mobile_app -> SoftwareApplication, game_asset -> CreativeWork)
and now carries keywords, image, an aggregateRating from stars, and a
comment InteractionCounter. project_devlog_schema emits a Blog node
with one BlogPosting per devlog entry. The detail route feeds both,
adds meta keywords, and exposes the devlog cursor as rel=next; the
sitemap's project lastmod follows the newest devlog post via one
grouped query. devlog_count/comment_count ride ProjectDetailOut, the
docs projects-detail endpoint documents the before cursor, and the
routers/projects CLAUDE.md, templates CLAUDE.md and README document the
new surface. Unit, api and e2e tests cover the schema mapping, the
JSON-LD in the rendered page, the stats strip, and the preset composer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 21:36:51 +02:00
co-authored by Claude Fable 5
parent 3fca4be72e
commit 2af5110399
16 changed files with 504 additions and 102 deletions
+67
View File
@@ -271,3 +271,70 @@ def test_devlog_works_for_guest_visitor(app_server):
assert "devlog_posts" in body
assert body["devlog_posts"] == []
def test_devlog_count_in_json(app_server):
"""devlog_count reflects every linked post, beyond the rendered page."""
session, name = _member()
project = _create_project(session)
slug = project["slug"] or project["uid"]
user = _db_user(name)
for i in range(3):
_create_post_direct(project["uid"], user["uid"], i)
r = session.get(f"{BASE_URL}/projects/{slug}", headers=JSON)
assert r.status_code == 200, r.text[:300]
body = r.json()
assert body["devlog_count"] == 3
assert body["comment_count"] == 0
def test_project_page_emits_typed_json_ld(app_server):
"""A game project renders VideoGame JSON-LD; the devlog renders a Blog graph."""
session, name = _member()
title = _unique("dlseo")
r = session.post(
f"{BASE_URL}/projects/create",
headers=JSON,
data={
"title": title,
"description": "SEO schema test project",
"project_type": "game",
"status": "In Development",
"platforms": "PC,Web",
},
)
assert r.status_code == 200, r.text[:300]
project = r.json()["data"]
slug = project["slug"] or project["uid"]
user = _db_user(name)
marker = _unique("dlseopost")
_create_post_direct(project["uid"], user["uid"], 0, marker=marker)
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert '"VideoGame"' in html, "Expected VideoGame JSON-LD for a game project"
assert '"Blog"' in html, "Expected a Blog node for the devlog"
assert '"BlogPosting"' in html, "Expected BlogPosting entries for devlog posts"
assert '"gamePlatform"' in html, "Expected gamePlatform from the platforms field"
def test_project_page_json_ld_rating_from_stars(app_server):
"""Stars surface as an aggregateRating; zero stars emit none."""
session, _ = _member()
project = _create_project(session)
slug = project["slug"] or project["uid"]
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert '"aggregateRating"' not in html, "No rating expected without stars"
r = session.post(
f"{BASE_URL}/votes/project/{project['uid']}",
data={"value": "1"},
headers={"X-Requested-With": "fetch"},
)
assert r.status_code == 200, r.text[:300]
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert '"aggregateRating"' in html, "Expected aggregateRating once starred"