class

Hwaro::Content::Seo::Amp

Inherits Reference < Object

Constants

AMP_CSS_AT_RULES = {"font-face", "keyframes", "-webkit-keyframes", "-moz-keyframes", "-o-keyframes", "-ms-keyframes", "media", "supports", "page"}

At-rules AMP accepts inside <style amp-custom>. Anything else (@view-transition, @starting-style, @layer, @import, …) is a CSS syntax error that fails the whole page.

AMP_DISALLOWED_ATTR_RE = /\s+(?:loading|decoding|fetchpriority)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+)/i

Attributes that are legal on the HTML element but not on the AMP element it becomes, so they have to be dropped during conversion rather than carried through with the rest of the attribute string.

loading is the one hwaro emits itself: the built-in youtube / vimeo / codepen / figure shortcodes hard-code loading="lazy", the image pipeline adds it, and [markdown] lazy_loading adds it to every content image. None of amp-img, amp-iframe or amp-video accepts it — AMP does its own lazy loading — so every AMP page carrying an image or an embed failed validation on a DISALLOWED_ATTR the author never wrote. Scoped to the converted tag's attribute string (not a document-wide gsub) so a loading= appearing inside <style amp-custom> or JSON-LD text is untouched. decoding and fetchpriority are the image hints themes write on hero/card images; amp-img rejects both the same way.

AMP_END_TAG_TAIL = "(?:[\\s\\/](?:[^<>=]++|=\\s*+\"[^\"<]*+\"|=\\s*+'[^'<]*+'|=)*+)?>"

What follows an end tag's name, through its >, in any form (</iframe >, </iframe x="a>b">): an exact one ran on to the next element's. A quote opens a value only after =, as in a start tag. Neither runs past a <: every </iframe before an unclosed tail rescanned to the end of the page (quadratic).

AMP_FONT_PROVIDERS = ["cloud.typography.com", "fast.fonts.net", "fonts.googleapis.com", "use.typekit.net", "maxcdn.bootstrapcdn.com", "use.fontawesome.com", "fonts.bunny.net"] of ::String

Font providers AMP allows via <link rel="stylesheet">. Every other external stylesheet is disallowed and is stripped during conversion.

AMP_IFRAME_RE = /<iframe(#{Utils::TextUtils::HTML_TAG_ATTRS_QUOTED_VALUES})>(.*?)<\/iframe#{AMP_END_TAG_TAIL}/im
AMP_LINK_TAG_RE = /<link\b#{Utils::TextUtils::HTML_TAG_ATTRS_QUOTED_VALUES}>/i

Quote-aware tag scans: media="(width >= 600px)" or title="a > b" does not end the tag.

AMP_SCRIPT_RE = /<script(?=[\s\/>])(?![^>]*type=["']application\/ld\+json["'])(?![^>]*src=["']https:\/\/cdn\.ampproject\.org)[^>]*>[\s\S]*?<\/script#{AMP_END_TAG_TAIL}/im
AMP_VIDEO_RE = /<video(#{Utils::TextUtils::HTML_TAG_ATTRS_QUOTED_VALUES})>(.*?)<\/video#{AMP_END_TAG_TAIL}/im

Class methods

convert_to_amp(html : String, page : Models::Page, config : Models::Config) : String

Convert standard HTML to AMP-compliant HTML

Source
effective_prefix(config : Models::Config, warn : Bool = false) : String | Nil

The path_prefix AMP will actually publish under, or nil when it must not publish at all. Both refusals below would otherwise let an AMP variant overwrite the very page it was converted from:

  • a "."/".." prefix is not empty, so the emptiness guard never fires for it, yet File.join resolves it straight back to the canonical path — with a rel="amphtml" pointing at itself. Refuse outright, the way every other emitter refuses a traversing output path, rather than reinterpreting the author's intent. The prefix itself is left verbatim (it is also a URL segment, so a literal percent-encoded directory name must survive unchanged); only the decision to proceed is taken on the traversal-safe reading of it;
  • a blank/slash-only prefix makes mirror_path collapse onto the canonical path (File.join drops empty components).

Public because the stale-output prune has to resolve the same prefix to find the mirror of a page that no longer exists; warn: keeps the log lines on the generating call only.

Source
generate(pages : Array(Models::Page), config : Models::Config, output_dir : String, verbose : Bool = false, builder : Core::Build::Builder | Nil = nil)

Generate AMP versions of rendered pages. Reads the already-rendered HTML files, converts to AMP-compliant HTML, and writes them under the configured path prefix.

Source
mirror_output_for(page : Models::Page, config : Models::Config, output_dir : String) : String | Nil

The mirror generate writes for page, or nil when it writes none (AMP off, section not enabled, draft/generated/unrendered/redirect page, or a refused prefix). Same gates as generate, so the builder can count the mirror among the files a page owns — and delete it with the page.

Source
mirror_path(canonical_output : String, output_dir : String, prefix : String) : String

The AMP mirror of a canonical output file: the same path with prefix inserted directly after the output directory. The one spelling of the AMP layout — generate mirrors each page through it, and the Finalize phase's stale-output prune mirrors the recorded output path of a page that is gone, so the two cannot disagree about where a mirror lives. Both sides are expanded first: a page's recorded output path is absolute (OutputGuard resolves it) while output_dir is whatever the caller passed, usually the relative "public" — and relative_to across those two spellings raises instead of matching, which silently left every AMP mirror behind. The result is rejoined onto the caller's own spelling so it stays comparable with the paths around it.

Source
same_origin_src?(src : String, base_url : String) : Bool

True when src resolves to the same origin as the AMP document.

Unknown means same-origin. An empty or unreadable src is the one case we must not guess cross-origin on: guessing wrong there hands allow-same-origin to a same-origin frame, which is precisely the AMP violation this function exists to prevent. Guessing the other way only drops a privilege.

Source
sandbox_for(src : String, base_url : String) : String

Default sandbox for a converted iframe.

AMP: "An amp-iframe must not be in the same origin as the container unless they do not allow allow-same-origin in the sandbox attribute." Third-party embeds (YouTube, CodePen, Gist) are cross-origin and need allow-same-origin to function, but adding it unconditionally makes any iframe pointing back at the site itself fail AMP validation outright.

Source