# \[Space-Lua\] Open-Graph widget

**URL:** https://community.silverbullet.md/t/space-lua-open-graph-widget/2095
**Category:** Tricks & Techniques
**Created:** [April 1, 2025, 12:30pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095 "2025-04-01T12:30:15Z")
**Posts on this page:** 8
**Page:** 1

<div class="post-metadata">

### Author: ![22rw](https://community.silverbullet.md/user_avatar/community.silverbullet.md/22rw/32/659_2.png) [@22rw](https://community.silverbullet.md/u/22rw)
#### Post date: [April 1, 2025, 12:30pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/1 "2025-04-01T12:30:15Z")

</div>

To prepare myself for the transition to lua and practice some syntax along the way (and maybe out of boredom), I recently decided to implement a simple link-preview widget using the Open Graph protocol properties found in many websites.

My code consists of two functions:  
`og.fetch`, wich extracts tags and their properties from a website  
and `og.card`, wich uses the data returned by `og.fetch` to display an info card in plain html.

Some examples:

 ![{26C2135E-FB53-4136-A8CE-C6D0B2DCD8B3}](https://community.silverbullet.md/uploads/default/original/2X/8/8cb690b27b0c629b08566f6994e5b9615413a0f6.png)  
`${og.card("https://xkcd.com/1272/", 300)}`  
  

 ![{41A14576-1C04-4113-A468-2F06DAA99B76}](https://community.silverbullet.md/uploads/default/original/2X/d/d8bce9ef061e118b400e9d6b5e3403d062f0c07c.png)  
`${og.card("https://medium.com/@TeddyYeung/what-is-open-graph-f3abadd7d1f9", 300)}`  
  

 ![{4B875353-2ED9-4B51-B4C7-95DD1EFD334E}](https://community.silverbullet.md/uploads/default/original/2X/e/e21d914d7b826b60006f2114d3a0eaf1ce92a6f7.png)  
`${og.card("https://www.youtube.com/watch?v=0FBxnKxxMhU", 300)}`  
  

This is my first time writing lua and I’m sure there’s lots of room for improvement, but at least it’s self-contained and works (so far xD).

space-lua:

```lua
og = {}

function og.fetch(url) 
  local function extract_prop(header, prop_name)
    local prop_start, prop_end = string.find(header, 'og:'..prop_name..'" content="[^"]+"/?>')
    local prop = string.sub(header, prop_start, prop_end)
    local tag_closer_length = 0
    if string.find(prop, "/>") == nil then
      -- <tag prop="value">
      tag_closer_length = 2
    else
      -- <tag prop="value"/>
      tag_closer_length = 3
    end
    return string.sub(prop, 15 + string.len(prop_name), prop_end - prop_start - tag_closer_length)
  end
  
  local response = http.request(url)
  if response.ok then
    local body = response.body
    local head_start, _ = string.find(body, "<head>")
    local head_end, _ = string.find(body, "</head>")
    local header = string.sub(body, head_start, head_end)

    local title = extract_prop(header, "title")
    local url = extract_prop(header, "url")
    local image = extract_prop(header, "image")
    -- description and site_name are optional properties, might return nil/""
    local description = extract_prop(header, "description")
    local sitename = extract_prop(header, "site_name")
    
    return {title = title, url = url, image = image, description = description, sitename = sitename}
  else
    return "Error: got status code "..response.status
  end
end

function og.card(url, width)
  local og_data = og.fetch(url)
  return widget.new {
    html = "<div style='background-color:rgba(0,0,0,0.15);padding:10px;border-radius:5px;display:flex;flex-direction:column;align-items:space-evenly;width:"..width.."px;'><span class='sb-sub'>"..og_data.sitename.."</span><a href = '"..og_data.url.."'><b>"..og_data.title.."</b></a>"..og_data.description.."<img src='"..og_data.image.."' style='margin-top:10px;'/></div>"
  }
end

```

Open for any feedback! 😃

---

<div class="post-metadata">

### Author: ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)
#### Post date: [April 1, 2025, 2:58pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/2 "2025-04-01T14:58:33Z")

</div>

Nice, thanks for sharing. It’s good to see what people are doing with this. Part of the Lua (and SilverBullet) fun is that it’s perfectly fine to hack things so that they work for you, without overthinking too much about how to “properly” engineer. It’s ok, chill 😆

That said, I am wondering whether parsing HTML is going to be a common use case. If so, there’s probably ways (already) that can expose some of (browser) JavaScript native DOM handling and querying to Space Lua as well to make this nicer.

---

<div class="post-metadata">

### Author: ![stag](https://community.silverbullet.md/user_avatar/community.silverbullet.md/stag/32/979_2.png) [@stag](https://community.silverbullet.md/u/stag)
#### Post date: [April 1, 2025, 3:13pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/3 "2025-04-01T15:13:20Z")

</div>

i like how you did the styling, feels very natural

---

<div class="post-metadata">

### Author: ![22rw](https://community.silverbullet.md/user_avatar/community.silverbullet.md/22rw/32/659_2.png) [@22rw](https://community.silverbullet.md/u/22rw)
#### Post date: [April 1, 2025, 3:54pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/4 "2025-04-01T15:54:58Z")

</div>

I’ll keep that in mind ;D

> HTML parsing

My first thought was to just use the [DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/DOMParser) class with a call to `js.new`, but I ended up implementing it in 100% Lua just to see how much more work that would be.  
Looking at the code now, I feel like the effort isn’t immensly higher than using native DOM parsing (though that probably depends on the application), but having a lua api available would certainly improve parsing stability & scalability a lot 👍

Edit: just realized, that using browser-native apis with js.new is not really possible (please correct me if I’m wrong), but using something like [dom-parser](https://www.npmjs.com/package/dom-parser) with js.import would probably work as well.

---

<div class="post-metadata">

### Author: ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)
#### Post date: [April 1, 2025, 4:27pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/5 "2025-04-01T16:27:25Z")

</div>

> [@22rw](#):
>
> realized, that using browser-native apis with js.new is not really possible (please correct me if I’m wrong), but using something like [dom-parser](https://www.npmjs.com/package/dom-parser) with js.import would probably work as well.

In #v2 you actually can:

```lua
local boldNode = js.new(js.window.DOMParser).parseFromString("<b>Bold</b>", "text/html")

```

---

<div class="post-metadata">

### Author: ![fiddler](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/f/e8c25b/32.png) [@fiddler](https://community.silverbullet.md/u/fiddler)
#### Post date: [August 26, 2025, 1:30pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/6 "2025-08-26T13:30:43Z")

</div>

Maybe script is outdated,  
it is showing me return output of og.card in query table format

 ![Screenshot_20250826-185923_Cromite](https://community.silverbullet.md/uploads/default/original/2X/c/c5fbbc49663b32a088eaa5b5a5f3c04765cb177d.png)

---

<div class="post-metadata">

### Author: ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)
#### Post date: [August 26, 2025, 3:36pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/7 "2025-08-26T15:36:31Z")

</div>

Yes, it should use `widget.new` now, just updated the code. Hope that fixes it.

---

<div class="post-metadata">

### Author: ![22rw](https://community.silverbullet.md/user_avatar/community.silverbullet.md/22rw/32/659_2.png) [@22rw](https://community.silverbullet.md/u/22rw)
#### Post date: [August 26, 2025, 3:56pm UTC](https://community.silverbullet.md/t/space-lua-open-graph-widget/2095/8 "2025-08-26T15:56:20Z")

</div>

`widget.new` should do the trick, I think I wrote this while still on v1, where the api was never updated to this syntax.  
I even wrote an improved version some time ago that makes use of `js.window.DOMParser`, making the whole thing a lot faster and more reliable. I don’t know why I didn’t share the code back then, but for anyone still looking to use this widget in v2, here’s my new version:

```lua
og = {}

function og.fetch(url) 
  local response = http.request(url)
  if response.ok then
    local parser = js.new(js.window.DOMParser)
    local document = parser.parseFromString(response.body, "text/html")
    local og_properties = {}
    for meta in each(document.getElementsByTagName("meta")) do
      local prop = tostring(meta.getAttribute("property"))
      if prop:startsWith("og:") then
        og_properties[prop:sub(4)] = meta.getAttribute("content")
      end
    end
    return og_properties
  else
    return { error = "Status code "..response.status }
  end
end

function og.card(url, width)
  width = width or 350
  local og = og.fetch(url)
  if og.error then return widget.markdown("==OG Error: "..og.error.."==") end
  return widget.html(dom.div {
    style = "width:"..width.."px;background-color:rgba(0,0,0,0.15);padding:3px 10px;border-radius:5px;",
    dom.sub { og.site or og.site_name }, dom.br {}, 
    dom.h3 { dom.a { href = og.url, og.title } },
    dom.p { og.description or "" },
    dom.img {
      style = "width:"..width.."px;border-radius:5px;",
      src = og.image
    }
  })
end

```

# [Gist](https://gist.github.com/22rw/95bf685c00fe3d92a5e116f2614eada8)
