\widget $contact.quote(contact)
<$link to=<<contact>>><$text text={{{ [<contact>has[caption]get[caption]] ~[<contact>get[title]] }}}/></$link>:
<blockquote>
<$slot $name="ts-raw"/>
</blockquote>
\end
<$contact.quote contact="JaneDoe">
This is how you do the thing.
</$contact.quote>Note that you need either has[caption] before or !is[blank] after the get[caption] step – otherwise the first filter will have one output tiddler that is the empty string, rather than zero output tiddlers, and the second filter run won't trigger (recall that ~ is another name for :else, i.e., use this run only if the previous run had no output). This is just a perhaps unexpected quirk of the get filter operator, previously mentioned in Ex:DefaultToField.
Another option for the fallback from caption to title is to use nested $transclude widgets, since the body of each $transclude widget is used if the tiddler or field is missing:
<$link to=<<contact>>>
<$transclude $tiddler=<<contact>> $field="caption">
<$transclude $tiddler=<<contact>> $field="title"/>
</$transclude>:
</$link>