feat(marketplace): rename CTA + expand submit-flow guides (#308)

Three coordinated tweaks to the publication discovery surface:

1. Action-row CTA on /marketplace?tab=curated reads 'Submit a skill
   or plugin' instead of 'Submit a plugin'. Skills are first-class
   citizens of the curated shelf; the old wording made them feel
   like an afterthought. Same rename in the empty-state JS innerHTML
   so the two paths can't drift.

2. Curated guide page (/marketplace/guide/curated) expanded from a
   4-line stub into a 3-step ordered list documenting the Named
   Curator handoff (find curator → handoff → publish + lifecycle).
   New '.guide-fastpath' callout block points users at the Flea
   Market when they want lighter review-bar / faster path. Primary
   CTA at the bottom of the curated guide now links to the flea
   guide too, so users who skim past the fast-path callout still
   see the escape hatch.

3. Flea guide page (/marketplace/guide/flea) expanded from a
   3-line stub into a 4-step ordered list (package → upload via
   form → automated review → published). Documents the actual
   /store/new flow + the automated guardrails (manifest, content
   quality, prompt-injection scan) so users know what 'self-service'
   actually means before they upload.

Route titles updated to match: 'Submit a skill or plugin to
Curated Marketplace'.

New file: tests/test_web_marketplace_guide.py — three tests covering
the CTA rename, the curated guide's structural elements (Named
Curators lede, 3 steps, fastpath callout, primary-CTA href), and
the flea guide's structural elements (4 steps, no fastpath
asymmetry, /store/new primary CTA).
This commit is contained in:
Vojtech 2026-05-14 21:44:33 +04:00 committed by GitHub
parent 17159bfad9
commit cb13f80241
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 280 additions and 25 deletions

View file

@ -1525,7 +1525,7 @@ async def marketplace_guide_curated(
):
ctx = _build_context(
request, user=user,
guide_title="Submit a plugin to Curated Marketplace",
guide_title="Submit a skill or plugin to Curated Marketplace",
guide_kind="curated",
)
return templates.TemplateResponse(request, "marketplace_guide.html", ctx)

View file

