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:
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user