This commit is contained in:
parent
2fa6e70af1
commit
6841623e23
17 changed files with 2306 additions and 148 deletions
126
tools/e2e.sh
126
tools/e2e.sh
|
|
@ -140,6 +140,10 @@ status /legal/nope 404
|
|||
# A slug that cannot be one of ours is rejected before any lookup.
|
||||
status /shop/BAD--slug 404
|
||||
status /demos/nope 404
|
||||
# A post slug that parsed but names nothing must be a real 404, or every typo
|
||||
# and every retired post becomes an indexable empty page.
|
||||
status /posts/nope 404
|
||||
status /posts/BAD--slug 404
|
||||
# The retired blog URLs are still in the wild; they must redirect, not 404.
|
||||
status /blog 301
|
||||
status /blog/hello-world 301
|
||||
|
|
@ -367,7 +371,7 @@ else
|
|||
fi
|
||||
# `poster` is in the list because a video poster is fetched on page load exactly
|
||||
# like an <img> src is, so a third-party poster leaks the same visitor IP.
|
||||
if curl -s "$BASE/posts" | grep -qE '(src|href|poster)="https?://[^"]*\.(mp4|webm|webp|png|jpe?g|gif)'; then
|
||||
if curl -s "$BASE/posts" | grep -qE '(src|srcset|href|poster)="https?://[^"]*\.(mp4|webm|webp|avif|png|jpe?g|gif)'; then
|
||||
bad "/posts media origin" "media loaded from a third party"
|
||||
else
|
||||
ok "/posts loads no media from a third party"
|
||||
|
|
@ -417,6 +421,126 @@ else
|
|||
bad "video preload" "expected preload=\"metadata\""
|
||||
fi
|
||||
|
||||
echo "== post pages =="
|
||||
# The post page is where the body lives, and the body is the reason the site
|
||||
# has anything for a search engine to index beyond a list of links off it. Its
|
||||
# slug is data, so take one from the page rather than hardcoding a title that
|
||||
# will be wrong the week after it is written.
|
||||
POST_PATH=$(curl -s "$BASE/posts" | grep -oE 'href="/posts/[a-z0-9-]+"' \
|
||||
| head -n1 | sed 's/href="//; s/"$//')
|
||||
if [ -z "$POST_PATH" ]; then
|
||||
bad "post pages" "/posts links no post page; nothing carries a body"
|
||||
else
|
||||
ok "/posts links a post page ($POST_PATH)"
|
||||
status "$POST_PATH" 200
|
||||
body_has /posts 'Read the full post' "/posts offers the full post"
|
||||
# And it trails the excerpt, immediately after the ellipsis the truncation
|
||||
# left, rather than sitting as its own row below the media. The excerpt is
|
||||
# escaped text, so nothing but the link can put a '<' between the two.
|
||||
if curl -s "$BASE/posts" \
|
||||
| grep -qE '<p class="post-card__excerpt">[^<]*<a class="link-more" href="/posts/'; then
|
||||
ok "read-more trails the excerpt"
|
||||
else
|
||||
bad "read-more placement" "not inside the excerpt paragraph"
|
||||
fi
|
||||
body_has "$POST_PATH" '<div class="post-body">' "post page carries the rendered body"
|
||||
# Rendered, not dumped: a body that reached the page as literal Markdown
|
||||
# would show its own asterisks and hashes to the reader and to a crawler.
|
||||
if curl -s "$BASE$POST_PATH" | grep -qE '<(p|h2|h3|h4|ul|ol|blockquote|pre)>'; then
|
||||
ok "post body is real markup, not literal Markdown"
|
||||
else
|
||||
bad "post body markup" "no block elements found in the body"
|
||||
fi
|
||||
# The canonical points here, not at the instance. That is the entire SEO
|
||||
# argument for hosting the body: two copies of the text exist, and this
|
||||
# says which one is the original as far as this site is concerned.
|
||||
body_has "$POST_PATH" 'rel="canonical" href="https://catcrafts.net/posts/' \
|
||||
"post page is its own canonical"
|
||||
body_has "$POST_PATH" '"@type":"BlogPosting"' "post page carries BlogPosting JSON-LD"
|
||||
body_has "$POST_PATH" '"@id":"https://catcrafts.net/#organization"' \
|
||||
"post JSON-LD joins the organization node"
|
||||
body_has "$POST_PATH" '"@id":"https://catcrafts.net/about#person"' \
|
||||
"post JSON-LD joins the founder node"
|
||||
body_has "$POST_PATH" 'property="og:type" content="article"' "post page is an article to og:"
|
||||
# Hosting the body does not mirror the discussion; the thread is still one
|
||||
# click away and is still where the comments are.
|
||||
if curl -s "$BASE$POST_PATH" | grep -qE 'href="https://[a-z0-9.-]+/post/[0-9]+"'; then
|
||||
ok "post page still links its thread"
|
||||
else
|
||||
bad "post thread link" "no https://<instance>/post/<id> link on the post page"
|
||||
fi
|
||||
# The body is prose, not an application. Same rule as /projects.
|
||||
body_lacks "$POST_PATH" '<script>' "post page ships no executable script"
|
||||
body_lacks "$POST_PATH" '<base' "post page has no base tag"
|
||||
# Every inline image and video the body embeds is mirrored, exactly like a
|
||||
# card's media — the privacy notice's "everything comes from catcrafts.net"
|
||||
# covers href as well as src, so a body linking a .webp on someone else's
|
||||
# instance is the same leak as embedding one.
|
||||
for pg in $(curl -s "$BASE/sitemap.xml" \
|
||||
| grep -oE '/posts/[a-z0-9-]+' | head -n 20); do
|
||||
if curl -s "$BASE$pg" \
|
||||
| grep -qE '(src|srcset|href|poster)="https?://[^"]*\.(mp4|webm|webp|avif|png|jpe?g|gif)'; then
|
||||
bad "$pg media origin" "body media loaded from a third party"
|
||||
else
|
||||
ok "$pg loads no media from a third party"
|
||||
fi
|
||||
done
|
||||
# The image format ladder: AVIF first, the mirrored original next, and a
|
||||
# PNG on the <img> underneath, so exactly one file is fetched and every
|
||||
# browser can read one of them. Order is the whole point — a browser takes
|
||||
# the first source it understands — so assert the sequence, not just that
|
||||
# the pieces are present.
|
||||
if curl -s "$BASE$POST_PATH" | grep -q '<picture>'; then
|
||||
if curl -s "$BASE$POST_PATH" \
|
||||
| grep -qE '<picture><source srcset="/media/[^"]+\.avif" type="image/avif">'; then
|
||||
ok "inline images lead with an AVIF source"
|
||||
else
|
||||
bad "image ladder" "the first source is not the AVIF"
|
||||
fi
|
||||
if curl -s "$BASE$POST_PATH" \
|
||||
| grep -qE '<img class="post-media__item"[^>]*src="/media/[^"]+\.png"'; then
|
||||
ok "inline images fall back to a PNG the img itself points at"
|
||||
else
|
||||
bad "image fallback" "the <img> base is not a PNG"
|
||||
fi
|
||||
# Every tier has to be a file that exists, or the ladder serves a 404 to
|
||||
# whichever browsers pick that rung — which is precisely the set of
|
||||
# browsers nobody testing this site is using.
|
||||
missing=0
|
||||
for f in $(curl -s "$BASE/sitemap.xml" | grep -oE '/posts/[a-z0-9-]+' | head -n 20 \
|
||||
| while read -r pg; do curl -s "$BASE$pg"; done \
|
||||
| grep -oE '(src|srcset)="/media/[^"]+"' \
|
||||
| sed 's/.*="//; s/"$//' | sort -u); do
|
||||
[ -f "media${f#/media}" ] || { missing=$((missing + 1)); echo " missing: $f" >&2; }
|
||||
done
|
||||
if [ "$missing" -eq 0 ]; then
|
||||
ok "every referenced media file is on the mount"
|
||||
else
|
||||
bad "media files" "$missing referenced file(s) not on the mount"
|
||||
fi
|
||||
else
|
||||
skip "image format ladder" "no <picture> on this page — ffmpeg absent at mirror time?"
|
||||
fi
|
||||
# Inline screenshots get dimensions from the sidecar list fetch-media.sh
|
||||
# writes, because Markdown syntax has nowhere to carry them — without it
|
||||
# the prose below every image jumps as the file arrives. Conditional: a
|
||||
# post whose body embeds nothing has nothing to check.
|
||||
if curl -s "$BASE$POST_PATH" | grep -q '<img class="post-media__item"'; then
|
||||
if curl -s "$BASE$POST_PATH" \
|
||||
| grep -qE '<img class="post-media__item"[^>]*width="[0-9]+" height="[0-9]+"'; then
|
||||
ok "inline body images carry width/height"
|
||||
else
|
||||
bad "inline image dimensions" "an embedded body image has no dimensions"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# The sitemap has to advertise the pages, or hosting the bodies buys nothing.
|
||||
if curl -s "$BASE/sitemap.xml" | grep -qE '<loc>https://catcrafts.net/posts/[a-z0-9-]+</loc>'; then
|
||||
ok "sitemap lists the post pages"
|
||||
else
|
||||
bad "sitemap post pages" "no /posts/<slug> entry"
|
||||
fi
|
||||
|
||||
echo "== headers =="
|
||||
header_has / 'x-content-type-options: *nosniff' "nosniff on pages"
|
||||
header_has / 'cache-control: *public' "pages are cacheable"
|
||||
|
|
|
|||
Loading…
Reference in a new issue