@ -440,7 +440,7 @@
</button>
</div>
<div class="mp-actions">
<a class="btn btn-secondary" data-actions-for="curated" href="/marketplace/guide/curated">Submit a plugin</a>
<a class="btn btn-secondary" data-actions-for="curated" href="/marketplace/guide/curated">Submit a skill or plugin</a>
<!-- Flea has a self-service +Upload button below — no second
"how to" CTA needed. Anyone uploads via the form directly. -->
<a class="btn btn-primary" data-actions-for="flea" href="/store/new" hidden>+ Upload</a>
@ -779,7 +779,7 @@ function fillEmptyState() {
if (state.tab === 'curated') {
t.textContent = 'No curated plugins available.';
b.textContent = 'Ask your admin to grant your group access.';
c.innerHTML = `<a href="/marketplace/guide/curated">Submit a plugin →</a>`;
c.innerHTML = `<a href="/marketplace/guide/curated">Submit a skill or plugin →</a>`;
} else if (state.tab === 'flea') {
t.textContent = 'No community entities yet.';
b.textContent = 'Be the first to share something.';

View file

@ -71,6 +71,39 @@
/* Flea variant uses purple step bubbles (matches flea badge accent). */
.guide-page[data-kind="flea"] .guide-steps li::before { background: #6D28D9; }
/* Fast-path callout — visually distinct from the numbered steps so
readers spot "wait, there's a quicker option" without re-reading
the whole page. Lives only on the curated guide; flea has no
equivalent escape hatch (it's already the fast path). */
.guide-fastpath {
margin: 16px 0 28px;
padding: 18px 22px;
background: linear-gradient(135deg, rgba(109, 40, 217, 0.06), rgba(109, 40, 217, 0.02));
border: 1px solid rgba(109, 40, 217, 0.25);
border-left: 4px solid #6D28D9;
border-radius: 12px;
}
.guide-fastpath h3 {
margin: 0 0 8px;
font-size: 15px; font-weight: 700;
color: var(--text-primary);
}
.guide-fastpath p {
margin: 0 0 12px;
font-size: 14px; line-height: 1.6;
color: var(--text-secondary);
}
.guide-fastpath p strong { color: var(--text-primary); }
.guide-fastpath a.fastpath-cta {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 14px;
background: #6D28D9; color: #fff;
border-radius: 8px;
font-size: 13px; font-weight: 500;
text-decoration: none;
}
.guide-fastpath a.fastpath-cta:hover { background: #5B21B6; }
/* ── Section block ──────────────────────────────────────────────── */
.guide-section {
background: var(--card-bg);
@ -130,32 +163,114 @@
<h1>{{ guide_title }}</h1>
{% if guide_kind == 'curated' %}
{# Curated submission guide — body intentionally left minimal; full
content to be authored separately. The CTA buttons at the bottom of
the page still let the user jump back to the Curated tab or browse
the Flea Market as a self-service alternative. #}
{# Curated submission guide — publish-by-curator flow. Named Curators
are the gatekeepers (security / telemetry hygiene / docs bar). The
fast-path callout points users at the Flea Market when they want
reach without the review wait. #}
<p class="lede">
The Curated Marketplace is publish-by-curator only. To get your skill,
agent, or plugin listed, hand it off to a curator who reviews and
publishes it on your behalf.
Curated Marketplace submissions go through <strong>Named Curators</strong>
— folks accountable for keeping the bar (security, telemetry hygiene,
documentation) consistent across what ships on this shelf.
</p>
{% else %}
{# Flea publishing guide — body intentionally left minimal; full content
to be authored separately. The CTA buttons at the bottom of the page
still let the user jump to /store/new or browse the Flea Market. #}
<p class="lede">
The Flea Market is self-service. Anyone in the company can upload a
skill, agent, or plugin.
</p>
{% endif %}
<div class="guide-cta">
<a class="primary" href="{{ '/store/new' if guide_kind == 'flea' else '/marketplace?tab=curated' }}">
{{ '+ Upload to Flea Market' if guide_kind == 'flea' else '← Back to Curated' }}
</a>
<a href="/marketplace?tab={{ 'flea' if guide_kind == 'curated' else 'curated' }}">
{{ 'Browse Flea Market' if guide_kind == 'curated' else 'Browse Curated' }} →
<ol class="guide-steps">
<li>
<strong>Find a Curator.</strong>
Each plugin in the Curated Marketplace lists its curator on its detail
page. Reach out to any curator whose published work overlaps with what
you're shipping — they're the most likely owner. The
<em>See all curators →</em> link on the
<a href="/marketplace?tab=curated">Curated tab</a> is the directory entry.
<span class="note">If no curator's domain matches, ask the
{{ instance_brand }} platform team — they'll either adopt the
submission or steer it to the right person.</span>
</li>
<li>
<strong>Hand off your skill or plugin.</strong>
Share the source repo (or a zipped bundle) plus a one-paragraph intro
covering <em>what it does</em>, <em>who it's for</em>, and
<em>what data it touches</em>. The curator runs it through the review
bar (manifest sanity, prompt-injection surface, doc completeness,
telemetry posture).
</li>
<li>
<strong>Curator publishes.</strong>
Once they accept, the curator commits the manifest entry to the
curated repo. Your plugin appears on the Curated tab on the next
nightly catalog sync (within ~24&nbsp;h of merge).
<span class="note">Iteration after publish goes through the same
curator — they own the entry's lifecycle (version bumps,
deprecation, takedown).</span>
</li>
</ol>
<div class="guide-fastpath">
<h3>&#x26A1; Want it live in minutes instead of days?</h3>
<p>
The <strong>Flea Market</strong> is the self-service shelf — anyone in
the company can upload a skill, agent, or plugin without going
through a curator. Same Claude Code reach, faster path, lighter
review bar (automated guardrails only; no human gatekeeper).
</p>
<a class="fastpath-cta" href="/marketplace/guide/flea">
Submit a skill to Flea Market &rarr;
</a>
</div>
<div class="guide-cta">
<a class="primary" href="/marketplace/guide/flea">Submit a skill to Flea Market &rarr;</a>
<a href="/marketplace?tab=curated">&larr; Back to Curated</a>
</div>
{% else %}
{# Flea publishing guide — self-service flow. No human gatekeeper,
automated guardrails only (content quality + security review run
server-side at submission time). Lives at /store/new. #}
<p class="lede">
The Flea Market is self-service. Anyone in the company can upload a
skill, agent, or plugin — no curator handoff, no review queue.
Automated guardrails check the manifest, content quality, and
prompt-injection surface at submission time.
</p>
<ol class="guide-steps">
<li>
<strong>Package what you're shipping.</strong>
A skill is a single <code>SKILL.md</code> (front-matter + body); an
agent is an <code>AGENT.md</code>; a plugin is a folder with a
<code>plugin.json</code> manifest and any commands / agents / skills
it bundles. Zip the folder (or point at a git URL containing it).
<span class="note">If you're starting fresh, the
<a href="/marketplace/format-guide">format guide</a> documents each
shape with copy-pasteable examples.</span>
</li>
<li>
<strong>Upload via the form.</strong>
Head to <a href="/store/new"><code>/store/new</code></a>. Drop the
zip (or paste the git URL), fill the metadata form (name, short
description, what data it touches, who it's for), submit.
<span class="note">The <em>+ Upload</em> button on the Flea Market
tab is the same form.</span>
</li>
<li>
<strong>Automated review.</strong>
Server runs content-quality checks (manifest completeness, prose
length, distinct-word floor) and a prompt-injection scan. Most
submissions pass within seconds. Failures surface inline with the
exact field to fix.
</li>
<li>
<strong>Published.</strong>
Once green, your submission goes live on the Flea Market tab
immediately — no nightly-sync delay. Iteration goes through the
same form: upload a new version, the form recognises the slug
and bumps the version pointer.
</li>
</ol>
<div class="guide-cta">
<a class="primary" href="/store/new">+ Upload to Flea Market</a>
<a href="/marketplace?tab=curated">Browse Curated &rarr;</a>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,140 @@
"""GET /marketplace/guide/{curated,flea} — submission flow guides.
Both routes are authed (`get_current_user` dependency). The curated guide
documents the Named Curator handoff and has a fast-path callout pointing
at the flea self-service guide; the flea guide documents the /store/new
upload flow. Together with the action-row CTA on /marketplace?tab=curated,
this trio is the discovery surface for "how do I get my plugin published".
"""
from __future__ import annotations
import tempfile
import uuid
import pytest
@pytest.fixture
def fresh_db(monkeypatch):
with tempfile.TemporaryDirectory() as tmp:
monkeypatch.setenv("DATA_DIR", tmp)
monkeypatch.setenv("TESTING", "1")
monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!")
yield tmp
def _make_user_and_session(conn, email="u@example.com"):
from src.repositories.users import UserRepository
from app.auth.jwt import create_access_token
uid = str(uuid.uuid4())
UserRepository(conn).create(id=uid, email=email, name=email.split("@")[0])
return uid, create_access_token(user_id=uid, email=email)
def _client():
from fastapi.testclient import TestClient
from app.main import app
return TestClient(app)
def test_marketplace_curated_tab_cta_text(fresh_db):
"""The action-row CTA on /marketplace?tab=curated reads
'Submit a skill or plugin' (renamed from 'Submit a plugin' so skills
aren't an afterthought) and links to the curated guide. Empty-state
fallback in JS uses the same string so both surfaces stay in sync."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get(
"/marketplace?tab=curated", cookies={"access_token": sess}
).text
# Action-row anchor — primary discovery path.
assert (
'<a class="btn btn-secondary" data-actions-for="curated" '
'href="/marketplace/guide/curated">Submit a skill or plugin</a>'
) in body
# Empty-state JS innerHTML — same string, no drift.
assert "Submit a skill or plugin →" in body
# Old wording must be gone — guards against partial rename.
assert ">Submit a plugin<" not in body
def test_marketplace_guide_curated_page(fresh_db):
"""Curated guide page documents the Named Curator handoff. Three-step
flow (find handoff publish) lives inside `.guide-steps`. The
fast-path callout points users at the flea guide as the lighter
review-bar alternative; the primary CTA at the bottom does the same
so users who skim past the callout still see the escape hatch."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
resp = _client().get(
"/marketplace/guide/curated", cookies={"access_token": sess}
)
assert resp.status_code == 200
body = resp.text
# Title carries the new 'skill or plugin' wording.
assert "Submit a skill or plugin to Curated Marketplace" in body
# Lede surfaces the gatekeeping concept.
assert "Named Curators" in body
# Three-step ordered list under `.guide-steps`.
assert '<ol class="guide-steps">' in body
assert "Find a Curator" in body
assert "Hand off your skill or plugin" in body
assert "Curator publishes" in body
# Fast-path callout exists and the CTA inside it points at the flea
# guide (NOT /store/new directly — we want users to read the flea
# context before they upload).
assert '<div class="guide-fastpath">' in body
assert 'href="/marketplace/guide/flea"' in body
# Primary CTA at the bottom also surfaces the flea path.
assert 'class="primary" href="/marketplace/guide/flea"' in body
def test_marketplace_guide_flea_page(fresh_db):
"""Flea guide documents the /store/new self-service flow. Four-step
body (package upload automated review published) replaces the
earlier stub. Primary CTA goes directly to /store/new since users
landing on the flea guide have already chosen the self-service path."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
resp = _client().get(
"/marketplace/guide/flea", cookies={"access_token": sess}
)
assert resp.status_code == 200
body = resp.text
assert "Upload to Flea Market" in body
# Four-step ordered list (no fast-path callout on flea — it IS the
# fast path, the curated guide is what links here).
assert '<ol class="guide-steps">' in body
assert "Package what you" in body
assert "Upload via the form" in body
assert "Automated review" in body
assert "Published" in body
# Primary CTA goes straight to /store/new (flea is one click away
# from being live, no intermediate handoff).
assert 'class="primary" href="/store/new"' in body
# No fast-path callout here — sanity check the asymmetry sticks.
assert '<div class="guide-fastpath">' not in body