Trade With Viet
Session Explainer
Internal use only — enter access password
launch

TWV SEO Strategy + Content Automation + VietConnect Phase 5

2026-06-10 · explainer.tradewithviet.com

Full SEO + content automation pipeline shipped for Trade With Viet

Two properties (tradewithviet.com WordPress + vietconnect.tradewithviet.com Next.js) now have a 12-week content calendar, a weekly auto-publish pipeline to WordPress + social via Buffer, 6 new industry landing pages, a blog section, and structured data throughout. Human time once running: under 10 minutes per week.

StatusLive (partial — 4 manual steps remain)ScopeTWV WordPress + VietConnect Next.jsDate2026-06-10Buildnpm run build ✓ 236 pages

6-layer thinking chain

L1
Raw request
"đánh giá SEO website; lên cho tôi chiến lược seo và nội dung bài đăng content cho web trade with viet, cũng như tự động hoá lịch trình đăng các nền tảng… tự động toàn bộ"
L2
Reframe — what was really needed
The real ask was not just an SEO audit — it was building a passive demand-generation engine. The consultancy (TWV) feeds the platform (VietConnect), which is investment-ready. Organic SEO + weekly auto-published content = the flywheel that makes lead generation passive. The audit was just the starting point; the publishing pipeline and the platform SEO enhancements were the actual deliverables.
L3
Constraints
• Two domains with different stacks (WordPress + Next.js) cannot share a canonical URL — one must own each piece of content • Buffer requires a separate org + token for TWV (cannot reuse BEUP's token — different billing, different channels) • Next.js 16.2 has breaking changes from training data — AGENTS.md mandates reading node_modules/next/dist/docs/ before writing any route code • Google Search Console service account must be manually added by the domain owner — cannot be automated • Hostinger shared hosting routes all subdomains through the same LiteSpeed vhost as the main domain — subdomain document root cannot be changed via SSH alone
L4
Options weighed
Option A — Build everything new from scratch. Cost: 40+ hours, high risk of divergence from BEUP patterns already proven. Option B — Fork BEUP scripts and maintain two codebases. Cost: doubles maintenance burden, drift guaranteed over time. Option C — Reuse BEUP scripts unchanged, swap only config files. Cost: requires scripts to be domain-agnostic (verified: they are — gsc.py, pagespeed.py, etc. all read from config.json). Option D (chosen) — Reuse scripts as-is + adapt WP/Buffer/n8n layers for TWV-specific env vars and brand. Cost: minimal — only wrapper scripts and SKILL.md files need to change.
L5
Principle invoked
Reuse before build: if an existing tool is domain-agnostic, a config swap is always cheaper than a fork.
L6
Pick + recognition signal
Chose Option D. BEUP SEO scripts (gsc.py, pagespeed.py, onsite_seo.py, score.py, render.py) used verbatim with two new config files (config-twv.json, config-vietconnect.json). WP automation adapted into wp_twv.py (stripped TRP + Meta direct, parametrized env). Buffer isolation enforced: TWV_BUFFER_TOKEN separate env var, never sharing BEUP's BUFFER_TOKEN. Rejected: fork — would have created two versions of 5 scripts to maintain with no benefit. Recognition signal: next time you see a domain-agnostic tool with a config file, the answer is always a new config, not a new tool.

Two-domain SEO system

WordPress owns informational content (blog, guides, brand pages) — it is the SEO canonical. Next.js owns commercial content (supplier directory, industry pages, RFQ, tools). Both point to each other via internal links. The automation pipeline publishes to WordPress first, then queues social posts via Buffer. Next.js blog carries rel=canonical pointing to the WordPress URL so Google knows which one to index.

WordPress Blogtradewithviet.comSEO Canonical OwnerVietConnectvietconnect.tradewithviet.comCommercial / DirectoryBufferTWV org (separate token)LinkedIn · Facebook · Instagramn8n WorkflowMon 07:00 ICT → Telegram notifytwv-publish skillhumanizer → WP → Buffer queuerel=canonical →
Technical termPlain nameRole
wp_twv.pyWP publish botCreates future-dated blog posts on tradewithviet.com via WP REST API; sets AIOSEO meta title/description; uploads images
humanizer-lint.py + twv-voice.jsonCopy quality gateScans draft for banned AI phrases (seamlessly, cutting-edge, robust…) — exit 1 blocks publish
getSuppliersByCategory()Industry filterSupabase query: finds category ID by slug, inner-joins supplier_categories, returns top N by verification_score
ISR (Incremental Static Regeneration)Self-refreshing static pageIndustry + blog pages pre-built at deploy, auto-refresh every 24h — no server needed per request
rel=canonicalSEO ownership tagTells Google: this Next.js blog URL is a mirror — the real page is at tradewithviet.com/blog/…

Content flows left to right: draft → humanizer gate → WordPress (canonical) → Buffer social queue. VietConnect mirrors blog content but points back to WordPress via canonical tag. n8n triggers the weekly reminder; /twv-publish skill handles the actual publish steps.

Lesson to carry forward: When two domains share content, assign canonical ownership before writing a line of code — the architecture follows the SEO decision, not the other way around.

What was built, in order

1
SEO audit configs
config-twv.json + config-vietconnect.json for both domains. BEUP scripts (gsc.py, pagespeed.py, onsite_seo.py) reused unchanged.
2
TWV automation scripts
wp_twv.py (WP publish), twv-preflight.py (env check), twv-voice.json (humanizer config), calendar-twv.md, 12 draft stubs
3
Skills + n8n
~/.claude/skills/twv-publish/SKILL.md (7-step pipeline), twv-seo-ux/SKILL.md (weekly dashboard), n8n twv-weekly-publish.json (Mon 07:00 ICT Telegram reminder)
4
VietConnect blog (/blog)
next-mdx-remote + gray-matter installed. getAllPosts(), getPostBySlug(). /blog index + /blog/[slug] with Article JSON-LD + canonical → WP
5
6 industry landing pages
/suppliers/apparel-textiles, /suppliers/food-agro-natural, /suppliers/home-lifestyles, /suppliers/industrial-metal, /suppliers/medical-health, /suppliers/personal-care-household. Static routes, ISR 24h, BreadcrumbList + ItemList JSON-LD
6
getSuppliersByCategory()
Added to server-queries.ts. Finds category by slug → inner join supplier_categories → top N by verification_score + total count
7
FAQ JSON-LD on /pricing
Injected via pricing/layout.tsx (server component wrapper) since page.tsx is 'use client'
8
Sitemap + nav + footer
sitemap.ts includes blog posts + 6 industry pages. Header: Blog link + Industries dropdown. Footer: Industries column added.
9
/explainer skill
Adapted from beup-explain. 6-layer reasoning, spec JSON → build-explainer-twv.py → deploy to explainer.tradewithviet.com via SSH

Layered decision cards

WordPress = SEO canonical, not Next.js
L1"có 2 domain, blog đăng ở đâu?"
L2Two URLs serving the same blog post = duplicate content penalty from Google. One must be declared the owner; the other must point to it.
L3WordPress is already live and indexed by Google. Next.js is newer, not yet indexed for blog. Cannot un-index WP without losing existing ranking.
L4Option A: WP canonical (chosen) — Next.js mirrors with rel=canonical pointing to WP. Option B: Next.js canonical — WP redirects. Risk: lose WP's existing index. Option C: Pick per-post. Cost: unmaintainable. Option D: No canonical — let Google pick. Cost: penalty guaranteed.
L5Canonical ownership must be assigned once and locked — Google does not forgive inconsistency.
L6Chose WP as canonical. Next.js /blog/[slug] sets alternates.canonical = post.wpUrl in generateMetadata(). Rejected Next.js canonical: WP already has search history and backlinks. Recognition signal: whenever two domains share content, ask 'which was indexed first?' before writing any code.
Static routes for industry pages, not a new dynamic segment
L1"tạo 6 industry landing pages dưới /suppliers/[category]"
L2There is already a /suppliers/[slug] dynamic route handling individual supplier profiles. A new [category-slug] segment would conflict with it.
L3Next.js App Router: two dynamic segments at the same path level cannot coexist. The 6 categories are fixed — they will not change frequently. Static routes take precedence over dynamic ones.
L4Option A: 6 static route directories (apparel-textiles/page.tsx, etc.) — chosen. Option B: New dynamic segment [category]/page.tsx alongside [slug]/page.tsx — impossible, collision. Option C: Route group (categories)/[slug] — changes URL structure. Option D: Middleware to detect category vs supplier slug — fragile.
L5Static takes precedence over dynamic in Next.js App Router — use that to your advantage when the set is small and known.
L6Created 6 static directories, each page.tsx delegating to shared IndustryLandingPage component. Rejected dynamic segment: would require restructuring existing [slug] route. Recognition signal: fewer than 10 fixed routes = static files, not a dynamic segment.
Buffer: TWV must use a separate org + token
L1"BEUP dùng Buffer rồi, TWV dùng luôn không?"
L2Buffer pricing is per-organization. Mixing TWV and BEUP content in one org means one billing account, shared channel limits, and no separation of brand voice or analytics.
L3Buffer API token is scoped to an organization. A token from BEUP org cannot post to TWV channels. TWV needs its own LinkedIn/FB/IG channels connected.
L4Option A: Share BEUP org — simpler. Risk: TWV posts appear in BEUP analytics, channel limits shared, brand separation lost. Option B (chosen): Separate TWV Buffer org with TWV_BUFFER_TOKEN env var. Cost: extra setup, user must connect 3 channels manually.
L5Isolate at the org boundary: never share a third-party API token between two distinct brands.
L6TWV_BUFFER_TOKEN is a separate env var, never aliased to BEUP's BUFFER_TOKEN. Both skill and n8n reference TWV_BUFFER_TOKEN explicitly. Recognition signal: if two brands share a tool account, ask whether analytics and limits need to be separate — if yes, separate orgs.
n8n v1 = human-in-loop, not full auto
L1"tự động toàn bộ"
L2Full automation without a human review step risks publishing AI-generated content that fails the humanizer gate or contains factual errors, directly under the TWV brand.
L3Humanizer gate can exit 1 (block) on any draft. First 3 runs of a new pipeline always surface edge cases. Buffer channel IDs not yet confirmed. Trust must be built before removing the human from the loop.
L4Option A: Full auto — n8n publishes without human. Risk: first failure is live and public. Option B (chosen): Telegram reminder → human runs /twv-publish. Cost: 5 min/week manual step. Option C: Auto with email approval gate. Cost: more complex n8n flow, email dependency.
L5Earn autonomy: run 3 successful human-approved cycles before removing the approval step.
L6n8n v1 sends Telegram reminder on Monday 07:00 ICT. Human reviews draft and runs /twv-publish. Upgrade to full-auto after 3 clean runs. Recognition signal: any new publish pipeline targeting a public brand channel should start human-in-loop.
Explainer subdomain: .htaccess bypass instead of hPanel document root
L1"sao click vào mà nó ko dẫn tới explainer mà về lại trade with viet"
L2Hostinger shared hosting routes all subdomains through the same LiteSpeed vhost as the main domain. Creating a directory via SSH does not change the web server routing — only hPanel can do that, and the user's subdomain was mapped to the WP document root.
L3Cannot change nginx/LiteSpeed vhost config on shared hosting. WordPress PHP was executing for the subdomain request and redirecting to its configured home URL. The .htaccess in WP root IS processed for subdomain requests.
L4Option A: Change document root in hPanel — requires finding correct setting, user reported it as confusing. Option B (chosen): Add .htaccess rule to detect HTTP_HOST = explainer.tradewithviet.com and serve a static file directly, bypassing WP PHP entirely. Option C: WordPress plugin to handle subdomain. Cost: overkill. Option D: Move to separate hosting. Cost: too much.
L5When you cannot change routing at the server level, change it at the .htaccess level — the web server processes it before PHP runs.
L6Added RewriteCond %{HTTP_HOST} ^explainer\.tradewithviet\.com rule at top of WP .htaccess. File served as explainer-session.html in WP public_html. WP PHP never runs. Recognition signal: shared hosting + subdomain redirect = check .htaccess before opening support ticket.

Artifact map

PathWhatWho reads it
scripts/twv-automation/wp_twv.pyWP REST publisher — create posts, upload media, queue Buffertwv-publish skill
scripts/twv-automation/twv-preflight.pyPre-run environment check — WP creds, Buffer token, dirs, voice confign8n, twv-publish skill
scripts/twv-automation/humanizer/twv-voice.jsonBanned phrases + specificity rules for TWV brand voicehumanizer-lint.py gate
scripts/twv-automation/seo/config-twv.jsonSEO audit config for tradewithviet.comtwv-seo-ux skill
scripts/twv-automation/seo/config-vietconnect.jsonSEO audit config for vietconnect.tradewithviet.comtwv-seo-ux skill
scripts/twv-automation/content/calendar-twv.md12-week publish calendar (2026-06-16 → 2026-09-01)Editorial reference
scripts/twv-automation/content/drafts/ (12 files)12 blog draft stubs with body + LI/FB/IG captionstwv-publish skill input
scripts/twv-automation/n8n/twv-weekly-publish.jsonn8n workflow — Mon 07:00 ICT cron → preflight → Telegram notifyn8n import
~/.claude/skills/twv-publish/SKILL.md7-step publish pipeline: validate → images → humanizer → WP → Buffer → archive → report/twv-publish command
~/.claude/skills/twv-seo-ux/SKILL.mdWeekly SEO dashboard for both domains (Friday 15:00 ICT)/twv-seo-ux command
src/lib/blog/index.tsgetAllPosts() + getPostBySlug() — reads MDX files from content/blog//blog and /blog/[slug] pages
src/app/blog/page.tsxBlog index page — lists all posts, ISR 1hNext.js SSG
src/app/blog/[slug]/page.tsxBlog post page — MDXRemote, Article JSON-LD, rel=canonical → WP, ISR 24hNext.js SSG
content/blog/complete-guide-sourcing-vietnam-2026.mdxFirst blog post (Week 1 of 12)Blog reader
src/components/industry-landing-page.tsxShared server component for all 6 industry pages — supplier grid, key facts, JSON-LD6 industry page routes
src/app/suppliers/apparel-textiles/page.tsx (×6)6 static industry landing pages — ISR 24h, BreadcrumbList + ItemList JSON-LDSEO, buyers
src/lib/supabase/server-queries.tsAdded getSuppliersByCategory() — category slug → inner join → top N suppliersIndustry landing pages
src/app/pricing/layout.tsxAdded FAQPage JSON-LD (workaround: page.tsx is use client)Google Rich Results
src/app/sitemap.tsAdded blog posts + 6 industry pages to sitemapGoogle Search Console
src/components/layout/header.tsxAdded Blog nav link + Industries dropdown (desktop + mobile)All pages
src/components/layout/footer.tsxAdded Industries column with 6 category linksAll pages
~/.claude/skills/explainer/SKILL.mdThis skill — session explainer builder + Hostinger deploy/explainer command
~/.claude/skills/explainer/scripts/build-explainer-twv.pySpec JSON → self-contained HTML with password gateexplainer skill
/home/u166949001/domains/tradewithviet.com/public_html/.htaccessAdded explainer subdomain bypass rule (routes explainer.tradewithviet.com → static HTML, skips WP PHP)Hostinger LiteSpeed
Pending manual actions — required before fully live
  • Google Search Console: add beup-n8n-analytics@gws-490608.iam.gserviceaccount.com as Owner for tradewithviet.com property — needed before /twv-seo-ux skill can pull keyword data
  • Buffer: create a new TWV org, connect LinkedIn + Facebook + Instagram, generate TWV_BUFFER_TOKEN, add to .env
  • Update 3 channel ID placeholders in ~/.claude/skills/twv-publish/SKILL.md: REPLACE_WITH_TWV_LI_CHANNEL_ID, REPLACE_WITH_TWV_FB_CHANNEL_ID, REPLACE_WITH_TWV_IG_CHANNEL_ID
  • n8n: import scripts/twv-automation/n8n/twv-weekly-publish.json, set TWV_TELEGRAM_CHAT_ID variable, test trigger manually
  • Set GA4 property IDs in scripts/twv-automation/seo/config-twv.json and config-vietconnect.json
  • Add TWV_WP_USER, TWV_WP_PASS, TWV_WP_BASE_URL, TWV_BUFFER_TOKEN to .env.local

Check your understanding

Why does the Next.js blog at vietconnect.tradewithviet.com/blog/[slug] set rel=canonical pointing to tradewithviet.com?
Why were 6 static route directories created (/suppliers/apparel-textiles/page.tsx etc.) instead of a single dynamic /suppliers/[category-slug]/page.tsx?
In your own words: why is TWV_BUFFER_TOKEN a separate env var and not an alias to BEUP's BUFFER_TOKEN?
Mastery checklist — tick what you can explain unprompted