-- software-microdata Soupault Plugin -- SPDX-FileCopyrightText: 2026 toastal -- SPDX-License-Identifier: MPL-2.0 -- -- Applies Schema.org software/install markup on exactly two pages: -- / → article#Article becomes mainEntity SoftwareApplication -- /install/ → article#Article becomes mainEntity HowTo (top-level -- sections become HowToSteps) -- -- Values live in Nickel (soupault.ncl custom_options.software); this plugin -- only applies them. It must run after website-microdata so main#Main -- already carries the WebPage scope. -- Plugin.require_version("5.0.0") software = soupault_config["custom_options"]["software"] if not Value.is_table(software) then Plugin.fail("custom_options.software is missing: cannot apply SoftwareApplication/HowTo microdata") end function software_str(key) local val = software[key] if not Value.is_string(val) or val == "" then Plugin.fail(format("custom_options.software.%s is missing or empty", key)) end return val end -- Cross-refs (install_url, about_*) are composed in Nickel (soupault.ncl -- custom_options.software); this plugin only applies them. 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 head_meta_content(name) local meta = HTML.select_one(page, format("head meta[name=\"%s\"]", name)) if HTML.is_element(meta) then return HTML.get_attribute(meta, "content") end return nil 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 author_stub(parent, name) local span = HTML.create_element("span") HTML.set_attribute(span, "itemprop", "author") HTML.set_attribute(span, "itemscope", "") HTML.set_attribute(span, "itemtype", "https://schema.org/Person") hidden_meta("name", name, span) HTML.append_child(parent, span) return span end function headline_element(main) local h1 = HTML.select_one(main, "h1[itemprop=\"headline\"]") if not HTML.is_element(h1) then h1 = HTML.select_one(main, "h1") end return h1 end function scope_article(itemtype) local 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 %s scope", page_file, itemtype)) end HTML.set_attribute(article, "itemscope", "") HTML.set_attribute(article, "itemprop", "mainEntity") HTML.set_attribute(article, "itemtype", itemtype) return article end function compensate_webpage_headline(main, text) -- Direct child of main ⇒ attaches to WebPage, not the article scope. local elem = HTML.create_element("meta") HTML.set_attribute(elem, "itemprop", "headline") HTML.set_attribute(elem, "content", text) HTML.prepend_child(main, elem) end path = url_path() if path == "/" 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 SoftwareApplication scope", page_file)) end article = scope_article("https://schema.org/SoftwareApplication") h1 = headline_element(main) if not HTML.is_element(h1) then Plugin.fail(format("No h1 found in “%s”, cannot name SoftwareApplication", page_file)) end h1_text = String.trim(HTML.strip_tags(h1)) -- headline is not valid on SoftwareApplication: re-type & compensate. HTML.set_attribute(h1, "itemprop", "name") compensate_webpage_headline(main, h1_text) description = head_meta_content("description") if Value.is_string(description) and description ~= "" then hidden_meta("description", description, article) end hidden_meta("applicationCategory", software_str("application_category"), article) hidden_meta("programmingLanguage", software_str("programming_language"), article) hidden_link("license", software_str("license_url"), article) hidden_meta("softwareVersion", software_str("version"), article) author_stub(article, software_str("author_name")) hidden_link("installUrl", software_str("install_url"), article) repos = software["code_repositories"] if Value.is_nil(repos) or Table.is_empty(repos) then Plugin.fail("custom_options.software.code_repositories is missing or empty") end local idx = 1 while repos[idx] do hidden_link("codeRepository", repos[idx], article) idx = idx + 1 end offer = HTML.create_element("div") HTML.set_attribute(offer, "itemprop", "offers") HTML.set_attribute(offer, "itemscope", "") HTML.set_attribute(offer, "itemtype", "https://schema.org/Offer") hidden_meta("price", software_str("offer_price"), offer) hidden_meta("priceCurrency", software_str("offer_currency"), offer) HTML.append_child(article, offer) features = HTML.select_one(page, "section#key-features ul") if HTML.is_element(features) then HTML.set_attribute(features, "itemprop", "featureList") else Log.warning(format("No key-features list found in “%s”, skipping featureList", page_file)) end elseif path == "/install" or path == "/install/" 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 HowTo scope", page_file)) end article = scope_article("https://schema.org/HowTo") h1 = headline_element(main) if not HTML.is_element(h1) then Plugin.fail(format("No h1 found in “%s”, cannot name HowTo", page_file)) end h1_text = String.trim(HTML.strip_tags(h1)) -- headline is valid on HowTo (a CreativeWork): keep it & add name. hidden_meta("name", h1_text, article) compensate_webpage_headline(main, h1_text) description = head_meta_content("description") if Value.is_string(description) and description ~= "" then hidden_meta("description", description, article) end about = HTML.create_element("div") HTML.set_attribute(about, "itemprop", "about") HTML.set_attribute(about, "itemscope", "") HTML.set_attribute(about, "itemtype", "https://schema.org/SoftwareApplication") hidden_meta("name", software_str("about_name"), about) hidden_link("url", software_str("about_url"), about) HTML.append_child(article, about) tools = software["install_tools"] if Value.is_nil(tools) or Table.is_empty(tools) then Plugin.fail("custom_options.software.install_tools is missing or empty") end local idx = 1 while tools[idx] do hidden_meta("tool", tools[idx], article) idx = idx + 1 end steps = HTML.select(article, "section#Content > section") if Table.is_empty(steps) then Log.warning(format("No HowTo steps found in “%s”", page_file)) end local idx = 1 while steps[idx] do local step = steps[idx] HTML.set_attribute(step, "itemscope", "") HTML.set_attribute(step, "itemprop", "step") HTML.set_attribute(step, "itemtype", "https://schema.org/HowToStep") local heading = HTML.select_one(step, "h2") if HTML.is_element(heading) then HTML.set_attribute(heading, "itemprop", "name") end idx = idx + 1 end end