Project Scope and ToDos
- Take a link and turn it into an oEmbed/Open Graph style share card
- Take a link and archive it in the most reliable way
- When the link is a tweet, display the tweet but also the whole tweet thread.
- When the link is a tweet, archive the tweets, and display them if the live ones are not available.
- Capture any embedded retweets in the thread. Capture their thread if one exists
- Capture any links in the Tweet
- Create the process as an abstract function that returns the data in a savable way
- Archive links on Archive.org and save the resulting archival links
- Create link IDs that can be used to cache related content
- Integrate it into the site to be able to make context pages here.
- Check if a link is still available at build time and rebuild the block with links to an archived link
- Use v1 Twitter API to get Gifs and videos
- Pull Twitter images into Eleventy archive.
- Add YouTube DL tool.
- Use https://github.com/oduwsdl/archivenow?
Day 16
Ok, so I wasn't actively logging the last two days of work because a lot of it was random fiddles that I didn't think would take very long and a bunch of playing around with styling. But it all turned out to take a lot longer than I thought, and ended up more complicated.
First I decided to make a more complex take on the embed HTML based on some stuff I learned from work. Specifically, I decided I wanted to more strongly encapsulate the styles based on custom HTML elements and the shadow DOM.
I played around a bunch in Glitch with HTML and styles to form the embed design I want for non-oEmbed cards.
I've ended up with the HTML (here filled with sample data):
    <contexter-box
      class="contexter-box"
      itemscope=""
      itemtype="https://schema.org/CreativeWork"
      >
      <contexter-thumbnail class="contexter-box__thumbnail" slot="thumbnail"
        ><img
          src="https://github.com/AramZS/aramzs.github.io/blob/master/_includes/beamdown.gif?raw=true"
          alt=""
          itemprop="image" /></contexter-thumbnail
      ><contexter-box-head
        slot="header"
        class="contexter-box__head"
        itemprop="headline"
        ><contexter-box-head
          slot="header"
          class="contexter-box__head"
          itemprop="headline"
          ><a
            is="contexter-link"
            href="http://aramzs.github.io/jekyll/schema-dot-org/2018/04/27/how-to-make-your-jekyll-site-structured.html"
            itemprop="url"
            target="_blank"
            >How to give your Jekyll Site Structured Data for Search with
            JSON-LD</a
          ></contexter-box-head
        ></contexter-box-head
      ><contexter-byline class="contexter-box__byline" slot="author"
        ><span class="p-name byline" rel="author" itemprop="author"
          >Aram Zucker-Scharff</span
        ></contexter-byline
      ><time
        class="dt-published published"
        slot="time"
        itemprop="datePublished"
        datetime="2018-04-27T22:00:51.000Z"
        >3/27/2018</time
      ><contexter-summary
        class="p-summary entry-summary"
        itemprop="abstract"
        slot="summary"
        ><p>
          Let's make your Jekyll site work with Schema.org structured data and
          JSON-LD.
        </p></contexter-summary
      ><contexter-keywordset
        itemprop="keywords"
        slot="keywords"
        class="contexter-box__keywordset"
        ><span rel="category tag" class="p-category" itemprop="keywords"
          >jekyll</span
        >,
        <span rel="category tag" class="p-category" itemprop="keywords"
          >schema-dot-org</span
        >,
        <span rel="category tag" class="p-category" itemprop="keywords"
          >Code</span
        ></contexter-keywordset
      ><a
          href="https://web.archive.org/web/20220219224214/https://aramzs.github.io/jekyll/schema-dot-org/2018/04/27/how-to-make-your-jekyll-site-structured.html"
          is="contexter-link"
          target="_blank"
          class="read-link archive-link"
          itemprop="archivedAt"
             slot="archive-link"
          >Archived</a
        > | <a
          is="contexter-link"
          href="http://aramzs.github.io/jekyll/schema-dot-org/2018/04/27/how-to-make-your-jekyll-site-structured.html"
          class="read-link main-link"
          itemprop="sameAs"
          target="_blank"
            slot="read-link"
          >Read</a
        ></contexter-box
    >And to make that work, I'll have to insert the following Javascript to make the functionality run with custom HTML elements and add the shadow DOM:
		window.contexterSetup = window.contexterSetup ? window.contexterSetup : function() {
			window.contexterSetupComplete = true;
		class ContexterLink extends HTMLAnchorElement {
		constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
		}
		connectedCallback() {
			this.setAttribute("target", "_blank");
		}
		}
		// https://stackoverflow.com/questions/70716734/custom-web-component-that-acts-like-a-link-anchor-tag
		customElements.define("contexter-link", ContexterLink, {
		extends: "a",
		});
		customElements.define(
		"contexter-inner",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__inner";
			}
		}
		);
		customElements.define(
		"contexter-thumbnail",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__thumbnail";
			}
		}
		);
		customElements.define(
		"contexter-byline",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__byline";
			}
		}
		);
		customElements.define(
		"contexter-keywordset",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__keywordset";
			}
		}
		);
		customElements.define(
		"contexter-linkset",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__linkset";
			}
		}
		);
		customElements.define(
		"contexter-meta",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "contexter-box__meta";
			}
		}
		);
		customElements.define(
		"contexter-summary",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			attributeChangedCallback(name, oldValue, newValue) {
			}
			connectedCallback() {
			this.className = "p-summary entry-summary";
			}
		}
		);
		customElements.define(
		"contexter-box-head",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			connectedCallback() {
			this.className = "contexter-box__head";
			}
		}
		);
		customElements.define(
		"contexter-box-inner",
		class extends HTMLElement {
			constructor() {
			// Always call super first in constructor
			super();
			// Element functionality written in here
			}
			connectedCallback() {
			}
		}
		);
		// https://developers.google.com/web/fundamentals/web-components/best-practices
		class ContexterBox extends HTMLElement {
		constructor() {
			// Always call super first in constructor
			super();
			this.first = true;
			this.shadow = this.attachShadow({ mode: "open" });
		}
		connectedCallback() {
			if (this.first){
			this.first = false
			var style = document.createElement("style");
			style.innerHTML = `
					:host {
						--background: #f5f6f7;
						--border: darkblue;
						--blue: #0000ee;
						--font-color: black;
						--inner-border: black;
						font-family: Franklin,Arial,Helvetica,sans-serif;
						font-size: 14px;
						background: var(--background);
						width: 600px;
						color: var(--font-color);
						min-height: 90px;
						display: block;
						padding: 8px;
						border: 1px solid var(--border);
						cursor: pointer;
						box-sizing: border-box;
						margin: 6px;
						contain: content;
					}
					// can only select top-level nodes with slotted
					::slotted(*) {
						max-width: 100%;
						display:block;
					}
					::slotted([slot=thumbnail]) {
						max-width: 100%;
						display:block;
					}
					::slotted([slot=header]) {
						width: 100%;
						font-size: 1.25rem;
						font-weight: bold;
						display:block;
						margin-bottom: 6px;
					}
					::slotted([slot=author]) {
						max-width: 50%;
						font-size: 12px;
						display:inline-block;
						float: left;
					}
					::slotted([slot=time]) {
						max-width: 50%;
						font-size: 12px;
						display:inline-block;
						float: right;
					}
					::slotted([slot=summary]) {
						width: 100%;
						margin-top: 6px;
						padding: 10px 2px;
						border-top: 1px solid var(--inner-border);
						font-size: 15px;
						display:inline-block;
						margin-bottom: 6px;
					}
					contexter-meta {
						height: auto;
						margin-bottom: 4px;
						width: 100%;
						display: grid;
						position: relative;
						min-height: 16px;
						grid-template-columns: repeat(2, 1fr);
					}
					::slotted([slot=keywords]) {
						width: 80%;
						padding: 2px 4px;
						border-top: 1px solid var(--inner-border);
						font-size: 11px;
						display: block;
						float: right;
						font-style: italic;
						text-align: right;
						grid-column: 2/2;
						grid-row: 1;
						align-self: end;
						justify-self: end;
					}
					::slotted([slot=archive-link]) {
						font-size: 1em;
						display: inline;
					}
					::slotted([slot=archive-link])::after {
						content: "|";
						display: inline;
						color: var(--font-color);
						text-decoration: none;
						margin: 0 .5em;
					}
					::slotted([slot=read-link]) {
						font-size: 1em;
						display: inline;
					}
					contexter-linkset {
						width: 80%;
						padding: 2px 4px;
						font-size: 13px;
						float: left;
						font-weight: bold;
						grid-row: 1;
						grid-column: 1/2;
						align-self: end;
						justify-self: start;
					}
					/* Extra small devices (phones, 600px and down) */
					@media only screen and (max-width: 600px) {
						:host {
						width: 310px;
						}
					}
					/* Small devices (portrait tablets and large phones, 600px and up) */
					@media only screen and (min-width: 600px) {...}
					/* Medium devices (landscape tablets, 768px and up) */
					@media only screen and (min-width: 768px) {...}
					/* Large devices (laptops/desktops, 992px and up) */
					@media only screen and (min-width: 992px) {...}
					/* Extra large devices (large laptops and desktops, 1200px and up) */
					@media only screen and (min-width: 1200px) {...}
					@media (prefers-color-scheme: dark){
						:host {
						--background: #354150;
						--border: #1f2b37;
						--blue: #55b0ff;
						--font-color: #ffffff;
						--inner-border: #787a7c;
						background: var(--background);
						border: 1px solid var(--border)
						}
					}
				`;
			var lightDomStyle = document.createElement("style");
			lightDomStyle.innerHTML = `
					contexter-box {
						contain: content;
					}
					contexter-box .read-link {
						font-weight: bold;
					}
					contexter-box a {
						color: #0000ee;
					}
					contexter-box img {
						width: 100%;
						border: 0;
						padding: 0;
						margin: 0;
					}
					/* Extra small devices (phones, 600px and down) */
					@media only screen and (max-width: 600px) {...}
					/* Small devices (portrait tablets and large phones, 600px and up) */
					@media only screen and (min-width: 600px) {...}
					/* Medium devices (landscape tablets, 768px and up) */
					@media only screen and (min-width: 768px) {...}
					/* Large devices (laptops/desktops, 992px and up) */
					@media only screen and (min-width: 992px) {...}
					/* Extra large devices (large laptops and desktops, 1200px and up) */
					@media only screen and (min-width: 1200px) {...}
					@media (prefers-color-scheme: dark){
						contexter-box a {
						color: #55b0ff;
						}
					}
			`;
			this.appendChild(lightDomStyle);
			//https://stackoverflow.com/questions/49678342/css-how-to-target-slotted-siblings-in-shadow-dom-root
			this.shadow.appendChild(style);
			// https://developers.google.com/web/fundamentals/web-components/shadowdom
			// https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots
			const innerContainer = document.createElement("contexter-box-inner")
			this.shadow.appendChild(innerContainer)
			// https://javascript.info/slots-composition
			const innerSlotThumbnail = document.createElement('slot');
			innerSlotThumbnail.name = "thumbnail"
			innerContainer.appendChild(innerSlotThumbnail)
			const innerSlotHeader = document.createElement('slot');
			innerSlotHeader.name = "header"
			innerContainer.appendChild(innerSlotHeader)
			const innerSlotAuthor = document.createElement('slot');
			innerSlotAuthor.name = "author"
			innerContainer.appendChild(innerSlotAuthor)
			const innerSlotTime = document.createElement('slot');
			innerSlotTime.name = "time"
			innerContainer.appendChild(innerSlotTime)
			const innerSlotSummary = document.createElement('slot');
			innerSlotSummary.name = "summary"
			innerContainer.appendChild(innerSlotSummary)
			const metaContainer = document.createElement("contexter-meta");
			innerContainer.appendChild(metaContainer)
			const innerSlotInfo = document.createElement('slot');
			innerSlotInfo.name = "keywords"
			metaContainer.appendChild(innerSlotInfo)
			const linkContainer = document.createElement("contexter-linkset");
			metaContainer.appendChild(linkContainer)
			const innerSlotArchiveLink = document.createElement('slot');
			innerSlotArchiveLink.name = "archive-link"
			linkContainer.appendChild(innerSlotArchiveLink)
			const innerSlotReadLink = document.createElement('slot');
			innerSlotReadLink.name = "read-link"
			linkContainer.appendChild(innerSlotReadLink)
			this.className = "contexter-box";
			this.onclick = (e) => {
				// console.log('Click on block', this)
				if (!e.target.className.includes('read-link') && !e.target.className.includes('title-link')) {
				const mainLinks = this.querySelectorAll('a.main-link');
				// console.log('mainLink', e, mainLinks)
				mainLinks[0].click()
				}
			}
			}
		}
		}
		customElements.define("contexter-box", ContexterBox);
}
if (!window.contexterSetupComplete){
	window.contexterSetup();
}You can see here I've made the entire box clickable by capturing any clicks (not on the archive link) and routing them to the Read link
	const mainLinks = this.querySelectorAll('a.main-link');
	mainLinks[0].click()I also don't want to re-run this script when there are multiple embeds so I encapsulate it inside a function call with a window level check that is set the first time I set up the script:
if (!window.contexterSetupComplete){
	window.contexterSetup();
}I originally set the links in a single element that contained both links. But I realized that I should slot them separately, to allow me to actively insert the archive link at another step for my Eleventy site's archive pages.
I had to fix my access of element properties, check the readability object as a backup for some of the finalizedMeta data, and fix the oembed for Twitter so it doesn't show replies in a thread that already includes replies.
I want to bring images used in the embeds local to the site. This turned out a lot harder than I expected.
I pulled tweets in easily enough, but realized I needed to print the author data for the Tweet to really make the archive readable.
Now that I have a local archive, I can take advantage of the slot at the point where I build the collection. If I don't have the archive link from Wayback I can use my own site's archive.
if (
	!contextData.data.archivedData.link &&
	!contextData.data.twitterObj
) {
	contextData.htmlEmbed =
		contextData.htmlEmbed.replace(
			`</contexter-box>`,
			`<a href="${options.domain}/${options.publicPath}/${contextData.sanitizedLink}" is="contexter-link" target="_blank" class="read-link archive-link" itemprop="archivedAt" slot="archive-link">Archived</a></contexter-box>`
		);
}Oops, I also need to add that to the initial build of in-article embeds too.
Ok, things are looking good! This is a good place to stop!