Blogging

FeedSubscribe

Follow this blog: its RSS, Atom or JSON feeds with their addresses to copy, links into feed readers, what a feed is, and an email box.

#blogging#feeds#subscribe#forms#interactive

Demo

Preview

Follow this blog

New posts every week or two, in your feed reader or your inbox.

What is a feed?

A feed lists a blog's newest posts in a form apps can read. Paste its address into a feed reader (an app like those above, or one on your phone) and new posts come to you, from every blog you follow, with no account here and nothing sent to anyone.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/feed_subscribe.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/feed_subscribe.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/feed_subscribe.qwe
view FeedSubscribeDemo() =    <FeedSubscribe id="feed-subscribe-demo" text="New posts every week or two, in your feed reader or your inbox."                   feeds={[{ label: "All posts", href: "https://blog.example.com/feed.xml" },                           { label: "Only the Quillo posts", href: "https://blog.example.com/tags/quillo/feed.xml", kind: "atom" },                           { label: "Week notes", href: "https://blog.example.com/week-notes/feed.json", kind: "json" }]}                   email_action="#feed_subscribe"/>

Source

ui/feed_subscribe.qwe
// FeedSubscribe: the "Follow this blog" box: each of the blog's feeds (RSS, Atom, JSON// Feed; all posts, or one tag's) with its address to copy in one press, links that// add the first feed to a feed reader, a short "What is a feed?" for those who do not// know, and, when given, an email box to get each post by email instead.////   <FeedSubscribe feeds={[{ label: "All posts", href: "https://blog.example.com/feed.xml" },//                          { label: "Only Quillo", href: "https://blog.example.com/tags/quillo.xml", kind: "atom" }]}//                  email_action="/subscribe"/>//// feeds: maps of { href, label?, kind? }: href the feed's full address; label what it// holds ("All posts"); kind "rss" (default), "atom" or "json", shown as a badge. title:// the heading ("Follow this blog"). text: a line under it. readers: offer to open the// first feed in Feedly, Inoreader and NewsBlur (true). explain: the "What is a feed?"// note (true). email_action: where an email box posts (`email`); none leaves it out.// email_label: the line over it ("Or get each new post by email"). id: the <section>'s id// ("feed-subscribe"), and a prefix for the others. Copy writes to the clipboard and// says "Copied" for a moment; where the browser will not, the address is selected for// Ctrl+C.import ui/theme.qwe use ui_clslet feed_subscribe_css = css`.ui-bfeed { display: grid; gap: 14px; min-width: 0; max-width: 520px; margin: 0; padding: 20px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-bfeed-head { display: flex; align-items: center; gap: 12px; }.ui-bfeed-icon { flex: none; display: grid; place-items: center; width: 38px; height: 38px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft);  color: var(--ui-warning); }.ui-bfeed-icon svg { width: 20px; height: 20px; }.ui-bfeed .ui-bfeed-title { margin: 0; font-size: 17px; font-weight: 700; }.ui-bfeed-text { margin: 2px 0 0; color: var(--ui-muted); }.ui-bfeed-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }.ui-bfeed-feed { display: grid; gap: 5px; }.ui-bfeed-name { display: flex; align-items: center; gap: 8px; font-weight: 600; }.ui-bfeed-kind { padding: 0 6px; border-radius: 4px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 10px; font-weight: 700;  letter-spacing: .06em; line-height: 18px; text-transform: uppercase; }.ui-bfeed-row { display: flex; gap: 6px; }.ui-bfeed-row input { flex: 1; min-width: 0; height: 36px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-subtle); color: var(--ui-fg); font: 13px var(--ui-mono); }.ui-bfeed-row input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-bfeed-copy { position: relative; flex: none; display: inline-grid; min-width: 82px; height: 36px; padding: 0 12px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 600 13px var(--ui-font); cursor: pointer; }.ui-bfeed-copy > span { grid-area: 1 / 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; }.ui-bfeed-copy svg { width: 14px; height: 14px; }.ui-bfeed-copy:hover { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-bfeed-copy:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bfeed-copy .ui-bfeed-done { visibility: hidden; color: var(--ui-success); }.ui-bfeed-ok .ui-bfeed-done { visibility: visible; } .ui-bfeed-ok .ui-bfeed-label { visibility: hidden; }.ui-bfeed-ok { border-color: var(--ui-success); }.ui-bfeed-bad-note { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-bfeed-readers { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-bfeed-readers a { display: inline-block; padding: 3px 10px; border-radius: 999px; background: var(--ui-subtle); color: var(--ui-fg); font-weight: 500;  text-decoration: none; }.ui-bfeed-readers a:hover { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-bfeed-readers a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bfeed-what summary { display: inline-block; color: var(--ui-accent); font-size: 13px; font-weight: 600; cursor: pointer; border-radius: 3px; }.ui-bfeed-what summary:hover { text-decoration: underline; }.ui-bfeed-what summary:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bfeed-what p { margin: 6px 0 0; color: var(--ui-muted); font-size: 13px; }.ui-bfeed-email { display: grid; gap: 6px; padding-top: 14px; border-top: 1px solid var(--ui-border); }.ui-bfeed-email label { font-weight: 600; }.ui-bfeed-email div { display: flex; gap: 6px; }.ui-bfeed-email input { flex: 1; min-width: 0; height: 38px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); }.ui-bfeed-email input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-bfeed-email button { flex: none; height: 38px; padding: 0 16px; border: 0; border-radius: var(--ui-radius-sm); background: var(--ui-accent);  color: var(--ui-accent-fg); font: 600 14px var(--ui-font); cursor: pointer; }.ui-bfeed-email button:hover { filter: brightness(1.08); }.ui-bfeed-email button:focus-visible { outline: none; box-shadow: var(--ui-focus); }`let feed_subscribe_readers = [    { name: "Feedly", at: "https://feedly.com/i/subscription/feed/" },    { name: "Inoreader", at: "https://www.inoreader.com/?add_feed=" },    { name: "NewsBlur", at: "https://www.newsblur.com/?url=" },]component FeedSubscribe(feeds: list, title: str = "Follow this blog", text: str? = null, readers: bool = true, explain: bool = true,                        email_action: str? = null, email_label: str = "Or get each new post by email", id: str = "feed-subscribe",                        class: str? = null) {    style global feed_subscribe_css    state copied = ""     // the href just copied    state failed = ""     // the href the browser would not copy    if copied != "" { every(1600, () => { copied = "" }) }    let copy = (e, href: str) => {        let box = e.target.closest(".ui-bfeed-feed").querySelector("input")        failed = ""        if js("isSecureContext") != true {            failed = href            box.select()        } else {            // The promise's callbacks are given undefined, which is no argument: hence `= null`.            js("navigator.clipboard.writeText", href).then((_ = null) => { copied = href }, (_ = null) => {                failed = href                box.select()            })        }    }    let first_feed = first(feeds)    <section id={id} class={ui_cls(["ui-bfeed", class])} aria-labelledby={`${id}-title`}>        <div.ui-bfeed-head>            <span.ui-bfeed-icon aria-hidden="true">                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">                    <path d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1.5" fill="currentColor"/>                </svg>            </span>            <div>                <h2.ui-bfeed-title id={`${id}-title`}>{title}</h2>                {text != null && <p.ui-bfeed-text>{text}</p>}            </div>        </div>        <ul.ui-bfeed-list>            {for i in range(len(feeds)) => {                let f = feeds[i]                let box_id = `${id}-feed-${i + 1}`                let ok = copied == f.href                <li.ui-bfeed-feed key={f.href}>                    <label.ui-bfeed-name for={box_id}>                        {f.label ?? "All posts"}<span.ui-bfeed-kind>{f.kind == "json" ? "JSON Feed" : (f.kind == "atom" ? "Atom" : "RSS")}</span>                    </label>                    <div.ui-bfeed-row>                        <input type="text" id={box_id} value={f.href} readonly spellcheck="false" @focus={(e) => { e.target.select() }}/>                        <button type="button" class={ui_cls(["ui-bfeed-copy", ok && "ui-bfeed-ok"])} aria-label={`Copy the address of ${f.label ?? "the feed"}`}                                @click={(e) => copy(e, f.href)}>                            <span.ui-bfeed-label aria-hidden="true">                                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                                    <path d="M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2ZM4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"/>                                </svg>Copy                            </span>                            <span.ui-bfeed-done aria-hidden="true">                                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Copied                            </span>                        </button>                    </div>                    {failed == f.href && <p.ui-bfeed-bad-note>Your browser would not copy it: it is selected, press Ctrl+C (⌘C on a Mac).</p>}                </li>            }}        </ul>        <span.ui-sr role="status">{copied != "" ? "Copied the feed's address." : ""}</span>        {readers && first_feed != null && <ul.ui-bfeed-readers aria-label="Add it to a feed reader">            <li>Add to</li>            {for r in feed_subscribe_readers => <li key={r.name}>                <a href={r.at + url_encode(first_feed.href)} target="_blank" rel="noopener">{r.name}</a>            </li>}        </ul>}        {explain && <details.ui-bfeed-what>            <summary>What is a feed?</summary>            <p>A feed lists a blog's newest posts in a form apps can read. Paste its address into a feed reader (an app like those               above, or one on your phone) and new posts come to you, from every blog you follow, with no account here and nothing               sent to anyone.</p>        </details>}        {email_action != null && <form.ui-bfeed-email method="post" action={email_action}>            <label for={`${id}-email`}>{email_label}</label>            <div>                <input type="email" id={`${id}-email`} name="email" required autocomplete="email" placeholder="you@example.com"/>                <button type="submit">Subscribe</button>            </div>        </form>}    </section>}
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

On FeedSubscribe: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.