UNPKG

@lobehub/seo-cli

Version:

Lobe seo is a CLI tool that automate generation seo content for mdx

54 lines (42 loc) 12.4 kB
"use strict";var I=Object.defineProperty;var r=(t,e)=>I(t,"name",{value:e,configurable:!0});var f=require("react/jsx-runtime"),d=require("@lobehub/cli-ui"),m=require("commander"),j=require("update-notifier"),q=require("conf"),M=require("cosmiconfig"),w=require("@inkjs/ui"),y=require("react"),l=require("chalk"),N=require("dotenv"),U=require("lodash-es"),u=require("consola"),F=require("glob"),x=require("gray-matter"),K=require("p-map"),D=require("@langchain/openai"),L=require("@langchain/core/prompts"),P=require("gpt-tokenizer"),C=require("node:fs"),$=require("node:path"),R="@lobehub/seo-cli",z="1.4.2",J="Lobe seo is a CLI tool that automate generation seo content for mdx",_=["ai","seo","mdx","openai","gpt","langchain"],W="https://github.comlobehub/lobe-cli-toolbox/tree/master/packages/lobe-seo",B={url:"https://github.com/lobehub/lobe-cli-toolbox/issues/new"},V={type:"git",url:"https://github.com/lobehub/lobe-cli-toolbox.git"},H="MIT",Q="LobeHub <i@lobehub.com>",Y=!1,G="module",X={"@":"./src"},Z={require:{types:"./dist/index.d.cts",default:"./dist/index.cjs"},import:{types:"./dist/index.d.mts",default:"./dist/index.mjs"}},ee="./dist/index.cjs",te="./dist/index.mjs",ne="./dist/index.d.cts",oe={"lobe-seo":"dist/cli.js"},re=["dist"],ie={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",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"},se={"@inkjs/ui":"^1.0.0","@langchain/core":"^0.2.20","@langchain/openai":"^0.2.5","@lobehub/cli-ui":"1.10.0",chalk:"^5.3.0",commander:"^12.1.0",conf:"^12.0.0",consola:"^3.2.3",cosmiconfig:"^9.0.0",dotenv:"^16.4.5","fast-deep-equal":"^3.1.3",glob:"^10.4.5","gpt-tokenizer":"^2.2.1","gray-matter":"^4.0.3",ink:"^4.4.1","json-stable-stringify":"^1.1.1","just-diff":"^6.0.2",langchain:"^0.2.12","lodash-es":"^4.17.21","p-map":"^7.0.2",pangu:"^4.0.7",react:"^18.3.1","remark-frontmatter":"^4.0.1","remark-gfm":"^3.0.1","remark-parse":"^10.0.2","remark-stringify":"^10.0.3",swr:"^2.2.5",unified:"^11.0.5","unist-util-visit":"^5.0.0","update-notifier":"^7.2.0",zustand:"^4.5.4"},ae={"@types/json-stable-stringify":"^1.0.36"},ce={node:">=18"},le={access:"public",registry:"https://registry.npmjs.org/"},v={name:R,version:z,description:J,keywords:_,homepage:W,bugs:B,repository:V,license:H,author:Q,sideEffects:Y,type:G,imports:X,exports:Z,main:ee,module:te,types:ne,bin:oe,files:re,scripts:ie,dependencies:se,devDependencies:ae,engines:ce,publishConfig:le};const ue="gpt-4o-mini",de={concurrency:5,entryExtension:".mdx",modelName:ue,temperature:0},ge=r((t,e)=>{t[e]||d.alert.error(`Can't find ${l.bold.yellow("outputLocales")} in config`)},"checkOptionKeys"),S={apiBaseUrl:{default:"",type:"string"},openaiToken:{default:"",type:"string"}},b=new q({projectName:"lobe-seo",schema:S});class pe{static{r(this,"ExplorerConfig")}explorer;customConfig;constructor(){this.explorer=M.cosmiconfigSync("seo")}loadCustomConfig(e){this.customConfig=e}getConfigFile(){return this.customConfig?this.explorer.load(this.customConfig)?.config:this.explorer.search()?.config}}const O=new pe;N.config();const k=r(t=>b.get(t),"getConfig"),fe=r(t=>S[t].default,"getDefulatConfig"),he=r((t,e)=>b.set(t,e),"setConfig"),me=r(()=>process.env.OPENAI_API_KEY||k("openaiToken"),"getOpenAIApiKey"),ye=r(()=>process.env.OPENAI_PROXY_URL||k("apiBaseUrl"),"getOpenAIProxyUrl"),T=r(()=>{const t=O.getConfigFile();return t?U.merge(de,t):d.alert.error(`Can't find ${l.bold.yellow("config")}`,!0)},"getConfigFile"),ve=r(()=>{const t=T();return ge(t,"entry"),t},"getSeoConfig");var p={getConfig:k,getConfigFile:T,getDefulatConfig:fe,getOpenAIApiKey:me,getOpenAIProxyUrl:ye,getSeoConfig:ve,setConfig:he};const be=r(()=>{const t=b.store;return{get:p.getConfig,getDefault:p.getDefulatConfig,set:p.setConfig,store:t}},"useConfStore"),ke=y.memo(()=>{const[t,e]=y.useState(),{store:n,set:o,getDefault:s}=be(),i=r((c,g)=>{o(c,g),e("")},"setConfig"),a=y.useMemo(()=>[{children:f.jsx(w.TextInput,{defaultValue:n.openaiToken,onSubmit:r(c=>i("openaiToken",c),"onSubmit"),placeholder:"Input OpenAI token..."}),defaultValue:s("openaiToken"),key:"openaiToken",label:"OpenAI token",showValue:!1,value:n.openaiToken},{children:f.jsx(w.TextInput,{defaultValue:n.apiBaseUrl,onSubmit:r(c=>i("apiBaseUrl",c),"onSubmit"),placeholder:"Set openAI API proxy, default value: https://api.openai.com/v1/..."}),defaultValue:s("apiBaseUrl"),desc:"OpenAI API proxy, default value: https://api.openai.com/v1/",key:"apiBaseUrl",label:"OpenAI API proxy",showValue:!1,value:n.apiBaseUrl}],[n]);return f.jsx(d.ConfigPanel,{active:t,items:a,logo:"\u{1F92F}",setActive:e,title:"Lobe SEO Config"})}),we=` ## 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. `,xe=r(t=>L.ChatPromptTemplate.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. ${t||we} ## 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.`],["human","{content}"]]),"promptSeo");class Ce{static{r(this,"SeoMdx")}model;config;isJsonMode;prompt;constructor(e,n,o){this.config=e,this.model=new D.ChatOpenAI({configuration:{baseURL:o},maxConcurrency:e.concurrency,maxRetries:4,modelName:e.modelName,openAIApiKey:n,temperature:e.temperature}),this.prompt=xe(this.config.reference),this.isJsonMode=!!this.config?.experimental?.jsonMode}async run(e){try{const n=await this.prompt.formatMessages({content:e}),o=await this.model.call(n,this.isJsonMode?{response_format:{type:"json_object"}}:void 0),s=this.isJsonMode?o.content:o.text;return s||this.handleError(),JSON.parse(s)}catch(n){this.handleError(n)}}handleError(e){d.alert.error(`Seo failed, ${e||"please check your network or try again..."}`,!0)}}const E=r(t=>P.encode(t).length,"calcToken");class Se{static{r(this,"SeoCore")}seoService;config;constructor({openAIApiKey:e,openAIProxyUrl:n,config:o}){this.config=o,this.seoService=new Ce(o,e,n)}async run({content:e,onProgress:n,matter:o}){const s=await this.seoService.prompt.formatMessages({content:e}),i=E(JSON.stringify(s));n?.({isLoading:!0,needToken:i});const a=await this.seoService.run(e);n?.({isLoading:!1,needToken:i});let c;return this.config.tagStringify&&(a.tags=a.tags.join(", ")),c=this.config.groupKey?{...o,[this.config.groupKey]:{...a,...o?.[this.config.groupKey]}}:{...a,...o},{result:c,tokenUsage:i+E(JSON.stringify(a))}}}const Oe=r(t=>C.readFileSync(t,"utf8"),"readMarkdown"),Te=r((t,e)=>{C.writeFileSync(t,e,"utf8")},"writeMarkdown"),A=r((t,e)=>t.map(n=>n.includes("*")||n.includes(e)?n:$.join(n,`**/*${e}`).replaceAll("\\","/")),"matchInputPattern");class Ee{static{r(this,"Seo")}config;query=[];seo;constructor(){this.config=p.getSeoConfig(),this.seo=new Se({config:this.config,openAIApiKey:p.getOpenAIApiKey(),openAIProxyUrl:p.getOpenAIProxyUrl()})}async start(){u.consola.start("Lobe Seo is analyzing your mdx... \u{1F92F}\u{1F30F}\u{1F50D}");const e=this.config.entry;(!e||e.length===0)&&d.alert.error("No mdx entry was found.",!0);let n=F.globSync(A(e,this.config.entryExtension),{ignore:A(this.config.exclude||[],this.config.entryExtension),nodir:!0}).filter(o=>o.includes(this.config.entryExtension));(!n||n.length===0)&&d.alert.error("No mdx entry was found.",!0),this.genFilesQuery(n),this.query.length>0?await this.runQuery():u.consola.success("No content requiring seo was found."),u.consola.success("All seo tasks have been completed\uFF01")}async runQuery(){u.consola.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 K(this.query,async n=>{const o=await this.seo.run({...n,onProgress:r(({isLoading:s})=>{s&&u.consola.start(n.entry)},"onProgress")});if(o?.result&&Object.keys(o.result).length>0){const s=x.stringify(n.rawContent,o.result);Te(n.entry,s),e+=o.tokenUsage,u.consola.success(l.yellow(n.entry),l.gray(`[Token usage: ${o.tokenUsage}]`))}else u.consola.warn("No translation result was found:",l.yellow(n.entry))},{concurrency:this.config.concurrency||5}),e>0&&u.consola.info("Total token usage:",l.cyan(e))}genFilesQuery(e,n){n||u.consola.start(`Running in ${l.bold.cyan(`\u{1F4C4} ${e.length} Mdx`)}..`);for(const o of e)try{const s=Oe(o),{data:i,content:a}=x(s);let c=a;if(i){let g;if(g=this.config.groupKey?i?.[this.config.groupKey]:i,g&&g.tags&&g.title&&g.description)continue}this.config.groupKey&&i?.title&&(c=[`#${i?.title}`,a].join(` `)),this.query.push({content:c,entry:o,matter:i||{},rawContent:a})}catch{d.alert.error(`${o} not found`,!0)}}}const Ae=j({pkg:v,shouldNotifyInNpmScript:!0});Ae.notify({isGlobal:!0});const h=new m.Command;h.name("lobe-seo").description(v.description).version(v.version).addOption(new m.Option("-o, --option","Setup lobe-seo preferences")).addOption(new m.Option("-c, --config <string>","Specify the configuration file")),h.command("locale",{isDefault:!0}).action(async()=>{const t=h.opts();t.option?d.render(f.jsx(ke,{})):(t.config&&O.loadCustomConfig(t.config),await new Ee().start())}),h.parse();