-- media-microdata Soupault Plugin -- SPDX-FileCopyrightText: 2026 toastal -- SPDX-License-Identifier: MPL-2.0 -- -- Applies Schema.org media markup on /media/ only. The page holds one -- section per medium (audio-casts/talks/blogs), each with h3 item -- subsections carrying flat field lists: -- article#Article → mainEntity ItemList -- h3 item section → itemListElement + AudioObject/VideoObject/Article -- (medium → type from custom_options.media_types) -- h3 → name -- Date dd → datePublished -- Host dd → publisher Organization stub -- Guest/Speaker dd → actor (on the peers-plugin Person spans) -- Author dd → author (ditto) -- Listen/Watch/Read dd → url (copied link href) Plugin.require_version("5.0.0") function url_path() local url = page_url url = Regex.replace(url, "^[a-z]+://[^/]+", "") if url == "" then url = "/" elseif not String.starts_with(url, "/") then url = "/" .. url end return url end function hidden_meta(itemprop, content, parent) local elem = HTML.create_element("meta") HTML.set_attribute(elem, "itemprop", itemprop) HTML.set_attribute(elem, "content", content) HTML.append_child(parent, elem) return elem end function hidden_link(itemprop, href, parent) local elem = HTML.create_element("link") HTML.set_attribute(elem, "itemprop", itemprop) HTML.set_attribute(elem, "href", href) HTML.append_child(parent, elem) return elem end function field_text(dd) return String.trim(HTML.strip_tags(dd)) end path = url_path() if path == "/media" or path == "/media/" then main = HTML.select_one(page, "main#Main") if not HTML.is_element(main) then Plugin.fail(format("No main#Main found in “%s”, skipping media markup", page_file)) end article = HTML.select_one(page, "article#Article") if not HTML.is_element(article) then Plugin.fail(format("No article#Article found in “%s”, cannot apply ItemList scope", page_file)) end HTML.set_attribute(article, "itemscope", "") HTML.set_attribute(article, "itemprop", "mainEntity") HTML.set_attribute(article, "itemtype", "https://schema.org/ItemList") h1 = HTML.select_one(main, "h1[itemprop=\"headline\"]") if not HTML.is_element(h1) then h1 = HTML.select_one(main, "h1") end if HTML.is_element(h1) then HTML.set_attribute(h1, "itemprop", "name") headline = HTML.create_element("meta") HTML.set_attribute(headline, "itemprop", "headline") HTML.set_attribute(headline, "content", String.trim(HTML.strip_tags(h1))) HTML.prepend_child(main, headline) else Log.warning(format("No h1 found in “%s”, skipping WebPage.headline compensation", page_file)) end types = soupault_config["custom_options"]["media_types"] if not Value.is_table(types) then Plugin.fail("custom_options.media_types is missing: cannot type media items") end mediums = HTML.select(article, "section#Content > section") if Table.is_empty(mediums) then Log.warning(format("No media medium sections found in “%s”", page_file)) end local idx = 1 while mediums[idx] do local medium = mediums[idx] local medium_id = HTML.get_attribute(medium, "id") local typename = nil if Value.is_string(medium_id) and medium_id ~= "" then typename = types[medium_id] end if not Value.is_string(typename) or typename == "" then Log.warning(format("Unknown media medium “%s”, skipping its items", Value.repr(medium_id))) else local itemtype = "https://schema.org/" .. typename local items = HTML.select(medium, "section") if Table.is_empty(items) then Log.warning(format("No media items found in medium “%s”", Value.repr(medium_id))) end local jdx = 1 while items[jdx] do local item = items[jdx] local heading = HTML.select_one(item, "h3") if not HTML.is_element(heading) then Log.warning("Media item without h3, skipping it") else HTML.set_attribute(item, "itemscope", "") HTML.set_attribute(item, "itemprop", "itemListElement") HTML.set_attribute(item, "itemtype", itemtype) HTML.set_attribute(heading, "itemprop", "name headline") local dts = HTML.select(item, "dl.field-list dt") local dds = HTML.select(item, "dl.field-list dd") local kdx = 1 while dts[kdx] do local key = field_text(dts[kdx]) local dd = dds[kdx] if Value.is_nil(dd) or not HTML.is_element(dd) then Log.warning(format("Media field “%s” without value, skipping it", key)) elseif key == "Date:" then hidden_meta("datePublished", field_text(dd), item) elseif key == "Host:" then local org = HTML.create_element("span") HTML.set_attribute(org, "itemprop", "publisher") HTML.set_attribute(org, "itemscope", "") HTML.set_attribute(org, "itemtype", "https://schema.org/Organization") hidden_meta("name", field_text(dd), org) HTML.append_child(item, org) elseif key == "Guest:" or key == "Speaker:" then local persons = HTML.select(dd, "span.peer") if Table.is_empty(persons) then Log.warning(format("Media field “%s” without person, skipping actor", key)) end local pdx = 1 while persons[pdx] do HTML.set_attribute(persons[pdx], "itemprop", "actor") pdx = pdx + 1 end elseif key == "Author:" or key == "Writer:" then local persons = HTML.select(dd, "span.peer") if Table.is_empty(persons) then Log.warning(format("Media field “%s” without person, skipping author", key)) end local pdx = 1 while persons[pdx] do HTML.set_attribute(persons[pdx], "itemprop", "author") pdx = pdx + 1 end elseif key == "Listen:" or key == "Watch:" or key == "Read:" then local link = HTML.select_one(dd, "a[href]") if HTML.is_element(link) then hidden_link("url", HTML.get_attribute(link, "href"), item) else Log.warning(format("Media field “%s” without link, skipping url", key)) end else Log.warning(format("Unrecognized media field “%s”, skipping it", key)) end kdx = kdx + 1 end end jdx = jdx + 1 end end idx = idx + 1 end end