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:
parent
17159bfad9
commit
cb13f80241
4 changed files with 280 additions and 25 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
<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 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>⚡ 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>
|
||||
{% endif %}
|
||||
<a class="fastpath-cta" href="/marketplace/guide/flea">
|
||||
Submit a skill to Flea Market →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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' }} →
|
||||
</a>
|
||||
<a class="primary" href="/marketplace/guide/flea">Submit a skill to Flea Market →</a>
|
||||
<a href="/marketplace?tab=curated">← 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 →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
140
tests/test_web_marketplace_guide.py
Normal file
140
tests/test_web_marketplace_guide.py
Normal 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
|
||||
Loading…
Reference in a new issue