forked from retoor/devplacepy
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:
@@ -85,7 +85,7 @@ def test_devlog_empty_state_on_project_page(alice):
|
||||
|
||||
devlog_section = page.locator(".project-devlog")
|
||||
expect(devlog_section).to_be_visible()
|
||||
expect(devlog_section.locator("h3:has-text('Devlog')")).to_be_visible()
|
||||
expect(devlog_section.locator("h2:has-text('Devlog')")).to_be_visible()
|
||||
expect(page.locator(".empty-state:has-text('No devlog posts yet.')")).to_be_visible()
|
||||
|
||||
|
||||
@@ -187,3 +187,43 @@ def test_devlog_multiple_posts_order(alice):
|
||||
f"Expected newest post first: {markers[-1]}, got: {first_text}"
|
||||
)
|
||||
|
||||
|
||||
def test_project_stats_strip_visible(alice):
|
||||
"""The overview stats strip shows stars, updates, comments, files, forks."""
|
||||
page, _ = alice
|
||||
slug, project_uid, owner_uid = _seed_project()
|
||||
_seed_project_post(project_uid, owner_uid, 0)
|
||||
|
||||
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
||||
|
||||
stats = page.locator(".project-stats")
|
||||
expect(stats).to_be_visible()
|
||||
expect(stats.locator(".project-stat")).to_have_count(5)
|
||||
expect(stats.locator(".project-stat:has-text('update')")).to_contain_text("1")
|
||||
|
||||
|
||||
def test_owner_post_update_button_opens_preset_composer(alice):
|
||||
"""The owner's Post update button opens the composer preselected to this project."""
|
||||
page, _ = alice
|
||||
_create_project_ui(page, f"Composer Project {uuid4().hex[:6]}")
|
||||
|
||||
button = page.locator(".project-devlog-post-btn")
|
||||
expect(button).to_be_visible()
|
||||
button.click()
|
||||
|
||||
modal = page.locator("#create-post-modal")
|
||||
expect(modal).to_be_visible()
|
||||
expect(modal.locator("input[name='topic'][value='devlog']")).to_be_checked()
|
||||
selected = modal.locator("#project_uid").input_value()
|
||||
assert selected != "", "Expected the project preselected in the composer"
|
||||
|
||||
|
||||
def test_guest_sees_no_post_update_button(app_server):
|
||||
"""Guests and non-owners get no Post update control."""
|
||||
import requests
|
||||
|
||||
slug, _, _ = _seed_project()
|
||||
r = requests.get(f"{BASE_URL}/projects/{slug}")
|
||||
assert r.status_code == 200
|
||||
assert "project-devlog-post-btn" not in r.text
|
||||
|
||||
|
||||
Reference in New Issue
Block a user