mirror of
https://github.com/tiennm99/blog.git
synced 2026-10-11 03:13:10 +00:00
feat(newsletter): report post entry tally after each URL add
This commit is contained in:
1 parent
edff23f0e3
commit
f759f7a8ba
8 files changed
+139
No files matched your search
@@ -84,7 +84,9 @@ Create `### Bonus` / `**Images:**` if missing.
|
|||||||
📄 content/post/YYYY/MM/DD/index.md
|
📄 content/post/YYYY/MM/DD/index.md
|
||||||
🏷️ label: "[label]" (source: candidate-pick | caption | user-input; post: [postTitle])
|
🏷️ label: "[label]" (source: candidate-pick | caption | user-input; post: [postTitle])
|
||||||
🖼️ [clean_image_url]
|
🖼️ [clean_image_url]
|
||||||
|
📊 [tally]
|
||||||
```
|
```
|
||||||
|
The tally comes from `post-stats` — see *Post tally* in the shared post mechanics.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ Trust `route: article`; skip if `duplicate` or not `accessible`.
|
|||||||
✅ Article added to Newsletter #[number]
|
✅ Article added to Newsletter #[number]
|
||||||
📄 content/post/YYYY/MM/DD/index.md
|
📄 content/post/YYYY/MM/DD/index.md
|
||||||
🔗 [clean_url]
|
🔗 [clean_url]
|
||||||
|
📊 [tally]
|
||||||
```
|
```
|
||||||
|
The tally comes from `post-stats` — see *Post tally* in the shared post mechanics.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,19 @@ Act on the user's choice. If they choose add/update, proceed to design that skil
|
|||||||
|
|
||||||
### 5. Final report
|
### 5. Final report
|
||||||
|
|
||||||
|
Close with the target post's TL;DR tally, then the per-URL detail. Read the tally from the post itself so it reflects everything the post now holds, not just this batch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./scripts/newsletter post-stats content/post/YYYY/MM/DD/index.md
|
||||||
|
```
|
||||||
|
|
||||||
Aggregate across all URLs:
|
Aggregate across all URLs:
|
||||||
```
|
```
|
||||||
✅ Newsletter URL Dispatch Complete
|
✅ Newsletter URL Dispatch Complete
|
||||||
|
|
||||||
|
📊 Newsletter #[number]: [articles] articles · [videos] videos · [images] images
|
||||||
|
(omit zero counts; documents too when present)
|
||||||
|
|
||||||
✅ Dispatched: [count]
|
✅ Dispatched: [count]
|
||||||
- [count] → mt-add-post (articles)
|
- [count] → mt-add-post (articles)
|
||||||
- [count] → mt-add-video (YouTube)
|
- [count] → mt-add-video (YouTube)
|
||||||
@@ -75,6 +84,8 @@ Aggregate across all URLs:
|
|||||||
- [url] (route: [route]): [user decision]
|
- [url] (route: [route]): [user decision]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The tally is report-only — never write it into `index.md`. See *Post tally* in `references/newsletter-post-mechanics.md`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Handlers (`mt-add-post`, `mt-add-video`, `mt-add-image`) remain directly invocable for single-purpose use, but `mt-add-url` is the normal entry point when a user pastes a URL.
|
- Handlers (`mt-add-post`, `mt-add-video`, `mt-add-image`) remain directly invocable for single-purpose use, but `mt-add-url` is the normal entry point when a user pastes a URL.
|
||||||
|
|||||||
@@ -83,6 +83,23 @@ If the post has **no `### Bonus`** yet:
|
|||||||
```
|
```
|
||||||
When a subsection (e.g. `**Videos:**`) already exists, append under it; otherwise create it. Keep subsections in this order: **Images** → **Videos** → **Documents**.
|
When a subsection (e.g. `**Videos:**`) already exists, append under it; otherwise create it. Keep subsections in this order: **Images** → **Videos** → **Documents**.
|
||||||
|
|
||||||
|
## 4a. Post tally (report line)
|
||||||
|
|
||||||
|
After every successful insertion, report the target post's running totals so the user can see what the post now holds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./scripts/newsletter post-stats content/post/YYYY/MM/DD/index.md
|
||||||
|
```
|
||||||
|
Output (JSON): `{ post, newsletter, articles, images, videos, documents, total }`.
|
||||||
|
|
||||||
|
Render it as a single TL;DR line in the handler's report, omitting zero counts:
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 4 articles · 2 videos · 1 image
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the counts the command returns — do not tally by hand. This is a **report-only** line: never write it into `index.md`.
|
||||||
|
|
||||||
## 5. Post content language guidelines
|
## 5. Post content language guidelines
|
||||||
|
|
||||||
These rules apply only to text written into `content/post/**/index.md`. Keep user-facing questions, status updates, reports, and final responses in English unless the user explicitly requests another language.
|
These rules apply only to text written into `content/post/**/index.md`. Keep user-facing questions, status updates, reports, and final responses in English unless the user explicitly requests another language.
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ A clean YouTube URL (passed by `mt-add-url`, or given directly).
|
|||||||
✅ Video added to Newsletter #[number] (Bonus → Videos)
|
✅ Video added to Newsletter #[number] (Bonus → Videos)
|
||||||
📄 content/post/YYYY/MM/DD/index.md
|
📄 content/post/YYYY/MM/DD/index.md
|
||||||
🎬 [Vietnamese title] — [clean_url]
|
🎬 [Vietnamese title] — [clean_url]
|
||||||
|
📊 [tally]
|
||||||
```
|
```
|
||||||
|
The tally comes from `post-stats` — see *Post tally* in the shared post mechanics.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ go run ./scripts/newsletter list-existing-tags # existing tag frequenc
|
|||||||
go run ./scripts/newsletter detect-image-source "<url>" # detect Substack image + uuid
|
go run ./scripts/newsletter detect-image-source "<url>" # detect Substack image + uuid
|
||||||
go run ./scripts/newsletter find-substack-post --uuid <uuid>
|
go run ./scripts/newsletter find-substack-post --uuid <uuid>
|
||||||
go run ./scripts/newsletter fetch-via-defuddle "<url>" # fallback fetch (tier 1 of the fetch chain)
|
go run ./scripts/newsletter fetch-via-defuddle "<url>" # fallback fetch (tier 1 of the fetch chain)
|
||||||
|
go run ./scripts/newsletter post-stats <post-path> # count a post's articles/images/videos/documents
|
||||||
```
|
```
|
||||||
|
|
||||||
These are shared by all three tools — no tool-specific copies.
|
These are shared by all three tools — no tool-specific copies.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Commands:
|
|||||||
detect-image-source <url> detect Substack image + uuid
|
detect-image-source <url> detect Substack image + uuid
|
||||||
find-substack-post --uuid <uuid> [--deep] find the post embedding an image uuid
|
find-substack-post --uuid <uuid> [--deep] find the post embedding an image uuid
|
||||||
fetch-via-defuddle <url> fallback fetch via defuddle.md proxy
|
fetch-via-defuddle <url> fallback fetch via defuddle.md proxy
|
||||||
|
post-stats <path/to/index.md> count the post's articles/images/videos/documents
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ func main() {
|
|||||||
runFindSubstackPost(args)
|
runFindSubstackPost(args)
|
||||||
case "fetch-via-defuddle":
|
case "fetch-via-defuddle":
|
||||||
runFetchViaDefuddle(args)
|
runFetchViaDefuddle(args)
|
||||||
|
case "post-stats":
|
||||||
|
runPostStats(args)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||||
usage()
|
usage()
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Count the entries already present in a newsletter post, so a handler can
|
||||||
|
// report a running tally after each insertion.
|
||||||
|
// Usage: go run ./scripts/newsletter post-stats <path/to/index.md>
|
||||||
|
// Outputs: JSON { post, newsletter, articles, images, videos, documents, total }
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entry shapes, per the Bonus format in the shared post mechanics:
|
||||||
|
//
|
||||||
|
// articles "## [Title](url)" (level-2 heading, main content)
|
||||||
|
// images "" (under **Images:**)
|
||||||
|
// videos "[Title](url)" (under **Videos:**)
|
||||||
|
// documents "[PDF: title](url)" (under **Documents:**)
|
||||||
|
var (
|
||||||
|
articleHeadingRe = regexp.MustCompile(`^##\s+\[`)
|
||||||
|
bonusHeadingRe = regexp.MustCompile(`^###\s+Bonus\b`)
|
||||||
|
subsectionRe = regexp.MustCompile(`^\*\*(Images|Videos|Documents):\*\*`)
|
||||||
|
imageEntryRe = regexp.MustCompile(`^!\[`)
|
||||||
|
linkEntryRe = regexp.MustCompile(`^\[`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type postStats struct {
|
||||||
|
Post string `json:"post"`
|
||||||
|
Newsletter int `json:"newsletter"`
|
||||||
|
Articles int `json:"articles"`
|
||||||
|
Images int `json:"images"`
|
||||||
|
Videos int `json:"videos"`
|
||||||
|
Documents int `json:"documents"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// countPostEntries walks the post once. Article headings are counted anywhere
|
||||||
|
// outside Bonus; asset entries are attributed to whichever subsection is open.
|
||||||
|
func countPostEntries(content string) postStats {
|
||||||
|
var s postStats
|
||||||
|
inBonus := false
|
||||||
|
subsection := ""
|
||||||
|
|
||||||
|
for _, raw := range strings.Split(content, "\n") {
|
||||||
|
line := strings.TrimSpace(raw)
|
||||||
|
switch {
|
||||||
|
case bonusHeadingRe.MatchString(line):
|
||||||
|
inBonus = true
|
||||||
|
subsection = ""
|
||||||
|
continue
|
||||||
|
case subsectionRe.MatchString(line):
|
||||||
|
subsection = subsectionRe.FindStringSubmatch(line)[1]
|
||||||
|
continue
|
||||||
|
case articleHeadingRe.MatchString(line):
|
||||||
|
s.Articles++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inBonus {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch subsection {
|
||||||
|
case "Images":
|
||||||
|
if imageEntryRe.MatchString(line) {
|
||||||
|
s.Images++
|
||||||
|
}
|
||||||
|
case "Videos":
|
||||||
|
// A direct video file entry looks the same as a YouTube entry;
|
||||||
|
// both belong to the Videos tally.
|
||||||
|
if linkEntryRe.MatchString(line) {
|
||||||
|
s.Videos++
|
||||||
|
}
|
||||||
|
case "Documents":
|
||||||
|
if linkEntryRe.MatchString(line) {
|
||||||
|
s.Documents++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Total = s.Articles + s.Images + s.Videos + s.Documents
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPostStats(args []string) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: post-stats <path/to/index.md>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
path := args[0]
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "read post:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
stats := countPostEntries(string(content))
|
||||||
|
stats.Post = path
|
||||||
|
if m := newsletterNumRe.FindSubmatch(content); m != nil {
|
||||||
|
stats.Newsletter, _ = strconv.Atoi(string(m[1]))
|
||||||
|
}
|
||||||
|
printJSON(stats)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user