the
cohost!
web component v0.1

This post that you're reading isn't a post at all. It's not even an <iframe> pointing to cohost! It's a custom element made to replicate the look of a post on cohost. It requires no 3rd party dependencies - just a JS and CSS file.

<​cohost-post
   avatarsrc="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIcSUNDX1BST0ZJTEUAAQEAAAIMbGNtcwIQAABtbnRyUkdCIFhZWiAH3AABA"
   avatarshape="squircle"
   comments="0"
   displayname="ash"
   handle="astral"
   originalurl="https://cohost.org/astral/post/7796679-sometimes-i-think-i/e20546ba0978407f9e7a378a0f7d0fad"
   standaloneheadline="sometimes, I think I can still hear eggbug's voice..."
   tags=""
>
   <div class="co-prose prose my-4 overflow-hidden break-words px-3">
     <p>This is so sad. Lorem ipsum dolor sit amet</p>
   </div>
</​cohost-post>

Save these two files to your web server:

(You'll also want to add Atkinson Hyperlegible as a webfont via another CSS file.)

Next, put these tags in your webpage's <head> element:

<script type="module" src="/assets/cohost-wc.js"></script>
<link rel="stylesheet" href="/assets/cohost-wc.css" />

(Replace /assets/ with whatever web-facing directory you saved the files to! And don't forget the webfont CSS file too!!)

Now your page has access to two custom elements, <cohost-post> and <cohost-shared-item>. Keep reading to see how you can create these elements!

I made this bookmarklet that you can add to your browser's Bookmarks tab to quickly generate the custom element for a post:

javascript:void%20function(){(async()=%3E{async%20function%20a(a,b){if(!b)return%20a;let%20c;if(a.startsWith(d))c=a.replace(d,%22https://cohost-web-component.meow.garden/cohostcdn-cors-proxy/%22),a.includes(%22/avatar/%22)%26%26(c+=%22%3Fdpr=2%26width=32%26height=32%26fit=cover%26auto=webp%22);else%20if(a.startsWith(e))c=a.replace(e,%22https://cohost-web-component.meow.garden/cohost-static-cors-proxy/%22);else%20return%20a;let%20f=await%20fetch(c),g=await%20f.blob();return%20new%20Promise(a=%3E{let%20b=new%20FileReader;b.onload=()=%3Ea(b.result),b.readAsDataURL(g)})}async%20function%20b(b){let%20c=JSON.parse(document.getElementById(%22trpc-dehydrated-state%22).innerText),f=c.queries.find(a=%3Ea.queryKey[0].includes(%22singlePost%22)).state.data.post,g=document.createElement(%22cohost-post%22);if(g.setAttribute(%22avatarSrc%22,await%20a(f.postingProject.avatarURL,b)),g.setAttribute(%22avatarShape%22,f.postingProject.avatarShape),g.setAttribute(%22comments%22,f.numComments),g.setAttribute(%22displayName%22,f.postingProject.displayName),g.setAttribute(%22handle%22,f.postingProject.handle),g.setAttribute(%22originalUrl%22,f.singlePostPageUrl),f.publishedAt%26%26g.setAttribute(%22publishedAt%22,f.publishedAt),f.numSharedComments%26%26g.setAttribute(%22sharedComments%22,f.numSharedComments),f.headline%26%26g.setAttribute(%22standaloneHeadline%22,f.headline),g.setAttribute(%22tags%22,f.tags.join(%22,%22)),f.shareTree%26%26f.shareTree.length){g.setAttribute(%22sharedItems%22,%22%22);let%20c=f.shareTree[f.shareTree.length-1];g.setAttribute(%22sharedAvatarSrc%22,await%20a(c.postingProject.avatarURL,b)),g.setAttribute(%22sharedAvatarShape%22,c.postingProject.avatarShape),g.setAttribute(%22sharedDisplayName%22,c.postingProject.displayName),g.setAttribute(%22sharedHandle%22,c.postingProject.handle);let%20d=[...f.shareTree,f],e=!0;for(sharedPost%20of%20d){if(null!==sharedPost.transparentShareOfPostId)continue;let%20c=document.createElement(%22cohost-shared-item%22);c.setAttribute(%22avatarSrc%22,await%20a(sharedPost.postingProject.avatarURL,b)),c.setAttribute(%22avatarShape%22,sharedPost.postingProject.avatarShape),c.setAttribute(%22displayName%22,sharedPost.postingProject.displayName),c.setAttribute(%22handle%22,sharedPost.postingProject.handle),sharedPost.headline%26%26c.setAttribute(%22headline%22,sharedPost.headline),c.setAttribute(%22originalUrl%22,sharedPost.singlePostPageUrl),sharedPost.publishedAt%26%26c.setAttribute(%22publishedAt%22,sharedPost.publishedAt),c.setAttribute(%22tags%22,sharedPost.tags.join(%22,%22));let%20d=document.getElementById(`post-${sharedPost.postId}`);if(d){let%20a=d.parentElement.querySelector(%22[data-post-body]%22);if(a){let%20b=document.createElement(%22noscript%22);b.innerText=`${e%3F%22Posted%22:%22Shared%22}%20by%20${sharedPost.postingProject.displayName||%22%22}%20(%40${sharedPost.postingProject.handle}):%20${sharedPost.headline||%22%22}`,c.appendChild(b),c.append(...a.cloneNode(!0).childNodes)}}g.appendChild(c),e=!1}}else{let%20a=document.getElementById(`post-${f.postId}`);if(a){let%20a=document.getElementById(`post-${f.postId}`).parentElement.querySelector(%22[data-post-body]%22);if(a){let%20b=document.createElement(%22noscript%22);if(f.shareTree%26%26f.shareTree.length){let%20a=f.shareTree[f.shareTree.length-1];b.innerText=`Post%20by%20${a.postingProject.displayName||%22%22}%20(%40${a.postingProject.handle}),%20shared%20by%20${f.postingProject.displayName||%22%22}%20(%40${f.postingProject.handle}):%20${f.headline||%22%22}`}else%20b.innerText=`Post%20by%20${f.postingProject.displayName||%22%22}%20(%40${f.postingProject.handle}):%20${f.headline||%22%22}`;g.appendChild(b),g.append(...a.cloneNode(!0).childNodes)}}}let%20h=!1,i=!1;for(const%20c%20of%20g.querySelectorAll(%22img,%20audio%22)){%22AUDIO%22==c.tagName%26%26(i=!0);let%20f=c.getAttribute(%22src%22);b%26%26f.startsWith(e)%3Fc.setAttribute(%22src%22,await%20a(f,!0)):(f.startsWith(%22https://cohost.org/%22)||f.startsWith(d))%26%26(h=!0)}return{postElement:g,mediaFlag:h,audioPlayerFlag:i}}async%20function%20c(a,d){let%20e=document.createElement(%22template%22);e.innerHTML=`%3Cdiv%20id=%22${g}%22%20class=%22fixed%20bottom-0%20p-2%20flex%20flex-col%20items-center%20gap-2%20font-sm%20bg-background%20co-themed-box%20border%20rounded-lg%22%3E %20%20%20%20%20%20%3Ctextarea%20rows=%225%22%20cols=%2235%22%20style=%22font-size:%20inherit;%20line-height:%201.4%22%3E%3C/textarea%3E %20%20%20%20%20%20%3Cdiv%20id=%22${h}%22%20class=%22co-info-box%20co-info%20text-sm%20mx-auto%20w-full%20rounded-lg%20p-3%22%3E %20%20%20%20%20%20%20%20Post%20%3Cb%3Estill%3C/b%3E%20contains%20media%20hosted%20on%20cohost.%3Cbr%20/%3EConsider%20rehosting%20any%20images%20or%20audio%20files%20on%20your%20website. %20%20%20%20%20%20%3C/div%3E %20%20%20%20%20%20%3Cdiv%20id=%22${i}%22%20class=%22co-info-box%20co-info%20text-sm%20mx-auto%20w-full%20rounded-lg%20p-3%22%3E %20%20%20%20%20%20%20%20cohost's%20audio%20player%20doesn't%20function%20correctly%20(yet). %20%20%20%20%20%20%3C/div%3E %20%20%20%20%20%20%3Cdiv%20class=%22flex%20flex-row%20gap-1%22%3E %20%20%20%20%20%20%20%20%3Cbutton%20id=%22${j}%22%20class=%22rounded-lg%20bg-cherry%20py-2%20px-4%20text-sm%20font-bold%20text-notWhite%20hover:bg-cherry-600%20active:bg-cherry-700%22%3EConvert%20avatars%20and%20custom%20emoji%20to%20data%20URIs%3C/button%3E %20%20%20%20%20%20%20%20%3Cbutton%20id=%22${k}%22%20class=%22rounded-lg%20bg-cherry%20py-2%20px-4%20text-sm%20font-bold%20text-notWhite%20hover:bg-cherry-600%20active:bg-cherry-700%22%3Edismiss%3C/button%3E %20%20%20%20%20%20%3C/div%3E %20%20%20%20%3C/div%3E`,e.content.querySelector(%22textarea%22).value=a.postElement.outerHTML,a.mediaFlag%3F!d%26%26e.content.querySelector(`%23${h}%20b`).remove():e.content.getElementById(h).remove(),a.audioPlayerFlag||e.content.getElementById(i).remove(),e.content.getElementById(j).onclick=async()=%3E{document.getElementById(j).style=%22cursor:%20progress%22;let%20a=await%20b(!0);c(a,!0)},e.content.getElementById(k).onclick=function(){document.getElementById(g).remove()};let%20f=document.getElementById(g);f%3Ff.replaceWith(e.content):document.body.append(e.content)}const%20d=%22https://staging.cohostcdn.org/%22,e=%22https://cohost.org/static/%22,f=%22cohost-wc-%22,g=f+%22bookmarklet-output%22,h=f+%22bookmarklet-media-flag%22,i=f+%22audio-player-flag%22,j=f+%22data-uris-button%22,k=f+%22dismiss-button%22;let%20l=await%20b(!1);c(l,!1)})()}();
How to add a bookmarklet (desktop only):
  1. Triple-click the code above to select it all. Press Ctrl+C to copy.
  2. Right-click your Bookmarks bar and choose Add page (Chrome) or Add bookmark (Firefox).
    • (If your Bookmarks bar is hidden, press Ctrl+Shift+B to show it.)
  3. Name the bookmark "cohost-wc" (or anything, really) and paste the code into the URL field.

Once you've added it to your bookmarks tab, you can click it while on any single post page on cohost (like this one) to generate the custom element for that post. Give it a shot! And act quickly, before cohost shuts down for good :eggbug-sob:

After generating the custom element, you can click the button labeled... Convert avatars and custom emoji to data URIs ...to durably encode these small images in the HTML itself. This is strongly recommended for future-proofing. It won't convert other images, though, so you may still need to rehost some pictures on your website.

Clicking the button will load the avatars & emoji through this website as a proxy due to CORS restrictions. I'm telling you this in case you consider it to be a privacy concern, although rest assured I don't care whose avatars are going through the proxy as long as you're not abusing the service.

⚠️ Show the whole post first If there are any content warnings or 18+ interstitials, make sure to click show post to dismiss them, otherwise the contents won't show up in the generated HTML! If there are any "read more" breaks, click them to show the whole post, otherwise only the post up to the break will show up in the generated HTML!

You shouldn't need to know these details if you're using the bookmarklet, but here they are for reference:

<cohost-post>
The outermost container of a post. Should either contain the HTML for a standalone post or <cohost-shared-item> elements for a shared post.

Attributes

All attributes are optional.

  • avatarSrc: URL or data URI
  • avatarShape: one of "squircle", "roundrect", "circle", "egg", "capsule-big", or "capsule-small"
  • displayName: the primary poster's display name
  • handle: the primary poster's handle
  • publishedAt: ISO 8601 date string, e.g. "2023-09-22T01:59:04.535Z"
  • originalUrl: URL of the original post
  • standaloneHeadline: headline for a standalone post (one that doesn't contain <cohost-shared-item>s)
  • sharedItems: boolean, include if post contains <cohost-shared-item>s
  • sharedAvatarSrc, sharedAvatarShape, sharedDisplayName, sharedHandle: the shared poster's details (shown after the share icon in the header)
  • tags: comma-separated tags for the post
  • comments: number of comments (default 0)
  • sharedComments: number of comments on shared posts (default 0 / hidden)

<cohost-shared-item>
A shared post inside of a <cohost-post>. These should always be direct children of the <cohost-post>, with no other elements in between.

Attributes

Attributes are the mostly same as for <cohost-post>, with these exceptions:

  • The headline is named headline instead of standaloneHeadline
  • No shared* attributes
  • No comments or sharedComments

Q: why do I need a CSS file alongside the JS file? can't web components style themselves?

A: web components can style themselves, but not their children. the CSS file provides styles for the post contents; in fact, every single rule in it is scoped to the cohost-post element.

Q: did you just copy cohost's entire stylesheet?

A: no, I painstakingly hand-selected styles until everything looked right. let me know if I missed something!

Think of this web component as being static for now. It has no interactivity, unless it's displaying an interactive CSS crime.

Known bugs (I'll fix these eventually):

  • Images cannot be expanded
  • Audio player doesn't work (renders correctly but clicking does nothing)

Other limitations (unlikely to change):

  • Content warnings / 18+ labels are not shown
  • "read more" / "read less" buttons are not shown
  • The "invert colors" option is unavailable; posts always follow the browser's light/dark theme
  • Avatars are always displayed in compact format (small and inside the post header, not large and to the left of the post)
  • Shares that only contain tags are not preserved
  • The Markdown source code is not preserved. You're encouraged to back it up separately.
  • The "like" and "share" buttons are shown, but cannot be clicked :eggbug-pensive: