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
+71
View File
@@ -49,6 +49,77 @@ def test_news_article_publisher_is_organization():
assert schema["publisher"]["@type"] == "Organization"
def test_project_schema_type_mapping():
assert seo.project_schema_type("game") == "VideoGame"
assert seo.project_schema_type("game_asset") == "CreativeWork"
assert seo.project_schema_type("website") == "WebApplication"
assert seo.project_schema_type("software") == "SoftwareApplication"
assert seo.project_schema_type("mobile_app") == "SoftwareApplication"
assert seo.project_schema_type(None) == "SoftwareApplication"
assert seo.project_schema_type("unknown") == "SoftwareApplication"
def test_software_application_schema_is_type_aware():
game = seo.software_application_schema(
{"uid": "p1", "slug": "p1-g", "title": "G", "project_type": "game", "platforms": "PC, Web"},
"https://x.test",
)
assert game["@type"] == "VideoGame"
assert game["gamePlatform"] == ["PC", "Web"]
assert "applicationCategory" not in game
web = seo.software_application_schema(
{"uid": "p2", "slug": "p2-w", "title": "W", "project_type": "website"},
"https://x.test",
)
assert web["@type"] == "WebApplication"
assert web["browserRequirements"] == "Requires JavaScript"
assert web["applicationCategory"] == "DeveloperApplication"
def test_software_application_schema_rating_and_image():
project = {"uid": "p1", "slug": "p1-s", "title": "S", "project_type": "software"}
plain = seo.software_application_schema(project, "https://x.test")
assert "aggregateRating" not in plain
assert "image" not in plain
rich = seo.software_application_schema(
project,
"https://x.test",
image_url="https://x.test/img.png",
star_count=7,
comment_count=3,
)
assert rich["aggregateRating"]["ratingCount"] == "7"
assert rich["image"] == "https://x.test/img.png"
assert rich["interactionStatistic"]["userInteractionCount"] == 3
def test_project_devlog_schema_builds_blog_graph():
project = {"uid": "p1", "slug": "p1-s", "title": "Nebula"}
assert seo.project_devlog_schema(project, [], "https://x.test") is None
posts = [
{
"post": {
"uid": "a1",
"slug": "a1-first",
"title": "First update",
"content": "Progress!",
"created_at": "2026-01-01T00:00:00+00:00",
},
"author": {"username": "dev"},
"comment_count": 2,
}
]
schema = seo.project_devlog_schema(project, posts, "https://x.test")
assert schema["@type"] == "Blog"
assert schema["url"] == "https://x.test/projects/p1-s#devlog"
entry = schema["blogPost"][0]
assert entry["@type"] == "BlogPosting"
assert entry["headline"] == "First update"
assert entry["url"] == "https://x.test/posts/a1-first"
assert entry["author"]["name"] == "dev"
assert entry["commentCount"] == 2
def test_site_url_precedence(monkeypatch):
import devplacepy.database as database