- Add Gemini CLI and Antigravity to the list of tools used
- Add dedicated section with deprecation timeline (June 18 cutoff)
- Update tags with Gemini CLI and Antigravity
Extract newsletter scripts to a neutral scripts/newsletter/ engine and add
side-by-side support for all three AI coding tools off one source of truth.
- Move 9 scripts + substack config from .claude/skills/**/scripts to
scripts/newsletter/ (history preserved); fix PROJECT_ROOT depth and
cross-require/config paths; repoint all SKILL.md invocations
- Add canonical AGENTS.md; reduce CLAUDE.md to an @AGENTS.md import
- Add opencode.json (permissions, no MCP); skills auto-discovered in place
- Add codex/prompts/*.md (6 prompts) + copy installers (install.sh/.ps1)
- Add docs/multi-tool-usage.md (setup, per-tool invocation, teardown) + README pointer
- fix checkDuplicate false-negative on Substack cover images (uuid with no size suffix)
- stop cleanUrl from corrupting the query string on unparseable input
- extract shared fetchWithTimeout into url-utils (consistent timer cleanup)
- split find-substack-post html/rss helpers into html-text-utils
- scan content/post for duplicates, matching the other scripts
- correct doc drift in mt-add-image/mt-add-url and drop plan-phase code comment
ByteByteGo images carry no caption/alt and image order cannot be mapped to
titles automatically (sponsor and video items interleave). Extract the source
post's TOC topic titles as candidates and let the user pick the matching label
(caption is used directly when a publication provides one; typing is always
available). Validated against newsletters 101-106: the correct title appears
in the candidate set for all 8 images.
Add the image handler in the meta-dispatch architecture. Given an image
URL it resolves a label and inserts it into the newsletter Bonus -> Images.
For Substack-hosted images, decode the S3 image UUID and find the source
post across configured publications (ByteByteGo by default): RSS feed first,
then a sitemap crawl up to ~3 months back. Label priority is figure caption,
then post title, then a user-typed name. Non-Substack images are best-effort
then ask. Publications are configurable and the skill offers to add a new one
when the user identifies it.
Wire mt-add-url to dispatch the image route to mt-add-image (previously a
fallback). Update the project skills list.
Add mt-add-url meta skill that classifies a URL and auto-dispatches to a
handler skill: articles to mt-add-post, YouTube links to the new
mt-add-video (Bonus -> Videos with a Vietnamese title + short summary).
Unsupported types (image, etc.) prompt the user to add or extend a skill;
image support is deferred to a future mt-add-image handler.
Shared logic (add-url.js router, url-utils.js, find-newsletter-number.js)
and a shared post-mechanics reference live under mt-add-url/. mt-add-post
is reduced to an article-only handler. add-url.js detects watch/youtu.be/
shorts, normalizes to the canonical watch URL, dedups by video id, and
fetches the title via oEmbed. checkDuplicate uses execFileSync (no shell).
bareUrl() dropped the query string, collapsing every youtube.com/watch
URL to the same bare key and flagging all YouTube links as duplicates.
Keep host identity params (v for youtube) so distinct videos are not
treated as duplicates.
Run 25919953710 still failed htmltest on both Hugo legs. Two distinct
issues, both pre-existing latent bugs that v0.3.0 made impossible to
ignore:
1. Several theme templates piped paths with a leading slash through
`relURL`, which in Hugo means "this is already absolute — leave it
alone." The intended subpath prefix (`/tsuki/`) was therefore never
added, producing hrefs like `href="/search/"` and `href="/"`. In
production the GitHub Pages mount masked this (broken links from
the page's perspective still happened to resolve under the repo
path most of the time), but htmltest correctly reported them as
broken. Fix by either:
- using `site.Home.RelPermalink` for the home link, or
- piping a string without a leading slash through `relLangURL`
(so Hugo prepends the baseURL path), or
- stripping the leading slash from data-driven URLs first via
`strings.TrimPrefix "/"` and then `relLangURL`.
Affected: header.html, search-button.html, recent-posts.html,
home/hero.html, nav.html, 404.html, search/list.html (Pagefind UI
asset URLs).
2. htmltest has no URL-rewriting feature (the `URLSwap` config in the
prior `d30f50f` "fix" was never read by htmltest — confirmed by
reading htmltest 0.17.0 source). The only clean way to make
internal-link checking work with a `/repo/`-style baseURL is to
mirror the URL structure on disk. CI now builds with
`hugo --destination public/tsuki`, runs Pagefind/smoke/htmltest
against that path, and uploads `exampleSite/public/tsuki/` as the
Pages artifact. The artifact contents become the served site
under `/tsuki/`, identical to the prior behaviour from the
browser's point of view. `.htmltest.yml` drops the dead URLSwap
block.
Also: `tmp/` (htmltest's runtime cache) added to .gitignore so local
runs don't dirty the working tree.
Local verification: 32 smoke checks green; htmltest reports
`✔✔✔ passed, tested 29 documents` against the subpath build.
Three independent CI failures from run 25916500415:
1. Smoke test [linkToSection aria-label] fail on Hugo 0.146 leg.
The assertion pinned to $post selected via `find ... | head -1`,
which on 0.146 picked `doc-sach-quan-sat/index.html` — a post with
zero h2/h3/h4 headings, so zero heading-anchor markup. Rewrite the
check to scan all built post HTML (same pattern already in use for
render-link rel). Now reports 13 matches on the demo build.
2. htmltest URLSwap silently never matched. The prior
`^/tsuki/: /` config (added in d30f50f) used a regex anchor that
doesn't match htmltest's substring evaluation against minified
`href=/tsuki/...` attributes. Switch to the unanchored
`"/tsuki/": /` form. CI has been failing on this since v0.2.0
despite the "fix" commit; this is the real fix.
3. head.html referenced `/favicon.ico` and `/favicon.svg` but neither
asset existed in exampleSite/static/. Add a minimal SVG favicon,
drop the legacy .ico reference (modern browsers fall back to a
site-rooted /favicon.ico by convention; sites needing one can
override head.html). Switch the SVG link to `relURL` so the
`/tsuki/` baseURL prefix is honoured under GitHub Pages subpath
deploy.
Local validation: all 32 smoke checks green; favicon resolves to
`/tsuki/favicon.svg` and ships in the build artifact.
- Append review footer to 101 newsletter posts
- Remove 13 duplicate sections across 11 posts (link kept in earlier post)
- Strikethrough user's personal commentary about AI tools/models in intros
- README: add a11y badge linking to docs/accessibility.md.
- CHANGELOG: full v0.3.0 entry under Unreleased (Added/Changed/Fixed/
Deferred-to-v0.3.1). Documents per-kind CSS bundles, Tier A
features, Lighthouse-relevant polish, contrast-token visual diff,
tap-target sizing decisions, and Hugo CI matrix.
- docs/accessibility.md: new — WCAG 2.2 AA conformance summary,
Lighthouse measurement instructions, projected baseline (TBD entries
to fill after first production deploy), known limitations.
- docs/customization.md: cover-image override snippet (v0.4.0 native
pipeline promise); document breadcrumbs/prev-next/lang-switcher opt-ins.
- plans/260510-0144-tsuki-v0.3.0/: mark all phases completed; note
narrow-TOC <details> deferral to v0.3.1; record audit-pass scope
additions integrated into Phases 2–6.
- plans/reports/: add researcher-260515-tsuki-vs-stack-papermod-feature
-gap.md (verdict: feature parity on must-haves; Tier A gaps shipped
this round) + code-reviewer-260515-lighthouse-80-baseline-audit.md
(projected ≥80 on all 4 categories; ≥95 a11y achievable with
shipped fixes).
scripts/smoke-tests.sh:
- Per-kind CSS budget (home/post/list/archives/search), each ≤ 4200 B gz
- theme-color light/dark meta tags
- aria-pressed SSR on theme-toggle
- breadcrumbs nav + BreadcrumbList JSON-LD presence on post,
absence on home
- prev/next nav + rel=prev attribute
- linkToSection aria-label on heading anchors
- Speculation Rules absent by default
- llm.txt artifact present
- Pagefind UI preload-swap pattern on /search/
- Giscus preconnect gating (absent when comments disabled)
- Per-kind CSS bundle routing (home loads home.css but not single.css;
post loads single.css but not home.css)
- code-copy.js gating (post loads it, home does not)
.github/workflows/pages.yml:
- Build matrix on Hugo 0.146.0 (theme.toml floor) + 0.154.0 (current).
Smoke + htmltest run on both. Artifact upload + deploy gated on
the current version only.
- Drop the redundant standalone CSS-budget step; smoke-tests.sh now
enforces per-kind budgets.
Headline outcomes:
- Feature parity with Stack/PaperMod on Tier A polish: breadcrumbs
(+BreadcrumbList JSON-LD), prev/next post navigation (rel=prev/next),
language switcher UI (gated on hugo.IsMultilingual), code-copy button
polish, <details> styling.
- Per-page-kind CSS bundles: core loads everywhere; home/single/archive/
search bundles load only where needed. Frees ~1 KB gz from non-post
pages. code-copy.js gated to post pages.
- Lighthouse-relevant network polish: Pagefind UI CSS preload-swap,
conditional preconnect to giscus.app on comment-enabled posts,
hreflang alternates on multilingual sites, <meta name=theme-color>
light/dark variants, aria-pressed SSR-rendered on theme-toggle.
- WCAG AA contrast: --tsuki-fg-subtle darkened to #6b6b6b (light); was
3.54:1 on bg, now 5:1. Pagination disabled uses --tsuki-fg-muted
without opacity compound. Header tap targets bumped to 40×40; pagination
to 44×44.
- i18n: full i18n/en.yml mirror (~50 keys); render-heading aria-label
i18n-driven via linkToSection key; new keys for breadcrumb*, prevPost,
nextPost, copyCode, copiedCode.
- Discovery: /llm.txt output format (llmstxt.org) on home; Speculation
Rules opt-in via params.prefetch.enable.
- <html lang> fallback: site.Language.LanguageCode → Lang → "en"
(was hard-coded "vi").
Per-kind bundle gz sizes on demo: home 3673 / post 4167 / list 2897 /
archives 3363 / search 3179 B (all under 4200 B cap).
Plan: plans/260510-0144-tsuki-v0.3.0/
Separate the general BigModel platform invite (20M free tokens) from
the new GLM Coding Plan referral (5% friend discount, tiered cashback
at 3 and 30 invitees).
htmltest was resolving links like `/tsuki/css/foo.css` against
`exampleSite/public/tsuki/...` (which doesn't exist); actual path
is `exampleSite/public/...`. URLSwap strips the baseURL prefix.
Failing on every CI run since v0.2.0 — deploys not propagating.
The 7-phase v0.2.0 implementation roadmap is shipped (commits
6e17ee6..5d73a13). Move it under plans/archive/ so plans/ root only
shows active work.
Add plans/260509-0947-v0.2.0-prerelease-checklist/ capturing what's
left before the v0.2.0 tag and gallery submission: action SHA pin,
phase-07 plan-todo accuracy fix, CI watch, journal, tag, plus
maintainer decisions (Q1 unsafe Goldmark, Q3 audience) and external
tasks (hugoBasicExample smoke, hugoThemes registry PR).
- Extract duplicated TOC predicate to _partials/toc-enabled.html
(single.html + footer.html were both computing the same 6-line gate).
- Inline _partials/head/og-image.html into head/seo.html — single caller,
the fallback chain is the contract; the partial wasn't earning its
override surface.
- Drop 6 unused i18n keys: postedOn, tags, categories, archive, noResults,
copyright. None referenced by any template (verified via grep). Also
removed an accidental duplicate of the callout title block; vi.yml
reorganized into thematic groups.
Behavior unchanged. Smoke (11 checks) + Hugo build clean. CSS bundle
unchanged at 3962 B gz.
- search/list.html: Pagefind UI container + module init with full vi
i18n strings; loads /pagefind/pagefind-ui.{css,js} from generated dir
- search-button.html in header, gated by params.search.enable
- comments.html for Giscus, opt-in via params.comments.giscus.enable;
reads repo/repoId/category/categoryId/mapping/theme/lang from params
- giscus-theme.js: MutationObserver on data-theme, postMessage iframe
on toggle so Giscus theme stays in sync with site
- search.css: header-actions row, search button, Pagefind UI tokens
mapped to tsuki design tokens
- comments.css: min-block-size on iframe to prevent layout shift
- exampleSite/hugo.yaml: commented giscus params block as docs
- i18n/vi.yml: Pagefind UI strings (clear, loadMore, zeroResults...)
Both features no-op when disabled. CSS bundle 3.5 KB gz (budget 15).
- post/list.html paginated section list with post-card
- pagination.html partial (renders only when >1 page)
- _default/taxonomy.html for term lists (/tags/, /categories/) with counts
- taxonomy/term.html for single term pages (paginated posts)
- archives/list.html + archive-group.html: year/month grouped flat list
- exampleSite: 4 new posts across 2025-2026, 2 categories, 4 tags, archive index
Modern Hugo 0.146+ template naming: term.html resolved via taxonomy/
path (Hugo 0.154 lookup ignores _default/term.html).