Showcase
Your portfolio at the top of taste.dev/@you: kit blocks, one motion each, filled from your products. The block catalogue, the JSON Schema, the plan limit and the API.
A showcase is your portfolio page. It sits at the top of taste.dev/@{username}, above your profile, and it's built from the Showcase kit: a dozen blocks (a hero tile, a phone with your screens, a ship log of your updates, an app grid, store badges…), each with one motion from a short list, and one page-wide intensity. You never write layout or keyframes, so every page comes out looking finished. See every block and motion live at taste.dev/kit.
You (or your AI, over MCP) write a showcase as JSON. Taste validates it against a published JSON Schema, saves it, and renders it with your theme.
{
"schema": 1,
"intensity": "subtle",
"blocks": [
{ "type": "hero", "motion": "drift", "content": { "product": "counter" } },
{ "type": "about", "content": { "name": "Mika", "bio": "I build tools for small shops." } }
]
}schemais always1.intensityis how much everything moves:still,subtle(the default) orexpressive.blocksis the page, top to bottom: at most 24. Each block is atype, amotion(optional: each type has a default) and itscontent.
Built from your products
Most blocks can name one of your products with content.product: its id, its slug or its name. Taste fills in what the product already knows (its name, tagline, icon, screenshots, homepage, follower count and your product updates) every time the page is shown, so the page stays fresh: edit the product, ship an update, and the showcase follows. Anything you set in content wins over the product.
- Only your own, live products. A showcase that names someone else's product, or an archived one, doesn't validate; if you archive a product later, the blocks naming it leave the page and the rest stay.
- References are saved as product ids, so renaming a product (or its slug) doesn't break the page.
- A product has to have what its block needs: a hero needs a tagline and an icon, a device frame a screenshot, a screenshot strip two. Validation says what's missing and how to add it. A ship log can wait: it appears with the product's first update.
Quotes, the film, your intro and the journey are your own words: they never name a product.
Plans
| Plan | Projects in your showcase |
|---|---|
| Free | 1 |
| Taste Pro ($5/month, coming soon) | as many as you have products (50) |
A project is a product the showcase names, or an app it shows by name without a product (a hero's, a ship log's or an app grid's name). An app named like one of your products is that product. Blocks that name no project (about, quotes, features, journey, film) are free to use. A free showcase can tell the whole story of one app; showing several (an app grid, a second hero) needs Taste Pro. Billing isn't open yet: Taste can turn Pro on for an account.
If a Pro account goes back to free, nothing is deleted: the page shows the blocks of its first project (and the project-free blocks) until Pro is back.
Blocks
| Block | type | What it is | Motions |
|---|---|---|---|
| Hero tile | hero | One app, big: who it is for, its name and line, two actions, a picture cropped by the edge. | rise, drift, pin-and-play, still |
| Device frame | device | Your screens in a phone or a browser window, changing from one to the next. | cycle, float, pin-and-play, still |
| Feature trio | features | Three things it does, said plainly, side by side. | stagger, rise, still |
| Screenshot strip | strip | A row of screenshots that scrolls sideways on its own. | stagger, drift, still |
| App grid | apps | The rest of what you have made, as icons with a line each. | stagger, float, still |
| Stats row | stats | Two to four numbers worth knowing: rating, people, updates shipped. | count-up, stagger, still |
| Ship log | shiplog | Your product updates on Taste, as a timeline: proof you keep shipping. | stagger, rise, still |
| Review quote | quote | What a real user said, in their words, with the line that matters set apart. | rise, cycle, still |
| Film tile | film | A poster with a play button; the film loads only when someone taps it. | rise, drift, still |
| Builder intro | about | Who is behind it: a photo, a few honest lines, where to find you. | rise, stagger, still |
| Store badges | badges | Where to get it: App Store, Google Play, the web. | rise, stagger, still |
| Journey | journey | How it came to be, a few milestones in order. | stagger, rise, still |
Every block also takes still. Text limits count characters as people do (an emoji is one); blank text counts as absent, and null means absent too.
Hero tile
"type": "hero". One app, big: who it is for, its name and line, two actions, a picture cropped by the edge. Motions: rise, drift, pin-and-play, still (default rise).
With product: name, tagline and icon from the product; its first screenshot as the art and a Visit button to its homepage, unless you set them.
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
persona | text, ≤ 40 | Who it is for, above the name ("For close friends"). | |
name | text, ≤ 40 | yes, unless product | |
tagline | text, ≤ 90 | yes, unless product | One line: what it does. |
body | text, ≤ 240 | ||
icon | https URL | yes, unless product | A square app icon. |
art | list of 1–7 | One picture under the words, or a few standing on the tile's floor, each at its own depth. | |
art[].src | https URL | yes | |
art[].alt | text, ≤ 140 | ||
actions | list of 1–2 | Buttons: the first filled, the second outlined. | |
actions[].label | text, ≤ 24 | yes | |
actions[].url | https URL | yes | |
note | text, ≤ 120 | Small print under the actions ("Rated 4.7 on the App Store."). |
Device frame
"type": "device". Your screens in a phone or a browser window, changing from one to the next. Motions: cycle, float, pin-and-play, still (default cycle).
With product: screens from the product's screenshots (up to 6); a browser's address bar reads its homepage.
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
frame | phone | browser | yes | |
title | text, ≤ 80 | ||
body | text, ≤ 240 | ||
address | text, ≤ 80 | What the browser's address bar reads (a label, not a link). | |
screens | list of 1–6 | yes, unless product | |
screens[].src | https URL | yes | |
screens[].alt | text, ≤ 140 | yes | |
screens[].caption | text, ≤ 60 |
Feature trio
"type": "features". Three things it does, said plainly, side by side. Motions: stagger, rise, still (default stagger).
| Field | Kind | Required | Notes |
|---|---|---|---|
title | text, ≤ 80 | ||
items | list of 2–3 | yes | |
items[].title | text, ≤ 48 | yes | |
items[].body | text, ≤ 180 | yes | |
items[].icon | spark | target | check | clock | heart | shield | bolt | leaf | moon | sun | chat | chart |
Screenshot strip
"type": "strip". A row of screenshots that scrolls sideways on its own. Motions: stagger, drift, still (default stagger).
With product: shots from the product's screenshots (it needs at least two).
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
title | text, ≤ 80 | ||
body | text, ≤ 200 | ||
frame | phone | plain | ||
shots | list of 2–10 | yes, unless product | |
shots[].src | https URL | yes | |
shots[].alt | text, ≤ 140 | yes |
App grid
"type": "apps". The rest of what you have made, as icons with a line each. Motions: stagger, float, still (default stagger).
Each app can name a product: name, tagline and icon from the product, linking to its page on Taste.
| Field | Kind | Required | Notes |
|---|---|---|---|
title | text, ≤ 80 | ||
apps | list of 2–12 | yes | |
apps[].product | product: id, slug or name | Fills what it can from one of your products. | |
apps[].name | text, ≤ 40 | yes, unless product | |
apps[].tagline | text, ≤ 80 | ||
apps[].icon | https URL | yes, unless product | |
apps[].url | https URL |
Stats row
"type": "stats". Two to four numbers worth knowing: rating, people, updates shipped. Motions: count-up, stagger, still (default count-up).
With product: items from the product: updates shipped, followers, and GitHub stars when it has some.
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
title | text, ≤ 80 | ||
items | list of 2–4 | yes, unless product | |
items[].value | number, 0–1,000,000,000 | yes | |
items[].decimals | whole number, 0–2 | ||
items[].prefix | text, ≤ 3 | ||
items[].suffix | text, ≤ 8 | ||
items[].label | text, ≤ 40 | yes |
Ship log
"type": "shiplog". Your product updates on Taste, as a timeline: proof you keep shipping. Motions: stagger, rise, still (default stagger).
With product: the product's name and page, and its latest updates on Taste (up to 12), always fresh.
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
name | text, ≤ 60 | yes, unless product | The product's name, as the log's heading. |
productUrl | https URL | ||
entries | list of 1–12 | yes, unless product | Newest first. |
entries[].date | date, YYYY-MM-DD | yes | |
entries[].title | text, ≤ 100 | yes | |
entries[].body | text, ≤ 280 | ||
entries[].url | https URL |
Review quote
"type": "quote". What a real user said, in their words, with the line that matters set apart. Motions: rise, cycle, still (default rise).
| Field | Kind | Required | Notes |
|---|---|---|---|
quotes | list of 1–6 | yes | |
quotes[].quote | text, ≤ 320 | yes | |
quotes[].highlight | text, ≤ 120 | Words from the quote to set apart. Must be words from quote. | |
quotes[].name | text, ≤ 40 | yes | |
quotes[].source | text, ≤ 40 | Where they said it ("App Store"). | |
quotes[].rating | whole number, 1–5 |
Film tile
"type": "film". A poster with a play button; the film loads only when someone taps it. Motions: rise, drift, still (default rise).
| Field | Kind | Required | Notes |
|---|---|---|---|
title | text, ≤ 80 | yes | |
body | text, ≤ 200 | ||
poster | https URL | yes | |
video | https URL | yes | An https video file (mp4), loaded only when the visitor taps play. |
length | text, ≤ 10 | "0:30" |
Builder intro
"type": "about". Who is behind it: a photo, a few honest lines, where to find you. Motions: rise, stagger, still (default rise).
| Field | Kind | Required | Notes |
|---|---|---|---|
name | text, ≤ 60 | yes | |
role | text, ≤ 80 | ||
photo | https URL | ||
bio | text, ≤ 480 | yes | |
location | text, ≤ 60 | ||
links | list of 1–4 | ||
links[].label | text, ≤ 24 | yes | |
links[].url | https URL | yes |
Store badges
"type": "badges". Where to get it: App Store, Google Play, the web. Motions: rise, stagger, still (default rise).
With product: the web badge from the product's homepage.
Set at least one of appStore, googlePlay, web (or a product with a homepage).
| Field | Kind | Required | Notes |
|---|---|---|---|
product | product: id, slug or name | Fills what it can from one of your products. | |
title | text, ≤ 80 | ||
appStore | https URL | ||
googlePlay | https URL | ||
web | https URL | ||
webLabel | text, ≤ 30 | ||
note | text, ≤ 120 |
Journey
"type": "journey". How it came to be, a few milestones in order. Motions: stagger, rise, still (default stagger).
| Field | Kind | Required | Notes |
|---|---|---|---|
title | text, ≤ 80 | ||
steps | list of 2–8 | yes | |
steps[].when | text, ≤ 20 | yes | |
steps[].title | text, ≤ 80 | yes | |
steps[].body | text, ≤ 200 |
Motions
| Motion | What it does |
|---|---|
rise | Rises a little and fades in, once, as it scrolls into view. |
stagger | Its items arrive one after another, a beat apart. |
drift | Layers move at different depths as you scroll: quiet parallax. |
float | Pictures bob gently in place, out of step, while on screen. |
cycle | Pictures crossfade from one to the next on their own; dots to choose. |
count-up | Numbers count up to their value as they come into view. |
pin-and-play | Holds the block on screen; its opening plays by itself, then the scroll carries it on. |
still | No motion: the block is simply there. |
Motion is transform and opacity only, never layout. Everything holds still for visitors who ask their device for reduced motion, and under "intensity": "still".
Intensity
| Intensity | What it does |
|---|---|
still | Nothing moves: every block is simply there. |
subtle | Gentle entrances and a little depth. The default. |
expressive | Longer travel, deeper parallax, bigger floats. |
Limits
- At most 24 blocks and 64 KB of JSON.
- Text fields have their own limits (above), never more than 600 characters.
- Links and pictures are https URLs, up to 2048 characters, with no spaces or passwords in them. Upload pictures the way the app does (
POST /uploads/presign), or link any https image. - Text passes the same Community Guidelines filter as posts.
The JSON Schema
The schema is JSON Schema 2020-12, published with the others (see Schemas):
GET https://api.taste.dev/schemas/showcase-1.json: the schema,$idhttps://taste.dev/schemas/showcase-1.json.GET https://api.taste.dev/showcase/schema: the schema plus the catalogue (every block with its fields, limits, motions and what a product fills), the motions, the intensities and the plan limits. No auth; cached for a day.- In the Taste repository it is
shared/showcase/showcase.schema.json, generated fromshared/showcase/registry.json, the one registry the API and the site both read.
The schema is the contract for writing a showcase. Validation adds a few checks a schema can't say: a quote's highlight must be words from the quote, dates must be real days, the products must be yours and have what their blocks need, and the plan's project limit. Fields marked "unless product" are required only when the block names no product.
API
All of these use the app's sign-in (Authorization: Bearer <access token>), except the schema.
| Endpoint | What it does |
|---|---|
GET /showcase/schema | The schema and catalogue (public) |
POST /showcase/validate | Check a showcase for you. Saves nothing. Always 200 with a report |
GET /me/showcase | Your saved showcase (showcase: null when you have none) |
PUT /me/showcase | Validate and save (replacing yours). 400 with the problems when it doesn't validate |
DELETE /me/showcase | Delete it. {"deleted": false} when there was none |
Your public page document (GET /public/u/{username}, see Public profile API) carries the showcase as visitors see it, products filled in, under showcase (or null).
A validation report names every problem with a JSON Pointer to it and a plain message:
{
"ok": false,
"errors": [
{ "path": "/blocks/0/motion", "message": "hero blocks take motion rise, drift, pin-and-play, still, not `count-up`", "code": "invalid" },
{ "path": "/blocks/1/content/product", "message": "Free shows one project. Showing more needs Taste Pro ($5/month, coming soon).", "code": "plan_limit" }
],
"warnings": [],
"showcase": { "schema": 1, "intensity": "subtle", "blocks": [] },
"preview": null,
"projects": 2,
"project_limit": 1,
"plan": "free"
}code is one of invalid, unknown_product (not one of your live products), product_incomplete (the product lacks what the block needs), objectionable_content, plan_limit or too_large. warnings don't stop a save: product_pending is a ship log whose product has no updates yet, which appears with the first one. showcase is the document as it would be saved; preview is the page as it would show now, products filled in.
With your AI
Over MCP, five tools build a showcase with you: get_showcase_schema (the schema, the catalogue and your plan's limit), validate_showcase (every problem, and a preview; saves nothing), get_showcase, set_showcase (saves, after you've seen the preview and said yes) and delete_showcase. Ask something like "make my Taste showcase from my products", look at the preview, then say save.
Examples
One app on the free plan, built from the product goals:
{
"$schema": "https://taste.dev/schemas/showcase-1.json",
"schema": 1,
"intensity": "subtle",
"blocks": [
{
"type": "hero",
"motion": "drift",
"content": {
"product": "goals",
"persona": "For big goals",
"note": "Rated 4.7 on the App Store."
}
},
{
"type": "stats",
"motion": "count-up",
"content": { "product": "goals" }
},
{
"type": "device",
"motion": "pin-and-play",
"content": {
"product": "goals",
"frame": "phone",
"title": "A coach in your pocket.",
"body": "Goals, habits and the Growth Coach, one tap apart."
}
},
{
"type": "quote",
"motion": "cycle",
"content": {
"quotes": [
{
"quote": "Has actually changed my life. Reminds me of my goals, and keeps me working harder.",
"highlight": "Has actually changed my life.",
"name": "Cody E.",
"source": "App Store",
"rating": 5
},
{
"quote": "I didn’t know productivity could be so serene.",
"highlight": "productivity could be so serene",
"name": "Emily L.",
"source": "App Store",
"rating": 5
}
]
}
},
{
"type": "shiplog",
"motion": "stagger",
"content": { "product": "goals" }
},
{
"type": "about",
"motion": "rise",
"content": {
"name": "Terry Xu",
"role": "Founder, Mindful Suite",
"location": "Boston",
"bio": "I was a software engineer at Twitter and a few startups, pushing hard and quietly burning out. Goals is how I learned to aim at what matters.",
"links": [{ "label": "mindfulsuite.com", "url": "https://www.mindfulsuite.com" }]
}
},
{
"type": "badges",
"motion": "rise",
"content": {
"product": "goals",
"title": "Get Goals",
"appStore": "https://apps.apple.com/app/id1474850893",
"googlePlay": "https://play.google.com/store/apps/details?id=com.mindfulsuite.goals",
"note": "On iPhone, iPad and Android."
}
}
]
}Several apps (Taste Pro): a hero, a screenshot strip, a ship log and an app grid mixing products with an app that isn't on Taste:
{
"$schema": "https://taste.dev/schemas/showcase-1.json",
"schema": 1,
"intensity": "expressive",
"blocks": [
{
"type": "hero",
"motion": "pin-and-play",
"content": {
"product": "goals",
"persona": "Mindful Suite",
"body": "Calm tools for living on purpose: goals, priorities, and a vision you can see."
}
},
{
"type": "strip",
"motion": "stagger",
"content": {
"product": "priorities",
"title": "Inside Priorities",
"frame": "phone"
}
},
{
"type": "shiplog",
"motion": "stagger",
"content": { "product": "priorities" }
},
{
"type": "apps",
"motion": "float",
"content": {
"title": "More from Mindful Suite",
"apps": [
{ "product": "priorities" },
{ "product": "vision" },
{ "product": "amelio", "tagline": "A private circle for close friends." },
{
"name": "Feng Shui",
"tagline": "Know what’s off. Then fix it.",
"icon": "https://taste.dev/kit/mindful/icons/feng-shui.webp",
"url": "https://fengshui.mindfulsuite.com"
}
]
}
},
{
"type": "about",
"motion": "stagger",
"content": {
"name": "Terry Xu",
"role": "Founder, Mindful Suite",
"bio": "In 2019 I founded Mindful Suite to answer one question: how can technology help us live more intentionally, not just get more done?",
"links": [
{ "label": "mindfulsuite.com", "url": "https://www.mindfulsuite.com" },
{ "label": "My story", "url": "https://www.mindfulsuite.com/about" }
]
}
}
]
}No products at all (free, no projects): your intro, how you build, your journey, a film and a review:
{
"$schema": "https://taste.dev/schemas/showcase-1.json",
"schema": 1,
"intensity": "still",
"blocks": [
{
"type": "about",
"motion": "rise",
"content": {
"name": "Ada Park",
"role": "Indie developer",
"photo": "https://taste.dev/kit/mindful/feelings/happy.webp",
"bio": "I make small, calm apps and write about building them in public.",
"location": "Lisbon"
}
},
{
"type": "features",
"motion": "stagger",
"content": {
"title": "How I build",
"items": [
{ "icon": "target", "title": "Small on purpose", "body": "One problem per app, solved all the way." },
{ "icon": "leaf", "title": "Calm by default", "body": "No streak guilt, no badges for their own sake." },
{ "icon": "chat", "title": "In public", "body": "Every release is a post on Taste first." }
]
}
},
{
"type": "journey",
"motion": "stagger",
"content": {
"title": "So far",
"steps": [
{ "when": "2024", "title": "First app", "body": "A timer that only does one thing." },
{ "when": "2025", "title": "Went full time" },
{ "when": "2026", "title": "Joined Taste", "body": "Building in public, one update at a time." }
]
}
},
{
"type": "film",
"motion": "rise",
"content": {
"title": "Meet Amelio",
"body": "Be real. Know yourself. Grow together.",
"poster": "https://taste.dev/kit/mindful/amelio-film-poster.webp",
"video": "https://www.mindfulsuite.com/videos/amelio-intro-wide.mp4",
"length": "0:30"
}
},
{
"type": "quote",
"motion": "rise",
"content": {
"quotes": [
{ "quote": "It’s crazy how much I get done when I only try to do three things.", "highlight": "how much I get done", "name": "Sissy V.", "source": "Google Play", "rating": 5 }
]
}
}
]
}These three files are in the repository under shared/showcase/examples/, and the tests validate them against the schema and the API.