This commit is contained in:
parent
faf881fa0c
commit
a8bc400371
3 changed files with 128 additions and 7 deletions
|
|
@ -446,6 +446,57 @@ export RenderedPage RenderPosts(std::span<const Post> posts) {
|
|||
return page;
|
||||
}
|
||||
|
||||
// The post's video, as a schema.org VideoObject hung off the BlogPosting.
|
||||
//
|
||||
// Google already indexed and carouselled these videos on the strength of the
|
||||
// bare <video> element; this makes the placement deliberate rather than lucky,
|
||||
// and a typed thumbnailUrl is what earns the still on the text result too.
|
||||
//
|
||||
// thumbnailUrl and uploadDate are what the video rich result requires, so a
|
||||
// video whose mirror produced no poster is skipped rather than emitted
|
||||
// incomplete — the same both-or-neither rule Dimensions() applies to width and
|
||||
// height. Only our own mirrored paths qualify, for the reason ogImage below
|
||||
// gives: an absolute src or a third-party poster is the off-site fetch the
|
||||
// mirror exists to avoid, just performed by a crawler.
|
||||
//
|
||||
// Headline media only. That IS the post — these posts are their recording —
|
||||
// and it is the one video a search result should be pointing at.
|
||||
//
|
||||
// No encodingFormat and no duration: neither is required, the mirror records no
|
||||
// duration, and naming a container guessed from a file extension would assert
|
||||
// something we do not know. A wrong type is worse than a missing one, for the
|
||||
// same reason MimeFor() refuses to guess.
|
||||
std::string PostVideoLd(const Post& p) {
|
||||
std::vector<std::string> videos;
|
||||
for (const PostMedia& m : p.media) {
|
||||
if (m.kind != "video") continue;
|
||||
if (!m.src.starts_with("/") || !m.poster.starts_with("/")) continue;
|
||||
|
||||
std::string node = std::format(
|
||||
R"({{"@type":"VideoObject","name":{},"uploadDate":{},)"
|
||||
R"("thumbnailUrl":{},"contentUrl":{})",
|
||||
JsonStr(p.title), JsonStr(p.published),
|
||||
JsonStr("https://catcrafts.net" + m.poster),
|
||||
JsonStr("https://catcrafts.net" + m.src));
|
||||
if (!p.excerpt.empty()) node += ",\"description\":" + JsonStr(p.excerpt);
|
||||
if (m.width > 0 && m.height > 0) {
|
||||
node += std::format(R"(,"width":{},"height":{})", m.width, m.height);
|
||||
}
|
||||
node += "}";
|
||||
videos.push_back(std::move(node));
|
||||
}
|
||||
if (videos.empty()) return {};
|
||||
if (videos.size() == 1) return ",\"video\":" + videos.front();
|
||||
|
||||
std::string out = ",\"video\":[";
|
||||
for (std::size_t i = 0; i < videos.size(); ++i) {
|
||||
if (i) out += ',';
|
||||
out += videos[i];
|
||||
}
|
||||
out += ']';
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── one post, in full ─────────────────────────────────────────────────
|
||||
|
||||
// The whole point of hosting the body: a page a search engine can index, a
|
||||
|
|
@ -494,14 +545,15 @@ export RenderedPage RenderPost(const Post& p) {
|
|||
R"("@type":"Person","name":"Jorijn van der Graaf"}},)"
|
||||
R"("publisher":{{"@id":"https://catcrafts.net/#organization",)"
|
||||
R"("@type":"Organization","name":"Catcrafts"}},)"
|
||||
R"("discussionUrl":{}{}{}}})",
|
||||
R"("discussionUrl":{}{}{}{}}})",
|
||||
JsonStr(p.title), JsonStr(p.published),
|
||||
JsonStr("https://catcrafts.net" + canonical),
|
||||
JsonStr("https://catcrafts.net" + canonical),
|
||||
JsonStr(p.permalink),
|
||||
p.excerpt.empty() ? std::string{} : ",\"description\":" + JsonStr(p.excerpt),
|
||||
ogImage.empty() ? std::string{}
|
||||
: ",\"image\":" + JsonStr("https://catcrafts.net" + ogImage));
|
||||
: ",\"image\":" + JsonStr("https://catcrafts.net" + ogImage),
|
||||
PostVideoLd(p));
|
||||
|
||||
page.main = Format(
|
||||
R"(<article class="post">)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue