feat(skills): split newsletter URL workflow into meta dispatcher + handlers

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).
This commit is contained in:
2026-05-30 16:23:27 +07:00
parent 9bf8dbbf77
commit cd6733409b
8 changed files with 393 additions and 205 deletions
+25 -160
View File
@@ -1,186 +1,51 @@
---
name: mt-add-post
description: 'URL processing workflow for Hugo blog newsletter posts. Use when user wants to add URLs to their newsletter, process blog content, extract content from links, or update daily newsletter posts. Automatically processes URLs, extracts content, generates Vietnamese summaries, and creates/updates daily newsletter posts with proper numbering and formatting.'
description: 'Article handler for the Hugo blog newsletter. Adds a single article/blog URL to today''s newsletter as a main-content entry with a Vietnamese summary. Normally invoked by the mt-add-url meta skill after classification, but can be used directly for a known article URL.'
---
## Overview
This skill manages the complete workflow for adding newsletter posts to a Hugo blog with Vietnamese tech content.
`mt-add-post` is the **article handler**: given a clean article/blog URL, it extracts the content, writes a Vietnamese summary, and inserts it into today's newsletter main content. It does **not** classify or route URLs — that is `mt-add-url`'s job. For YouTube/images/other types, use `mt-add-url`.
**Project Context:**
- Type: Hugo static site with `hugo-theme-stack` theme
- Language: Vietnamese (99%) with minimal English tech terms (1%)
- Timezone: Asia/Ho_Chi_Minh (UTC+7)
- Content structure: `content/post/YYYY/MM/DD/index.md`
Shared scripts: `.claude/skills/mt-add-url/scripts/`. Shared procedure: `../mt-add-url/references/newsletter-post-mechanics.md` (find/create today's post, newsletter numbering, section insertion, language rules) — **follow it** for all post mechanics.
## Quick Workflow
## Input
```
1. Prepare URLs (clean → validate → check duplicates → classify)
2. Find today's post (create or update)
3. Determine newsletter number
4. Extract content and generate Vietnamese summaries
5. Write formatted content
6. Provide final report
```
## Step-by-Step Instructions
### 1. Prepare URLs
For each URL, run the bundled script:
A clean article URL (passed by `mt-add-url`, or given directly). If a raw URL is provided directly, you may run the classifier to clean/dedup it first:
```bash
node .claude/skills/mt-add-post/scripts/prepare-url.js "<url>"
node .claude/skills/mt-add-url/scripts/add-url.js "<url>"
```
Trust `route: article`; skip if `duplicate` or not `accessible`.
The script handles:
- **Clean**: Remove tracking params (`utm_*`, `fbclid`, `gclid`, `msclkid`, `mc_eid`, `aid`, `ref`, `ref_src`, `source`, `s`, `ck_subscriber_id`, `igshid`, `yclid`, `vero_id`)
- **Validate**: Check accessibility (HTTP 200)
- **Check duplicate**: Compare by bare URL (scheme + host + path) — catches the same article even if previously saved with different tracking params
- **Classify**: Article (for main content) or asset (image/video/document → Bonus section)
## Workflow
**Skip** URLs that are: inaccessible, duplicates, or fail extraction.
1. **Post mechanics** — follow `../mt-add-url/references/newsletter-post-mechanics.md` to find/create today's post and (if new) get the newsletter number + apply the template.
2. **Extract** the article title and main content.
3. **Summarize in Vietnamese** — 1-2 paragraphs, max 300 words, professional tone for junior developers. Optionally add 3-5 key points.
4. **Write** the main-content block and insert it **before** the `### Bonus` section (or append at end of file if there is no Bonus yet — do not create an empty Bonus):
### 2. Find Today's Post
Get current date in `YYYY-MM-DD` format (UTC+7).
Check if `content/post/YYYY/MM/DD/index.md` exists:
- **Exists** → Update this file
- **Not exists** → Create new with incremented newsletter number
### 3. Determine Newsletter Number
Run the bundled script:
```bash
node .claude/skills/mt-add-post/scripts/find-newsletter-number.js
```
This searches backwards from current date to find the most recent newsletter and returns the next number.
### 4. Generate Content
**For Article URLs:**
- Extract title and main content
- Generate Vietnamese summary (1-2 paragraphs, max 300 words)
- Optionally add 3-5 key points as bullets
**For Asset URLs** (images, videos, PDFs):
- Extract type and title
- Add to Bonus section
### 5. Write Content
**New Post Template:**
```markdown
---
title: "Newsletter #[number]"
date: YYYY-MM-DD
tags: ["AI-Assisted"]
categories: ["Newsletter"]
---
## [Tiêu đề bài viết](clean_url)
*Mời bạn thưởng thức Newsletter #[number].*
## [Article Title](clean_url)
[Vietnamese summary - professional, max 300 words]
[Tóm tắt tiếng Việt — tối đa 300 từ]
**Điểm chính:**
- [Key point 1]
- [Key point 2]
- [Ý chính 1]
- [Ý chính 2]
```
**Update Existing Post** - insert new articles **before** the Bonus section:
To insert safely without clobbering the `### Bonus` heading, use an Edit that targets `### Bonus` as the anchor and prepends the new article:
5. **Report**:
```
old_string: "### Bonus"
new_string: "## [New Article Title](clean_url)\n\n[Summary paragraphs]\n\n**Điểm chính:**\n- [point 1]\n- [point 2]\n\n### Bonus"
```
Result:
```markdown
[Existing articles...]
## [New Article Title](clean_url)
[New summary...]
### Bonus
**Images:**
![image1](url1)
```
If the post has no `### Bonus` yet (first article of the day), append the article at end of file and do NOT create an empty Bonus section — add Bonus only when there's an asset to put in it.
**Bonus Section Format:**
```markdown
### Bonus
**Images:**
![title](url)
**Videos:**
[Video: title](url)
**Documents:**
[PDF: title](url)
```
## Language Guidelines
**Primary**: Vietnamese (99%) | **Secondary**: English (1% - unavoidable technical terms only)
**Allow English when no Vietnamese equivalent exists:**
- Technology names: AI, API, GitHub, Rust, Go, Docker, Kubernetes
- Database names: PostgreSQL, MongoDB, Redis
- Company names: Google, Microsoft, Databricks
- Acronyms: MCP, CI/CD, JSON, SQL, HTTP
**Always translate to Vietnamese:**
- Common terms: code → mã nguồn, software → phần mềm, deployment → triển khai, performance → hiệu năng
- Action verbs: use → sử dụng, check → kiểm tra, build → xây dựng, test → kiểm thử
- For any term not listed above: translate if a common Vietnamese equivalent exists
**Content Requirements:**
- Audience: Junior developers
- Tone: Professional, clear, accessible
- Verify content is ≥99% Vietnamese before saving
## Error Handling
| Issue | Action |
|-------|--------|
| URL inaccessible | Skip, note in report |
| Duplicate URL | Skip, note in report |
| Extraction fails | Skip, treat as error |
| Directory missing | Create directories |
## Final Report
```
✅ Newsletter Processing Complete
📄 File: content/post/YYYY/MM/DD/index.md
📊 Action: [Created new / Updated existing] Newsletter #[number]
✅ Processed: [count] URLs
- [count] articles added
- [count] assets added to Bonus
❌ Failed: [count] URLs
- [url]: [reason]
✅ Article added to Newsletter #[number]
📄 content/post/YYYY/MM/DD/index.md
🔗 [clean_url]
```
## Checklist
Before finalizing, verify:
- [ ] Valid Hugo front matter with correct date format
- [ ] Title: `"Newsletter #[number]"`
- [ ] Valid Hugo front matter (if post was created) with correct date
- [ ] Title `"Newsletter #[number]"` (if created)
- [ ] Tags include `"AI-Assisted"`
- [ ] URLs are clean (no tracking params)
- [ ] Vietnamese grammar is correct
- [ ] Articles appear before Bonus section
- [ ] Bonus assets grouped by type
- [ ] URL is clean (no tracking params)
- [ ] Vietnamese content ≥99%, grammar correct
- [ ] Article inserted before the Bonus section
+78
View File
@@ -0,0 +1,78 @@
---
name: mt-add-url
description: 'Meta entry for adding URLs to the Hugo blog newsletter. Use whenever the user provides one or more URLs to add to their newsletter (articles, YouTube videos, etc.). Classifies each URL and auto-dispatches to the right handler skill (mt-add-post for articles, mt-add-video for YouTube). For unsupported types it asks the user how to proceed. This is the default entry point for newsletter URL processing.'
---
## Overview
`mt-add-url` is the **router**: it classifies each URL and auto-invokes the matching handler skill. Handlers (`mt-add-post`, `mt-add-video`) own the actual content writing. Shared scripts live in `.claude/skills/mt-add-url/scripts/`; shared post mechanics in `references/newsletter-post-mechanics.md`.
**Supported routes (this version):**
- `article``mt-add-post`
- `youtube``mt-add-video`
Everything else (`image`, direct `video` file, `document`, or anything unrecognized) is **not supported yet** → ask the user how to handle it. Image support is planned as a future `mt-add-image` skill.
## Workflow
### 1. Classify each URL
For every URL the user provides:
```bash
node .claude/skills/mt-add-url/scripts/add-url.js "<url>"
```
Output (JSON): `{ original_url, clean_url, http_status, accessible, duplicate, route, title?, author? }`.
- `route``youtube | image | video | document | article`
- For `youtube`, `clean_url` is the canonical `https://www.youtube.com/watch?v=ID` and `title`/`author` come from oEmbed.
### 2. Skip non-actionable URLs
- `accessible: false` → skip, note in report.
- `duplicate: true` → skip, note in report (already in a newsletter).
### 3. Dispatch on route
| route | Action |
|-------|--------|
| `article` | Invoke the **`mt-add-post`** skill, passing `clean_url` |
| `youtube` | Invoke the **`mt-add-video`** skill, passing `clean_url` |
| `image` / `video` / `document` / anything else | **Fallback** — see step 4 |
Dispatch by calling the Skill tool for the chosen handler with `clean_url` as the argument. The handler completes the write end-to-end.
**Multiple URLs:** dispatch **sequentially** (one handler finishes before the next starts) — handlers edit the same daily `index.md`, so concurrent edits would clobber each other. Process article(s) and video(s) one at a time.
### 4. Fallback for unsupported types
When `route` is not `article` or `youtube`, do NOT write anything automatically. Use `AskUserQuestion`:
- **Question:** "URL type `<route>` isn't supported yet (`<clean_url>`). How do you want to handle it?"
- **Options:**
- "Add a new skill" — e.g. create `mt-add-image` for images. (Recommended for a type you'll reuse.)
- "Update an existing skill" — extend a handler to support this URL type.
- "Skip this URL" — leave it out of the newsletter.
Act on the user's choice. If they choose add/update, proceed to design that skill change (or hand off to a planning step); do not silently route the URL into a post.
### 5. Final report
Aggregate across all URLs:
```
✅ Newsletter URL Dispatch Complete
✅ Dispatched: [count]
- [count] → mt-add-post (articles)
- [count] → mt-add-video (YouTube)
⏭️ Skipped: [count]
- [url]: duplicate / inaccessible
❓ Unsupported: [count]
- [url] (route: [route]): [user decision]
```
## Notes
- Handlers (`mt-add-post`, `mt-add-video`) remain directly invocable for single-purpose use, but `mt-add-url` is the normal entry point when a user pastes a URL.
- Shared mechanics (numbering, post find/create, Bonus insertion, language rules) are defined once in `references/newsletter-post-mechanics.md`; handlers reference it.
@@ -0,0 +1,95 @@
# Newsletter Post Mechanics (shared)
Shared procedure used by the newsletter handler skills (`mt-add-post`, `mt-add-video`, …).
All shared scripts live in `.claude/skills/mt-add-url/scripts/`.
**Project Context:**
- Hugo static site, theme `hugo-theme-stack`
- Language: Vietnamese (99%) + minimal English tech terms (1%)
- Timezone: Asia/Ho_Chi_Minh (UTC+7)
- Content path: `content/post/YYYY/MM/DD/index.md`
## 1. Find / create today's post
Get current date in `YYYY-MM-DD` (UTC+7). Check `content/post/YYYY/MM/DD/index.md`:
- **Exists** → update this file.
- **Missing** → create it (new newsletter number; template below). Create directories as needed.
## 2. Newsletter number
```bash
node .claude/skills/mt-add-url/scripts/find-newsletter-number.js
```
Searches backwards from today for the most recent newsletter and returns the next number. Only needed when **creating** a new post.
## 3. New post template
```markdown
---
title: "Newsletter #[number]"
date: YYYY-MM-DD
tags: ["AI-Assisted"]
categories: ["Newsletter"]
---
*Mời bạn thưởng thức Newsletter #[number].*
```
(Handlers then append their own content block — article body, or Bonus entry.)
## 4. Section insertion (no clobbering)
**Articles go before the `### Bonus` section; Bonus assets go inside it.**
To insert an article safely, anchor the Edit on `### Bonus` and prepend:
```
old_string: "### Bonus"
new_string: "## [New Article Title](clean_url)\n\n[Summary]\n\n**Điểm chính:**\n- ...\n\n### Bonus"
```
If the post has **no `### Bonus`** yet:
- Article → append at end of file. Do NOT create an empty Bonus section.
- Bonus asset (video/image/doc) → create the `### Bonus` section now (only because there's an asset to put in it).
**Bonus section format:**
```markdown
### Bonus
**Images:**
![title](url)
**Videos:**
[Tiêu đề video tiếng Việt](https://www.youtube.com/watch?v=ID)
> Tóm tắt 1-2 câu về nội dung video.
[Video: title](url) <!-- direct video file -->
**Documents:**
[PDF: title](url)
```
When a subsection (e.g. `**Videos:**`) already exists, append under it; otherwise create it.
## 5. Language guidelines
**Primary**: Vietnamese (99%) | **Secondary**: English (1% — unavoidable technical terms only)
**Allow English when no Vietnamese equivalent exists:**
- Technology: AI, API, GitHub, Rust, Go, Docker, Kubernetes
- Databases: PostgreSQL, MongoDB, Redis
- Companies: Google, Microsoft, Databricks
- Acronyms: MCP, CI/CD, JSON, SQL, HTTP
**Always translate to Vietnamese:**
- Common terms: code → mã nguồn, software → phần mềm, deployment → triển khai, performance → hiệu năng
- Action verbs: use → sử dụng, check → kiểm tra, build → xây dựng, test → kiểm thử
- Any other term: translate if a common Vietnamese equivalent exists
**Content requirements:** audience = junior developers; tone = professional, clear, accessible; verify content is ≥99% Vietnamese before saving.
## 6. Error handling
| Issue | Action |
|-------|--------|
| URL inaccessible | Skip, note in report |
| Duplicate URL | Skip, note in report |
| Extraction fails | Skip, treat as error |
| Directory missing | Create directories |
@@ -0,0 +1,107 @@
#!/usr/bin/env node
// Meta URL router for the mt-add-post skill — the single entry per URL.
// Usage: node add-url.js "<url>"
// Outputs: JSON { original_url, clean_url, http_status, accessible,
// duplicate, route, title?, author? }
// route ∈ youtube | image | video | document | article
const path = require("path");
const {
cleanUrl,
checkAccessibility,
checkDuplicate,
classifyType,
} = require("./url-utils");
const url = process.argv[2];
if (!url) {
console.error("Usage: node add-url.js <url>");
process.exit(1);
}
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
const CONTENT_DIR = path.join(PROJECT_ROOT, "content");
const YT_HOSTS = new Set(["youtube.com", "www.youtube.com", "m.youtube.com"]);
// Detect a YouTube video and extract its id from the supported URL shapes:
// youtube.com/watch?v=ID, youtu.be/ID, youtube.com/shorts/ID
// Playlists/channels are intentionally NOT YouTube routes (fall through to type).
function detectYouTube(targetUrl) {
try {
const p = new URL(targetUrl);
const host = p.host.toLowerCase();
if (host === "youtu.be") {
const id = p.pathname.slice(1).split("/")[0];
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
}
if (YT_HOSTS.has(host)) {
if (p.pathname === "/watch") {
const id = p.searchParams.get("v");
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
}
if (p.pathname.startsWith("/shorts/")) {
const id = p.pathname.split("/")[2];
return id ? { isYouTube: true, videoId: id } : { isYouTube: false };
}
}
return { isYouTube: false };
} catch {
return { isYouTube: false };
}
}
// Canonical watch URL — oEmbed accepts watch URLs reliably for all shapes.
function canonicalWatchUrl(videoId) {
return `https://www.youtube.com/watch?v=${videoId}`;
}
// Fetch title/author via YouTube oEmbed (no API key). Best-effort: any failure
// returns {} so the route stays `youtube` and the skill can fall back.
async function fetchYouTubeMeta(watchUrl) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const endpoint = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`;
const res = await fetch(endpoint, { redirect: "follow", signal: controller.signal });
clearTimeout(timeout);
if (!res.ok) return {};
const data = await res.json();
return { title: data.title, author: data.author_name };
} catch {
return {};
}
}
async function main() {
const cleanedUrl = cleanUrl(url);
const yt = detectYouTube(cleanedUrl);
// For YouTube, dedup/store against the canonical watch URL so youtu.be and
// shorts links collapse onto the same identity-param key as watch URLs.
const effectiveUrl = yt.isYouTube ? canonicalWatchUrl(yt.videoId) : cleanedUrl;
const route = yt.isYouTube ? "youtube" : classifyType(cleanedUrl);
const httpStatus = await checkAccessibility(cleanedUrl);
const accessible = httpStatus === "200";
const duplicate = checkDuplicate(effectiveUrl, CONTENT_DIR);
const out = {
original_url: url,
clean_url: effectiveUrl,
http_status: httpStatus,
accessible,
duplicate,
route,
};
if (route === "youtube") {
const meta = await fetchYouTubeMeta(effectiveUrl);
if (meta.title) out.title = meta.title;
if (meta.author) out.author = meta.author;
}
console.log(JSON.stringify(out, null, 2));
}
main();
@@ -1,20 +1,10 @@
#!/usr/bin/env node
// URL preparation script for mt-add-post skill
// Usage: node prepare-url.js "<url>"
// Outputs: JSON with status, clean_url, type, accessible, duplicate
// Shared URL helpers for the mt-add-post skill.
// Used by add-url.js (the meta router entry point).
// CommonJS so plain `node script.js` works without a build step.
const { execSync } = require("child_process");
const path = require("path");
const { execFileSync } = require("child_process");
const url = process.argv[2];
if (!url) {
console.error("Usage: node prepare-url.js <url>");
process.exit(1);
}
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
// Remove common tracking parameters
// Remove common tracking parameters (utm_* plus a fixed set of known trackers).
function cleanUrl(rawUrl) {
const EXACT_TRACKING = new Set([
"fbclid", "gclid", "msclkid", "mc_eid",
@@ -23,7 +13,6 @@ function cleanUrl(rawUrl) {
]);
try {
const parsed = new URL(rawUrl);
// Strip any utm_* param plus known exact trackers
[...parsed.searchParams.keys()].forEach((k) => {
if (k.toLowerCase().startsWith("utm_") || EXACT_TRACKING.has(k.toLowerCase())) {
parsed.searchParams.delete(k);
@@ -63,7 +52,7 @@ function bareUrl(targetUrl) {
}
}
// Check if URL is accessible (returns HTTP status code)
// Check if URL is accessible (returns HTTP status code as a string).
async function checkAccessibility(targetUrl) {
try {
const controller = new AbortController();
@@ -80,22 +69,24 @@ async function checkAccessibility(targetUrl) {
}
}
// Check if URL already exists in project content.
// Compare by bare URL (no query string) so stored copies with different tracking
// params still register as duplicates.
function checkDuplicate(targetUrl) {
// Check if URL already exists under contentDir.
// Compare by bare URL so stored copies with different tracking params
// still register as duplicates. contentDir is passed by the caller so this
// module stays decoupled from any specific project layout.
function checkDuplicate(targetUrl, contentDir) {
try {
const contentDir = path.join(PROJECT_ROOT, "content");
const needle = bareUrl(targetUrl);
execSync(`grep -rF "${needle}" "${contentDir}"`, { stdio: "pipe" });
// execFileSync (no shell) — the needle is URL-derived and could otherwise
// carry shell metacharacters; passing args directly avoids any injection.
execFileSync("grep", ["-rF", needle, contentDir], { stdio: "pipe" });
return true;
} catch {
return false;
}
}
// Classify URL type by extension
function classifyUrl(targetUrl) {
// Classify URL type by file extension. Returns image|video|document|article.
function classifyType(targetUrl) {
const lower = targetUrl.toLowerCase();
if (/\.(png|jpg|jpeg|gif|webp|svg)(\?.*)?$/.test(lower)) return "image";
if (/\.(mp4|webm|mov|avi)(\?.*)?$/.test(lower)) return "video";
@@ -103,22 +94,11 @@ function classifyUrl(targetUrl) {
return "article";
}
// Main
async function main() {
const cleanedUrl = cleanUrl(url);
const httpStatus = await checkAccessibility(cleanedUrl);
const accessible = httpStatus === "200";
const duplicate = checkDuplicate(cleanedUrl);
const type = classifyUrl(cleanedUrl);
console.log(JSON.stringify({
original_url: url,
clean_url: cleanedUrl,
http_status: httpStatus,
accessible,
duplicate,
type,
}, null, 2));
}
main();
module.exports = {
cleanUrl,
bareUrl,
IDENTITY_PARAMS,
checkAccessibility,
checkDuplicate,
classifyType,
};
+59
View File
@@ -0,0 +1,59 @@
---
name: mt-add-video
description: 'YouTube video handler for the Hugo blog newsletter. Adds a YouTube link to today''s newsletter Bonus → Videos section as a Vietnamese title link plus a 1-2 sentence Vietnamese summary. Normally invoked by the mt-add-url meta skill after classification, but can be used directly for a known YouTube URL. Handles YouTube links only (not direct video files).'
---
## Overview
`mt-add-video` is the **YouTube handler**: given a YouTube link, it adds the video to today's newsletter **Bonus → Videos** as a Vietnamese title link with a 1-2 sentence Vietnamese summary. It does **not** classify or route — that is `mt-add-url`'s job.
Scope: **YouTube links only** (`watch`, `youtu.be`, `shorts`). Direct video files (`.mp4` etc.) are not handled here — they go through `mt-add-url`'s fallback.
Shared scripts: `.claude/skills/mt-add-url/scripts/`. Shared procedure: `../mt-add-url/references/newsletter-post-mechanics.md`**follow it** for post find/create, numbering, Bonus insertion, and language rules.
## Input
A clean YouTube URL (passed by `mt-add-url`, or given directly).
## Workflow
1. **Classify / fetch title** — run the router to get the canonical URL + title:
```bash
node .claude/skills/mt-add-url/scripts/add-url.js "<url>"
```
Confirm `route: youtube`; skip if `duplicate` or not `accessible`. Use the returned `clean_url` (canonical `watch?v=ID`) and `title`.
- If `title` is missing (oEmbed failed), fetch the title via WebFetch on the watch URL.
2. **Localize title** — render the title in Vietnamese where natural; keep proper nouns (product/company/channel names) as-is.
3. **Write a 1-2 sentence Vietnamese summary.** Source for the summary content, in order:
- WebFetch the watch URL for the video description.
- If thin/unavailable, optionally use the `ai-multimodal` (Gemini) skill to analyze the video — only if available.
- Worst case, derive a single sentence from the title.
Keep it to 1-2 sentences (KISS).
4. **Post mechanics** — follow `../mt-add-url/references/newsletter-post-mechanics.md` to find/create today's post and locate the Bonus section.
5. **Insert under Bonus → Videos:**
```markdown
**Videos:**
[Tiêu đề video tiếng Việt](https://www.youtube.com/watch?v=ID)
> Tóm tắt 1-2 câu về nội dung video.
```
- If `### Bonus` doesn't exist yet, create it (there's now an asset to put in it).
- If `**Videos:**` already exists, append the new entry under it; otherwise create the `**Videos:**` subsection.
6. **Report:**
```
✅ Video added to Newsletter #[number] (Bonus → Videos)
📄 content/post/YYYY/MM/DD/index.md
🎬 [Vietnamese title] — [clean_url]
```
## Checklist
- [ ] `route: youtube` confirmed; not a duplicate
- [ ] Title in Vietnamese (proper nouns kept)
- [ ] 1-2 sentence Vietnamese summary present
- [ ] Entry under Bonus → **Videos** (section/subsection created if missing)
- [ ] Vietnamese content ≥99%
+5 -1
View File
@@ -38,9 +38,13 @@ The site will be available at `http://localhost:1313`
This project uses custom skills for automated workflows:
- **mt-add-post**: URL processing workflow for newsletter posts
- **mt-add-url**: Meta entry for adding URLs — classifies each URL and auto-dispatches to the right handler (the default entry when adding URLs to the newsletter)
- **mt-add-post**: Article handler — adds an article/blog URL to the newsletter main content
- **mt-add-video**: YouTube handler — adds a YouTube link to the newsletter Bonus → Videos
- **mt-add-tags**: Add/update tags in Hugo post frontmatter
Shared scripts (`add-url.js`, `url-utils.js`, `find-newsletter-number.js`) and the shared post-mechanics reference live under `.claude/skills/mt-add-url/`. Image support is planned as a future `mt-add-image` handler; until then `mt-add-url` asks how to handle non-article/non-YouTube URLs.
Refer to `.claude/skills/` for skill implementations.
---