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
@@ -0,0 +1,49 @@
{# Shared create-post form. Locals: _composer_topic (preselected topic), _composer_project (preselected project uid or ""). #}
<form id="create-post-form" method="POST" action="/posts/create" enctype="multipart/form-data">
{% set _topics = TOPICS %}{% set _selected = _composer_topic or 'random' %}{% include "_topic_selector.html" %}
<div class="auth-field auth-field-gap">
<label for="post-content">What are you sharing?</label>
<textarea id="post-content" name="content" required maxlength="125000" placeholder="What's on your mind?" class="min-h-120" data-mention></textarea>
<small class="hint-text"><span id="post-content-count">0/125000</span></small>
</div>
<div class="auth-field auth-field-gap">
<label for="post-title">Title (optional)</label>
<input type="text" id="post-title" name="title" maxlength="500" placeholder="Post title">
<small class="hint-text"><span id="post-title-count">0/500</span></small>
</div>
<div class="auth-field auth-field-gap">
<label for="project_uid">Link to Project (Optional)</label>
<select id="project_uid" name="project_uid">
<option value="">No project</option>
{% for p in get_user_projects(user['uid']) %}
<option value="{{ p['uid'] }}" {% if _composer_project and p['uid'] == _composer_project %}selected{% endif %}>{{ p['title'] }}</option>
{% endfor %}
</select>
</div>
<div class="auth-field auth-field-gap">
<label>Attach files (images, video, audio, documents)</label>
{% include "_attachment_form.html" %}
</div>
<div class="auth-field auth-field-gap">
<button type="button" class="btn btn-secondary btn-sm" data-poll-toggle><span class="icon">&#x1F4CA;</span> <span data-poll-toggle-label>Add poll</span></button>
</div>
<div class="poll-builder" data-poll-builder hidden>
<input type="text" name="poll_question" maxlength="200" placeholder="Poll question" aria-label="Poll question" class="poll-builder-question" disabled>
<div class="poll-builder-options" data-poll-options>
<div class="poll-builder-row"><input type="text" name="poll_options" maxlength="100" placeholder="Option 1" aria-label="Poll option 1" disabled></div>
<div class="poll-builder-row"><input type="text" name="poll_options" maxlength="100" placeholder="Option 2" aria-label="Poll option 2" disabled></div>
</div>
<button type="button" class="btn-ghost btn-sm" data-poll-add-option>+ Add option</button>
<p class="poll-builder-error" data-poll-error hidden></p>
</div>
<div class="modal-footer">
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">Post</button>
</div>
</form>