@lobehub/seo-cli
Version:
Lobe seo is a CLI tool that automate generation seo content for mdx
55 lines (43 loc) • 13 kB
JavaScript
var I=Object.defineProperty;var r=(o,e)=>I(o,"name",{value:e,configurable:!0});var j=(o,e)=>()=>(e||o((e={exports:{}}).exports,e),e.exports);import{jsx as f}from"react/jsx-runtime";import{alert as g,TextInput as x,ConfigPanel as N,render as U}from"@lobehub/cli-ui";import{Command as K,Option as C}from"commander";import F from"update-notifier";import D from"conf";import{cosmiconfigSync as L}from"cosmiconfig";import{memo as P,useState as R,useMemo as $}from"react";import l from"chalk";import J from"dotenv";import{merge as z}from"lodash-es";import{consola as p}from"consola";import{globSync as q}from"glob";import S from"gray-matter";import W from"p-map";import B from"dirty-json";import V from"openai";import{encode as _}from"gpt-tokenizer";import{writeFileSync as H,readFileSync as Q}from"node:fs";import{join as Y}from"node:path";var Pe=j((k,b)=>{var G="@lobehub/seo-cli",X="1.6.0",Z="Lobe seo is a CLI tool that automate generation seo content for mdx",ee=["ai","seo","mdx","openai","gpt"],te="https://github.comlobehub/lobe-cli-toolbox/tree/master/packages/lobe-seo",oe={url:"https://github.com/lobehub/lobe-cli-toolbox/issues/new"},ne={type:"git",url:"https://github.com/lobehub/lobe-cli-toolbox.git"},re="MIT",ie="LobeHub <i@lobehub.com>",se=!1,ae="module",ce={"@":"./src"},k={require:{types:"./dist/index.d.cts",default:"./dist/index.cjs"},import:{types:"./dist/index.d.mts",default:"./dist/index.mjs"}},le="./dist/index.cjs",b="./dist/index.mjs",ge="./dist/index.d.cts",pe={"lobe-seo":"dist/cli.js"},de=["dist"],ue={build:"npm run type-check && pkgroll --minify -p tsconfig.prod.json --env.NODE_ENV=production && npm run shebang",dev:"pkgroll -p tsconfig.prod.json --env.NODE_ENV=development --watch",link:"npm run build && npm link -f",prepack:"clean-package",postpack:"clean-package restore",shebang:"lobe-shebang -t ./dist/cli.js",start:"node ./dist/cli.js",test:"vitest --passWithNoTests","test:coverage":"vitest run --coverage --passWithNoTests","type-check":"tsc --noEmit"},fe={"@lobehub/cli-ui":"1.12.0",chalk:"^5.4.1",commander:"^13.0.0",conf:"^13.1.0",consola:"^3.3.3",cosmiconfig:"^9.0.0","dirty-json":"^0.9.2",dotenv:"^16.4.7","fast-deep-equal":"^3.1.3",glob:"^10.4.5","gpt-tokenizer":"^2.8.1","gray-matter":"^4.0.3",ink:"^6.0.0","json-stable-stringify":"^1.2.1","just-diff":"^6.0.2","lodash-es":"^4.17.21",openai:"^4.103.0","p-map":"^7.0.3",pangu:"^4.0.7",react:"^19.0.0","remark-frontmatter":"^5.0.0","remark-gfm":"^4.0.0","remark-parse":"^11.0.0","remark-stringify":"^11.0.0",swr:"^2.3.0",unified:"^11.0.5","unist-util-visit":"^5.0.0","update-notifier":"^7.3.1",zustand:"^5.0.3"},me={"@types/json-stable-stringify":"^1.1.0","clean-package":"^2.2.0"},he="pnpm@10.10.0",ye={node:">=18"},ve={access:"public",registry:"https://registry.npmjs.org/"},h={name:G,version:X,description:Z,keywords:ee,homepage:te,bugs:oe,repository:ne,license:re,author:ie,sideEffects:se,type:ae,imports:ce,exports:k,main:le,module:b,types:ge,bin:pe,files:de,scripts:ue,dependencies:fe,devDependencies:me,packageManager:he,engines:ye,publishConfig:ve};const ke="o4-mini",be={concurrency:5,entryExtension:".mdx",modelName:ke,temperature:0},we=r((o,e)=>{o[e]||g.error(`Can't find ${l.bold.yellow("outputLocales")} in config`)},"checkOptionKeys"),O={apiBaseUrl:{default:"",type:"string"},openaiToken:{default:"",type:"string"}},y=new D({projectName:"lobe-seo",schema:O});class xe{static{r(this,"ExplorerConfig")}explorer;customConfig;constructor(){this.explorer=L("seo")}loadCustomConfig(e){this.customConfig=e}getConfigFile(){return this.customConfig?this.explorer.load(this.customConfig)?.config:this.explorer.search()?.config}}const E=new xe;J.config();const v=r(o=>y.get(o),"getConfig"),Ce=r(o=>O[o].default,"getDefulatConfig"),Se=r((o,e)=>y.set(o,e),"setConfig"),Oe=r(()=>process.env.OPENAI_API_KEY||v("openaiToken"),"getOpenAIApiKey"),Ee=r(()=>process.env.OPENAI_PROXY_URL||v("apiBaseUrl"),"getOpenAIProxyUrl"),T=r(()=>{const o=E.getConfigFile();return o?z(be,o):g.error(`Can't find ${l.bold.yellow("config")}`,!0)},"getConfigFile"),Te=r(()=>{const o=T();return we(o,"entry"),o},"getSeoConfig");var u={getConfig:v,getConfigFile:T,getDefulatConfig:Ce,getOpenAIApiKey:Oe,getOpenAIProxyUrl:Ee,getSeoConfig:Te,setConfig:Se};const Ae=r(()=>{const o=y.store;return{get:u.getConfig,getDefault:u.getDefulatConfig,set:u.setConfig,store:o}},"useConfStore"),Me=P(()=>{const[o,e]=R(),{store:t,set:n,getDefault:i}=Ae(),s=r((c,d)=>{n(c,d),e("")},"setConfig"),a=$(()=>[{children:f(x,{defaultValue:t.openaiToken,onSubmit:r(c=>s("openaiToken",c),"onSubmit"),placeholder:"Input OpenAI token..."}),defaultValue:i("openaiToken"),key:"openaiToken",label:"OpenAI token",showValue:!1,value:t.openaiToken},{children:f(x,{defaultValue:t.apiBaseUrl,onSubmit:r(c=>s("apiBaseUrl",c),"onSubmit"),placeholder:"Set openAI API proxy, default value: https://api.openai.com/v1/..."}),defaultValue:i("apiBaseUrl"),desc:"OpenAI API proxy, default value: https://api.openai.com/v1/",key:"apiBaseUrl",label:"OpenAI API proxy",showValue:!1,value:t.apiBaseUrl}],[t]);return f(N,{active:o,items:a,logo:"\u{1F92F}",setActive:e,title:"Lobe SEO Config"})});class w{static{r(this,"ChatPromptTemplate")}messages;constructor(e){this.messages=e}static fromMessages(e){const t=e.map(([n,i])=>({content:i,role:n}));return new w(t)}async formatMessages(e){return this.messages.map(t=>({content:this.formatString(t.content,e),role:t.role}))}formatString(e,t){let n=e;for(const[i,s]of Object.entries(t))if(s!==void 0){const a=new RegExp(`\\{${i}\\}`,"g");n=n.replace(a,s)}return n}}const Ie=`
## Rules
1. **Maintain Content Relevance**: Ensure the generated titles, descriptions, and tags are highly relevant to the article content.
2. **Avoid Keyword Stuffing**: Use keywords naturally in titles, descriptions, and tags, avoiding over-optimization.
3. **Length of Titles and Descriptions**: Descriptions are recommended to be around 30-40 characters, and descriptions should be around 100 characters.
### Title (Title)
- **Include Keywords**: Ensure the title contains target keywords but avoids keyword stuffing.
- **Uniqueness**: Write a unique title for each page.
- **Length Optimization**: Keep the title length moderate, usually recommended to be between 30-40 characters.
- **Written for Humans**: While the title needs to be search engine friendly, it ultimately needs to attract human users.
- **Consider Format**: Titles with clear formats are easier to understand and click.
- **Similarity to H1 Tag**: Ensure the title is similar to the page's H1 tag for consistency.
### Description (Description)
- **Include Keywords**: Include target keywords in the description, ensuring it flows naturally.
- **Clear Value**: The description should clearly articulate the page's value and what it offers.
- **Click-Worthy**: Write descriptions that are compelling and enticing enough to generate clicks, concise yet attractive.
- **Length Control**: Keep the description length around 100 characters.
### Tags (Tags)
- **Keyword Relevance**: Tags should be highly relevant to the content, including target keywords.
- **Avoid Over-Optimization**: Avoid using keywords excessively for SEO, keeping tags natural and relevant.
- **Diversity**: Use a variety of tags to cover a broader range of potential search queries.
`,je=r(o=>w.fromMessages([["system",`# Role: Markdown SEO Expert
## Profile
As a Markdown SEO expert, I specialize in converting Markdown-formatted article content into JSON format matter data optimized for SEO. My goal is to enhance articles' online visibility and search engine rankings through carefully crafted Titles, Descriptions, and Tags, ensuring each article achieves optimal SEO performance.
## Expertise:
1. **Analyzing Markdown Articles**: Understanding and analyzing the content of Markdown-formatted articles to extract key information.
2. **Creating SEO-friendly Titles**: Crafting titles that include target keywords and are enticing enough to generate user clicks, based on the article content.
3. **Writing Compelling Descriptions**: Writing descriptions that include keywords, are concise, and based on the article's theme.
4. **Selecting Appropriate Tags**: Choosing tags that are highly relevant to the article's theme and content.
${o||Ie}
## The structure for generating SEO JSON format matter is as follows:
\`\`\`json
"title": "Your Page Title - Including Main Keyword",
"description": "Concisely describe the page content, including keywords, to attract user clicks.",
"tags": ["Main Keyword", "Related Keyword 1", "Related Keyword 2"]
\`\`\`
## Workflow
1. Users provide Markdown-formatted article content.
2. Analyze the article content to extract key information and concepts.
3. The output seo json language matches the provided markdown original language (if the original text is in Chinese, the seo content will also be in Chinese):
4. Based on the extracted information, generate JSON format matter data for SEO, including title, description, and tags.`],["user","{content}"]]),"promptSeo");class Ne{static{r(this,"SeoMdx")}client;config;isJsonMode;prompt;constructor(e,t,n){this.config=e,this.client=new V({apiKey:t,baseURL:n,maxRetries:4}),this.prompt=je(this.config.reference),this.isJsonMode=!!this.config?.experimental?.jsonMode}async run(e){try{const t=await this.prompt.formatMessages({content:e}),i=(await this.client.chat.completions.create({messages:t,model:this.config.modelName||"gpt-3.5-turbo",temperature:this.config.temperature,...this.isJsonMode&&{response_format:{type:"json_object"}}})).choices[0]?.message?.content;i||this.handleError();try{return JSON.parse(i)}catch{g.warn("parse fail, try to use dirty json");try{return B.parse(i)}catch{g.error("seo dirty json fail"),g.error(i,!0)}}}catch(t){this.handleError(t)}}handleError(e){g.error(`Seo failed, ${e||"please check your network or try again..."}`,!0)}}const A=r(o=>_(o).length,"calcToken");class Ue{static{r(this,"SeoCore")}seoService;config;constructor({openAIApiKey:e,openAIProxyUrl:t,config:n}){this.config=n,this.seoService=new Ne(n,e,t)}async run({content:e,onProgress:t,matter:n}){const i=await this.seoService.prompt.formatMessages({content:e}),s=A(JSON.stringify(i));t?.({isLoading:!0,needToken:s});const a=await this.seoService.run(e);t?.({isLoading:!1,needToken:s});let c;return this.config.tagStringify&&(a.tags=a.tags.join(", ")),c=this.config.groupKey?{...n,[this.config.groupKey]:{...a,...n?.[this.config.groupKey]}}:{...a,...n},{result:c,tokenUsage:s+A(JSON.stringify(a))}}}const Ke=r(o=>Q(o,"utf8"),"readMarkdown"),Fe=r((o,e)=>{H(o,e,"utf8")},"writeMarkdown"),M=r((o,e)=>o.map(t=>t.includes("*")||t.includes(e)?t:Y(t,`**/*${e}`).replaceAll("\\","/")),"matchInputPattern");class De{static{r(this,"Seo")}config;query=[];seo;constructor(){this.config=u.getSeoConfig(),this.seo=new Ue({config:this.config,openAIApiKey:u.getOpenAIApiKey(),openAIProxyUrl:u.getOpenAIProxyUrl()})}async start(){p.start("Lobe Seo is analyzing your mdx... \u{1F92F}\u{1F30F}\u{1F50D}");const e=this.config.entry;(!e||e.length===0)&&g.error("No mdx entry was found.",!0);let t=q(M(e,this.config.entryExtension),{ignore:M(this.config.exclude||[],this.config.entryExtension),nodir:!0}).filter(n=>n.includes(this.config.entryExtension));(!t||t.length===0)&&g.error("No mdx entry was found.",!0),this.genFilesQuery(t),this.query.length>0?await this.runQuery():p.success("No content requiring seo was found."),p.success("All seo tasks have been completed\uFF01")}async runQuery(){p.info(`Current model setting: ${l.cyan(this.config.modelName)} (temperature: ${l.cyan(this.config.temperature)}) ${this.config.experimental?.jsonMode?l.red(" [JSON Mode]"):""}}`);let e=0;await W(this.query,async t=>{const n=await this.seo.run({...t,onProgress:r(({isLoading:i})=>{i&&p.start(t.entry)},"onProgress")});if(n?.result&&Object.keys(n.result).length>0){const i=S.stringify(t.rawContent,n.result);Fe(t.entry,i),e+=n.tokenUsage,p.success(l.yellow(t.entry),l.gray(`[Token usage: ${n.tokenUsage}]`))}else p.warn("No translation result was found:",l.yellow(t.entry))},{concurrency:this.config.concurrency||5}),e>0&&p.info("Total token usage:",l.cyan(e))}genFilesQuery(e,t){t||p.start(`Running in ${l.bold.cyan(`\u{1F4C4} ${e.length} Mdx`)}..`);for(const n of e)try{const i=Ke(n),{data:s,content:a}=S(i);let c=a;if(s){let d;if(d=this.config.groupKey?s?.[this.config.groupKey]:s,d&&d.tags&&d.title&&d.description)continue}this.config.groupKey&&s?.title&&(c=[`#${s?.title}`,a].join(`
`)),this.query.push({content:c,entry:n,matter:s||{},rawContent:a})}catch{g.error(`${n} not found`,!0)}}}const Le=F({pkg:h,shouldNotifyInNpmScript:!0});Le.notify({isGlobal:!0});const m=new K;m.name("lobe-seo").description(h.description).version(h.version).addOption(new C("-o, --option","Setup lobe-seo preferences")).addOption(new C("-c, --config <string>","Specify the configuration file")),m.command("locale",{isDefault:!0}).action(async()=>{const o=m.opts();o.option?U(f(Me,{})):(o.config&&E.loadCustomConfig(o.config),await new De().start())}),m.parse()});export default Pe();