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.
Demo
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.qwemain.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>}Related
BlogrollThe blogs a writer recommends, in groups: name, a line about each, its latest post and how long ago, its feed, and the list as OPML.9 views · 8 downloadsPublishPanelThe editor's panel that puts a post out: its status, who may read it, now or scheduled, its address, then Save draft and Publish or Schedule.8 views · 9 downloadsSeoPreviewA post's title and description for search and sharing, with counts that say short, good or too long, and a live search result and link card.7 views · 8 downloadsAuthorBioThe box at the end of a post about its author: picture or initials, name and role, a short bio, links elsewhere, their post count and a follow link.8 views · 9 downloadsClapButtonApplause for a post: each press adds a clap up to a limit per reader, a +3 rises for presses in a row, and a small × takes them back.8 views · 8 downloadsPostArchiveA blog's posts by year and month, newest first: each year a disclosure with its count, its months and their posts, or only the months as links.10 views · 8 downloads
Thread
Thread 0 posts
On FeedSubscribe: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.