-- Ported from anyfeed-server's overwatch-patch-notes.lua. That version
-- rendered a patch's markup to markdown via a shared converter; luafeed
-- has no HTML->markdown capability, so `description` here is the patch
-- block's plain text instead (loses bold/list formatting, keeps the
-- actual words) via `:text()`. Everything else -- title, link, date, and
-- crucially no second fetch needed at all, since every patch is already
-- inline on this one page -- ports directly.
--
-- site{
-- name = "overwatch-patch-notes",
-- url = "https://overwatch.blizzard.com/en-us/news/patch-notes/",
-- method = "overwatch-patch-notes",
-- title = "Overwatch Patch Notes",
-- }
--
-- Markup per patch (as of writing):
--
--
--
--
Overwatch Retail Patch Notes - August 20, 2026
-- ...section content...
--
--
-- The page only ever renders the current month's patches server-side --
-- the year/month selectors re-fetch via client-side JS, which a plain
-- fetch doesn't run -- so this only ever sees recent entries. That's the
-- right behavior for a feed tracker: older patches were already seen.
local function has_class(node, name)
local class = node:attr("class")
if not class then
return false
end
for token in class:gmatch("%S+") do
if token == name then
return true
end
end
return false
end
local function find_by_class(node, tag, class_name)
local matches = {}
for _, el in ipairs(node:find_all(tag)) do
if has_class(el, class_name) then
table.insert(matches, el)
end
end
return matches
end
-- Strips any query string/fragment so `#patch-...` can be appended cleanly,
-- even if the configured url already carries one.
local function page_url_of(url)
return url:match("^([^?#]+)")
end
-- os.time() interprets a field table as the host's *local* time, and Lua
-- has no timezone-aware alternative in the standard library -- this
-- converts a UTC field table to the right epoch by computing the host's
-- current UTC offset and applying it, rather than assuming the host runs
-- in UTC. (Not exact across a DST transition between now and the target
-- date, an acceptable approximation for ordering feed items.)
local function utc_time(fields)
local now = os.time()
local isdst = os.date("*t", now).isdst
local utc_now = os.date("!*t", now)
utc_now.isdst = isdst
local offset = now - os.time(utc_now)
fields.isdst = isdst
return os.time(fields) + offset
end
local function rfc822_midnight(date_part)
local year, month, day = date_part:match("^(%d%d%d%d)-(%d%d)-(%d%d)$")
if not year then
return nil
end
local timestamp = utc_time({ year = tonumber(year), month = tonumber(month), day = tonumber(day), hour = 0 })
return os.date("!%a, %d %b %Y %H:%M:%S GMT", timestamp)
end
local function build_item(page_url, patch)
local anchor_el = find_by_class(patch, "div", "anchor")[1]
local title_el = find_by_class(patch, "h3", "PatchNotes-patchTitle")[1]
if not anchor_el or not title_el then
return nil
end
local id = anchor_el:attr("id")
local date_part = id and id:match("^patch%-(%d%d%d%d%-%d%d%-%d%d)$")
return {
title = title_el:text(),
link = page_url .. "#" .. id,
guid = id,
description = patch:text(),
pub_date = date_part and rfc822_midnight(date_part),
}
end
local root, url = ...
local page_url = page_url_of(url)
local patches = find_by_class(root, "div", "PatchNotes-patch")
local items = {}
for _, patch in ipairs(patches) do
local item = build_item(page_url, patch)
if item then
table.insert(items, item)
end
end
return items