Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: oembed

  • oEmbedding Hugo

    oEmbedding Hugo

    No, not Hugo …

    Hugo Weaving as "Mitzi Del Bra" from "The Adventures of Priscilla, Queen of the Desert"

    No no. Hugo

    HUGO - logo to the app.

    I’ve been using Hugo to power a part of a website for quite a while now. Since 2015 I’ve had a static site, a simple library, with around 2000 posts that I wanted to be a static, less-possible-to-be-hackable site. It’s purpose was to be an encyclopedia, and as a Hugo powered site, it works amazingly.

    But… I do use WordPress, and sometimes I want to link and embed.

    Integrations Are Queen

    It helps to have a picture of how I built things. Back in *cough* 1995, the site was a single HTML page. By 1996 it was a series of SHTML (yeah) with a manually edited gallery. Fast-forward to 2005 and we have a gallery in the thousands and a full blown wiki.

    Now. Never once did I have integrated logins. While I love it for the ease of … me, I hate it from a security standpoint. Today, the blog is powered by WordPress and the gallery by NetPhotoGraphics and the ‘wiki’ by Hugo (I call it a library now). Once in a while I’ll post articles or transcripts or recaps over on the library and I want to cross link to the blog to tell people “Hey! New things!”

    But… from a practical standpoint, what are the options?

    1. A plain ol’ link
    2. A ‘table’ list of articles/transcripts/etc by name with links
    3. oEmbed

    Oh yes. Option 3.

    oEmbed and Hugo is Complex

    Since Hugo is a static HTML generator, you have to create faux ‘endpoints’ and you cannot make a dynamic JSON generator per post. Most of the things you’ll find when you google for oEmbed and Hugo is how to make it read oEmbed (like “adding a generic oEmbed handler for Hugo“). I wanted the other way, so I broke down what I needed to do:

    1. Make the ‘oembed’ JSON
    2. Make the returning iframe
    3. Add the link/alternate tag to the regular HTML

    Unlike with NetPhotoGraphics, wherein I could make a single PHP file which generated the endpoints and the json and the iframe, I had to approach it from a different angle with Hugo, and ask myself “How do I want the ‘endpoints’ to look?

    See you actually can make a pseudo endpoint of example.com/json/link/to/page which would generate the iframe from example.com/link/to/page and then example.com/oembed/link/to/page but this comes with a weird cost. You will actually end up having multiple folders on your site, and you’d want to make an .htaccess to block things.

    This has to do with how Hugo (and most static site generators) make pages. See if I wanted to make a page for ‘about’, then I would go into /posts/ and make a file called about.md with the right headers. But that doesn’t make a file called about.html, it actually makes a folder in my public_html director, called about with a file in there named index.html — that’s basic web directory stuff, though.

    But Hugo has an extra trick, which allows you to make custom files. Most people use it to make AMP pages and they explain the system like this:

    A page can be output in as many output formats as you want, and you can have an infinite amount of output formats defined as long as they resolve to a unique path on the file system. In the above table, the best example of this is AMP vs. HTMLAMP has the value amp for Path so it doesn’t overwrite the HTML version; e.g. we can now have both /index.html and /amp/index.html.

    Except… your ‘unique path’ doesn’t have to be a path! And you can customize it to kick out differently named files. So instead of /index.html and /amp/index.html I could do /index-amp.html in the same location.

    So that means my options were:

    1. A custom folder (and subfolders) for every post per ‘type’ of output
    2. Subfiles in the already existing folder

    I picked the second and here’s how:

    Output Formats

    The secret sauce for Hugo is making a new set of output formats.

    outputFormats:
      iframe:
        name: "iframe"
        baseName: "iframe"
        mediaType: "text/html"
        isHTML: true
      oembed:
        name: "oembed"
        baseName: "oembed"
        mediaType: "application/json"
        isPlainText: true

    By omitting the path value and telling it that my baseName is iframe and oembed, I’m telling Hugo not to make a new folder, but to rename the files! Instead of making /oembed/index.html and /oembed/about/index.html I’m making /about/oembed.html!

    Boom.

    The next trick was to tell Hugo what ‘type’ of content should use those new formats:

    outputs:
      home: [ "HTML", "JSON", "IFRAME", "OEMBED" ]
      page: [ "HTML", "IFRAME", "OEMBED" ]
      section: [ "HTML", "IFRAME", "OEMBED" ]

    Home also has a JSON which is something I use for search. No one else needs it.

    New Template Files

    I’ll admit, this took me some trial and error. In order to have Hugo generate the right files, and not just a copy of the main index, you have to add new template files. Remember those basenames?

    • index.oembed.json
    • index.iframe.html

    Looks pretty obvious, right? The iframe file is the HTML for the iframe. The oembed is the JSON for oembed discovery. Those go right into the main layouts folder of your theme. But… I ended up having to duplicate things in order to get everything working and that meant I also made:

    • /_default/baseof.iframe.html
    • /_default/baseof.oembed.json
    • /_default/single.iframe.html
    • /_default/single.json

    Now, if you;’re wondering “Why is it named single.json?” I don’t know. What I know is if I named it any other way, I got this error:

    WARN: found no layout file for “oembed” for layout “single” for kind “page”: You should create a template file which matches Hugo Layouts Lookup Rules for this combination.

    So I did that and it works. I also added in these:

    • /section/section.iframe.html
    • /section/section.oembed.json

    Since I make heavy use of special sections, that was needed.

    The Template Files

    They actually all look pretty much the same.

    There’s the oembed JSON:

    {
      "version": "1.0",
      "provider_name": "{{ .Site.Title }}",
      "provider_url": "{{ .Site.BaseURL }}",
      "type": "rich",
      "title": "{{ .Title }} | {{ .Site.Title }}",
      "url": "{{ .Permalink }}",
      "author_name": "{{ if .Params.author }}{{ .Params.author }}{{ else }}Anonymous{{ end }}",
      "html": "<iframe src=\"{{ .Permalink }}iframe.html\" width=\"600\" height=\"200\" title=\"{{ .Title }}\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"hugo-embedded-content\"></iframe>"
    }
    
    

    And there’s the iframe HTML:

    <!DOCTYPE html>
    <html lang="en-US" class="no-js">
    <head>
    	<title>{{ .Title }} &middot; {{ .Site.Title }}</title>
    	<base target="_top" />
    	<style>
    		{{ partial "oembed.css" . | safeCSS }}
    	</style>
    	<meta name="robots" content="noindex, follow"/>
    	<link rel="canonical" href="{{ .Permalink }}" />
    </head>
    <body class="hugo hugo-embed-responsive">
    	<div class="hugo-embed">
    		<p class="hugo-embed-heading">
    			<a href="{{ .Permalink }}" target="_top">{{ .Title }}</a>
    		</p>
    		<div class="hugo-embed-excerpt">
    			{{ .Summary }}...
    		</div>
    		<div class="hugo-embed-footer">
    			<div class="hugo-embed-site-title">
    				<a href="{{ .Site.BaseURL }}" target="_top">
    					<img src="/images/oembed-icon.png" width="32" height="32" alt="{{ .Site.Title }}" class="hugo-embed-site-icon"/>
    					<span>{{ .Site.Title }}</span>
    				</a>
    			</div>
    		</div>
    	</div>
    </body>
    </html>
    
    

    Note: I set summaryLength: 10 in my config to limit the summary to something manageable. And no, you’re not mis-reading that, the library generally has no images.

    And then in my header code for the ‘normal’ html pages:

    	{{ if not .Params.notoembed }}
    	{{ "<!-- oEmbed -->" | safeHTML }}
    	<link rel="alternate" type="application/json+oembed" href="{{ .Permalink }}/oembed.json"/>
    	{{ end }}
    

    I wanted to leave a way to say certain pages were non embeddable, and while I’m not using it at the moment, the logic remains.

    Does it Float Work?

    Of course!

    Nice, quick, to the point.

  • oEmbedding Galleries

    oEmbedding Galleries

    I use NetPhotoGraphics to handle a 2.5 gig gallery, spanning back 20 or so years. The gallery used to be a home grown PHP script, then it was Gallery, then Gallery 2, then ZenPhoto, and now NetPhotoGraphics (which ostensibly is a fork of ZenPhoto, but diverged in a way I’m more supportive of).

    Anyway. I use this gallery in conjunction with a WordPress site. I’ll post news on WordPress and link to the gallery. But for years, to do that my choices were:

    1. make a text link
    2. make a photo which is a link
    3. copy all the thumbnails over and link each one

    Those all suck. Especially the third, since you can’t (out of the box) custom link images in a gallery in WordPress and frankly I don’t like any of the plugins.

    Once upon a time, I used a ZenPhoto plugin, but it’s been abandoned for years and stopped working a while ago. I needed something that had an elegant fallback (i.e. if you uninstall the plugin) and seriously thought about forking the WordPress plugin…

    But then I had a better idea.

    Why oEmbed?

    oEmbed is an industry standard. By having your app (Flickr, Twitter, your WordPress blog) offer a custom endpoint, someone can embed it easily into their own site! WordPress has supported many embeds for a long time, but as of 2015, it’s included oEmbed Discovery. That’s why you can paste in a link to Twitter, and WordPress will automagically embed it!

    I maybe wrote an oembed plugin for another CMS so I could embed things into WordPress… Because the other option was a MASSIVE complex WP Plugin and FFS why not?

    — ipstenu (Mika E.) (@Ipstenu) September 26, 2021

    (Note: I shut down my twitter account in November ‘22 when it was taken over by a narcissist who brought back abuse.)

    I just pasted the URL https://twitter.com/Ipstenu/status/1441950326777540609 in and WordPress automagically converts it to a pretty embed. About the only social media company you can’t do that with is Facebook, who requires you to make an app (I use Jetpack for it). Anyway, point being, this is also how tools like Slack or Discord know to embed your content when you paste in a link!

    By making an oEmbed endpoint, I allow my site to become more shareable and more engageble, which is a net positive for me. If I do it right, out of the box it’ll allow anyone with a WordPress site (i.e. me) to paste in a URL to my gallery and it looks pretty! Win win!

    The NetPhotoGraphics Plugin

    Now. I’m a terrible designer, so I literally copied the design WordPress itself uses for embeds and spun up a (relatively) fast solution: oEmbed for NetPhotoGraphics.

    The code is one file (oembed.php) which goes in the /plugins/ folder in your NetPhotoGraphics install. Then you activate the plugin and you’re done. There are only one thing to customize, the ‘gallery’ icon. By default it grabs a little NPG logo, but if you put a /images/oembed-icon.png image in your gallery, it’ll use that.

    And does it work? Here’s how the first version looked on a live page:

    An example of the m

    I wanted to limit the images since sometimes I have upwards of 200 (look, episodes of CSI are a thing for me). And frankly pasting in a URL to the gallery is a lot easier than drilling down on a list of a hundred albums. This is exactly what I needed.

    Since the creation of that, I worked with netPhotoGraphics and he helped me make it better.

    One Bug and a Future

    There’s room to grow here. Thanks to S. Billard, we’ve got a lot more flexible. You can override the basic design with your own theme, you can replace the icons, and there are even options to adjust the size of the iframes. Part of me thinks it could use a nicer design, maybe a single-photo Instagram style embed instead of what I have, but that’s not my forte. Also I have yet to get around to putting in ‘share’ options. (Pull Requests welcome!)

    And yes, I know the security isn’t ‘enough’ but I wasn’t able to get it to work how I wanted due to a weird bug. You see, I did run into a rare quirk with WordPress due to how I built out the site. IF you have your gallery in a subfolder under/beside a WordPress install AND you try to embed the gallery into that WordPress site, you MAY find out WP thinks your embed is WordPress and not NPG.

    In my case, I have:

    • example.com – WordPress
    • example.com/gallery – NetPhotoGraphics

    I guess WordPress reads a little too deep into who’s WP and who’s not, which resulted in me making this WordPress filter:

    add_filter( 'embed_oembed_html', 'npg_wrap_oembed_html', 99, 4 );
    }
    
    function npg_wrap_oembed_html( $cached_html, $url, $attr, $post_id ) {
    	if ( false !== strpos( $url, '://example.com/gallery' ) ) {
    		$cached_html = '&lt;div class="responsive-check">' . $cached_html . '&lt;/div>';
    
    		$cached_html = str_replace( 'wp-embedded-content', 'npg-embedded-content', $cached_html );
    		$cached_html = str_replace( 'sandbox="allow-scripts"', '', $cached_html );
    		$cached_html = str_replace( 'security="restricted"', '', $cached_html );
    
    	}
    	return $cached_html;
    }
    
    

    Change '://example.com/gallery' to the location of your own gallery install.

    No I don’t like this either, but it was a ‘get it done’ moment. Also this is why the iframe security is lacking.