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