Repository navigation
Refuse hosting addresses such as *.vercel.app in newsletters - #36
Merged
Merged
Conversation
Public links go to policyengine.org, never to the address of the platform that hosts a page (Max, 2026-10-29). The 29 October US edition went out with seven ballot-measures-2026.vercel.app links because the policyengine.org route was not live yet. - hosting_links.py finds *.vercel.app, vercel.com, policyengine.github.io, Netlify, Cloudflare Pages, Modal, Cloud Run and App Engine hosts in an edition's HTML, after decoding entities and percent-escapes. - upload-newsletter runs it before any Mailchimp call, for new campaigns and --campaign-id updates. - lint-newsletters.sh fails editions dated 2026-10-30 or later. - CLAUDE.md documents the rule and moves draft updates to upload-newsletter --campaign-id, which runs the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes three findings from the review of TheAxiomFoundation/newsletters#15, which apply here too: - The uploader now checks the subject line, preview text and campaign title as well as the HTML, before any Mailchimp call, for new campaigns and --campaign-id updates. - Tabs and newlines inside an attribute value drop out before the host check, raw or as entities or percent-escapes, as browsers drop them from URLs. Lines outside the value keep their numbers. - Hosts fold the way a browser folds them: NFKC, the ideographic and full-width stops as dots, and invisible characters dropped. Labels may be Unicode, so vercel.app.例子.org and vercel.app。example.org pass while x.vercel。app is caught. All 32 editions give the same hits as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to TheAxiomFoundation/newsletters
that referenced
this pull request
Oct 29, 2026
Fixes the three findings from the review of this PR, ported from PolicyEngine/newsletters#36 so the two checks stay the same: - The uploader now checks the subject line, preview text and campaign title as well as the HTML, before any Mailchimp call, for new campaigns and --campaign-id updates. - Tabs and newlines inside an attribute value drop out before the host check, raw or as entities or percent-escapes, as browsers drop them from URLs. Lines outside the value keep their numbers. - Hosts fold the way a browser folds them: NFKC, the ideographic and full-width stops as dots, and invisible characters dropped. Labels may be Unicode, so vercel.app.例子.org and vercel.app。example.org pass while x.vercel。app is caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the second review's findings: - Tabs and newlines drop out only inside URL attributes such as href and src, where a browser drops them. In alt or title text, or a shell flag, they stay breaks between words, so "at\npolicyengine.github.io" is caught again and "Modal.\nRun" no longer reads as modal.run. A URL value that runs past a missing closing quote into markup stays as it is. - Link text reads through inline tags, so x.vercel<wbr>.app is caught. - Underscores around a host count as Markdown emphasis: _vercel.app_ is caught, foo_vercel.app still passes. - The ignored characters are the UTS #46 set (idna 3.20) plus stringprep B.1. - Both orgs' GitHub Pages hosts count as hosting addresses. - Nested matches are filtered in O(k log k), not O(k^2). - The lint fails when the checker crashes, and checks Markdown social posts. All 41 editions, posts and Axiom files give the same hits as 4891a55. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
to TheAxiomFoundation/newsletters
that referenced
this pull request
Oct 30, 2026
Ports the second review's fixes from PolicyEngine/newsletters#36 (91f450b), so the two checks stay the same: - Tabs and newlines drop out only inside URL attributes such as href and src. In alt or title text they stay breaks between words, so a host after a line break is caught and "Modal.\nRun" no longer reads as modal.run. - Link text reads through inline tags, so x.vercel<wbr>.app is caught. - Underscores around a host count as Markdown emphasis. - The ignored characters are the UTS #46 set (idna 3.20) plus stringprep B.1. - policyengine.github.io now counts as a hosting address here too. - Nested matches are filtered in O(k log k). This lint already fails closed and has no Markdown posts to check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tag now ends before the next "<", so a line of unclosed openers such as "<a <a <a" no longer scans to the end of the line from each one. 300 KB of them took 7.5 s before and takes 0.05 s now, with the same hits. Adds a 900 KB timing test. Round-3 review finding R3-1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 30, 2026 01:06
MaxGhenis
added a commit
to TheAxiomFoundation/newsletters
that referenced
this pull request
Oct 30, 2026
* Refuse hosting addresses such as *.vercel.app in newsletters Keep public Axiom links on axiom.org instead of hosting addresses, following Max's 2026-10-29 rule. Port the check from PolicyEngine/newsletters#36 with Axiom's GitHub Pages host and canonical site routes. Reject hosting addresses after decoding HTML entities and percent-escapes, before any Mailchimp call for uploads or draft updates. Check every edition and template in the HTML lint with no date cutoff: all five current editions pass. Document the rule and cover detection, encoding, domain boundaries, line numbers, uploader/CLI refusal, and lint behavior with regression and Hypothesis tests. Validate all 92 tests on Python 3.11 and 3.8, Black, Ruff, newsletter lint, and three mutation checks that each produce failing tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read hosts the way a browser does; check subject, preview and title Fixes the three findings from the review of this PR, ported from PolicyEngine/newsletters#36 so the two checks stay the same: - The uploader now checks the subject line, preview text and campaign title as well as the HTML, before any Mailchimp call, for new campaigns and --campaign-id updates. - Tabs and newlines inside an attribute value drop out before the host check, raw or as entities or percent-escapes, as browsers drop them from URLs. Lines outside the value keep their numbers. - Hosts fold the way a browser folds them: NFKC, the ideographic and full-width stops as dots, and invisible characters dropped. Labels may be Unicode, so vercel.app.例子.org and vercel.app。example.org pass while x.vercel。app is caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop line breaks only in URL attributes; read text through inline tags Ports the second review's fixes from PolicyEngine/newsletters#36 (91f450b), so the two checks stay the same: - Tabs and newlines drop out only inside URL attributes such as href and src. In alt or title text they stay breaks between words, so a host after a line break is caught and "Modal.\nRun" no longer reads as modal.run. - Link text reads through inline tags, so x.vercel<wbr>.app is caught. - Underscores around a host count as Markdown emphasis. - The ignored characters are the UTS #46 set (idna 3.20) plus stringprep B.1. - policyengine.github.io now counts as a hosting address here too. - Nested matches are filtered in O(k log k). This lint already fails closed and has no Markdown posts to check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the inline-tag pattern linear on unclosed tags A tag now ends before the next "<", so a line of unclosed openers such as "<a <a <a" no longer scans to the end of the line from each one. 300 KB of them took 7.5 s before and takes 0.05 s now, with the same hits. Adds a 900 KB timing test. Round-3 review finding R3-1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Max, 2026-10-29: "we need a rule to never share vercel links publicly - should always be on policyengine.org, esp in newsletters".
The 29 October US edition (#35) went out to 762 people with seven
ballot-measures-2026.vercel.applinks. The policyengine.org route (PolicyEngine/policyengine-app-v2#1415) wasn't live, and the drafting session swapped the links to Vercel to make them work. Earlier editions did the same thing:2025-11-25-uk.htmllinked agit-devpreview deployment;2026-03-18-us.htmllinkedpolicyengine.github.io/tanf-calculator;2026-04-01-us.htmllinked a Vercel app.What changes
src/newsletter_uploader/hosting_links.pyfinds hosting addresses in an edition:*.vercel.app,vercel.com,*.netlify.app,*.pages.dev,*.modal.run,*.run.app,*.appspot.com, and the GitHub Pages sites of PolicyEngine and Axiom (policyengine.github.io,theaxiomfoundation.github.io);It reads each host the way a browser or a reader does:
href,src,srcset,actionand the like), as browsers drop them from a URL. In alt or title text a line break stays a break between words, so "We left Modal.⏎Run it" passes.<wbr>,<span>or<b>don't split the host a reader sees:x.vercel<wbr>.appcounts.。,.and。become.; the characters a hostname ignores (UTS #46 and stringprep B.1: soft hyphen, zero-width spaces, variation selectors and the rest) drop out._policyengine.github.io/tanf_counts.Labels may be Unicode, so a longer host that only starts like a hosting one, such as
vercel.app.例子.orgorvercel.app。example.org, passes. Other people'sgithub.iosites (for examplefhoces.github.io) pass. A reviewed exception goes inALLOWED_HOSTS.upload-newsletterrefuses such HTML, subject lines, preview text and campaign titles before it calls Mailchimp. The check runs for new campaigns and for--campaign-idupdates, so an empty campaign never gets created.lint-newsletters.shfails any edition dated 2026-10-30 or later that links one, and adds a GitHub Actions error annotation on the line. It checks HTML editions, Markdown editions and the rootsocial-posts-*.mdfiles. Older editions went out as they are, so the lint skips them. If the checker crashes, the lint fails rather than passing the file.CLAUDE.mdgets a "Links" section with the rule. Its "Updating existing drafts" section now usesupload-newsletter --campaign-id, which runs the check, in place of a raw PUT to Mailchimp's/contentendpoint, which skipped it.The open PRs' editions are all dated before 30 October, so none of them starts failing.
Invariants (tested)
test_property_any_hosting_host_in_any_text_is_caught,test_property_a_host_in_running_text_is_caught,test_property_encoding_never_hides_a_host). ,	or%0Aanywhere in a URL attribute, and any of the dots.,。,.,。,.or%2E, still give the hosting host (test_property_whitespace_and_unicode_dots_never_hide_a_host). A host after a line break in any attribute is caught (test_property_a_host_after_a_line_break_in_any_attribute_is_caught).is_hosting_hostrejects (test_property_other_hosts_are_never_flagged,test_property_a_longer_host_that_starts_like_a_hosting_one_passes). Lookalikes such asnotvercel.app,myvercel.com,vercel.app.example.com,vercel.app.例子.organdfoo_vercel.apppass, and only URL attributes drop line breaks (test_only_url_attributes_drop_line_breaks).test_property_reported_line_is_the_line_the_link_sits_on). A host inside an attribute value that spans lines reports the line where the value starts; every line after it keeps its number.test_many_hits_on_one_line_stay_fast,test_unclosed_inline_tags_stay_fast).mock_calls == []), for both upload and update.ballot-measures-2026.vercel.appoccurrence in the sent 29 October edition, plus the hosting links in the three older editions above. On all 35 edition and social-post files in the repo, it gives the same 12 hits as the first version (4891a55).Checks run here
pytest: 178 passed on Python 3.11 and on Python 3.8, the low end of the CI matrix.black --checkandruff checkare clean.lint-newsletters.shpasses on every file in the repo.。mapping or the ignored-character removal, or leaving out its supplementary-plane ranges;Known limits
This package and TheAxiomFoundation/newsletters#15 share the same matching code, so neither backs up the other on these:
<nobr>,<o:p>, an HTML comment or a tag that spans lines.x.vercel.app--now.href, and the check then leaves thathrefunjoined. It also leaves any value containing<unjoined.<inside an inline tag's quoted attribute ends the tag early, so the check missesx.vercel<span title="a<b">.app. On broken markup such as<a Run</b>.appit can flagrun.app.display:blockorinline-blockwith no whitespace between them can glue words a reader sees apart, such as "We left Modal." and "Run it". Reading through inline tags adds no hit on any file in the repo.Review
axiom: n/a: newsletter tooling
🤖 Generated with Claude Code