{"id":51129953,"url":"https://github.com/osisdie/node-seo-for-fun","last_synced_at":"2026-06-25T11:01:32.445Z","repository":{"id":41941170,"uuid":"148706454","full_name":"osisdie/node-seo-for-fun","owner":"osisdie","description":"A configurable Node.js SEO validator that parses HTML DOM, checks against customizable rules, and reports actionable recommendations. Supports file and stream I/O with flexible output options.","archived":false,"fork":false,"pushed_at":"2026-05-24T04:55:56.000Z","size":762,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-24T06:36:34.007Z","etag":null,"topics":["code-for-fun","demo","dom-analysis","html-parser","javascript","nodejs","seo","seo-tool","seo-validator","vercel"],"latest_commit_sha":null,"homepage":"https://node-seo-for-fun.vercel.app","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/osisdie.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-09-13T22:45:05.000Z","updated_at":"2026-05-24T04:55:54.000Z","dependencies_parsed_at":"2024-12-06T04:25:18.130Z","dependency_job_id":"12e2fbc5-b2e5-4122-aa7c-39fee697b6db","html_url":"https://github.com/osisdie/node-seo-for-fun","commit_stats":null,"previous_names":["osisdie/node-seo-for-fun"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/osisdie/node-seo-for-fun","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osisdie%2Fnode-seo-for-fun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osisdie%2Fnode-seo-for-fun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osisdie%2Fnode-seo-for-fun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osisdie%2Fnode-seo-for-fun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/osisdie","download_url":"https://codeload.github.com/osisdie/node-seo-for-fun/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osisdie%2Fnode-seo-for-fun/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34771664,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-for-fun","demo","dom-analysis","html-parser","javascript","nodejs","seo","seo-tool","seo-validator","vercel"],"created_at":"2026-06-25T11:01:30.406Z","updated_at":"2026-06-25T11:01:32.439Z","avatar_url":"https://github.com/osisdie.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# node-seo-for-fun\n\n[![CI](https://github.com/osisdie/node-seo-for-fun/actions/workflows/ci.yml/badge.svg)](https://github.com/osisdie/node-seo-for-fun/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)\n[![Last Commit](https://img.shields.io/github/last-commit/osisdie/node-seo-for-fun)](https://github.com/osisdie/node-seo-for-fun/commits/main)\n\nA configurable Node.js SEO validator that parses HTML DOM, checks against customizable rules, and reports actionable recommendations. Supports file and stream I/O with flexible output options.\n\n*Series of code_for_fun*\n\n## Features\n\n- **5 built-in SEO rules** — covers `\u003cimg alt\u003e`, `\u003ca rel\u003e`, `\u003chead\u003e` meta tags, `\u003cstrong\u003e` overuse, `\u003cH1\u003e` uniqueness\n- **Custom rules** — define your own rules via JSON config (no code changes needed)\n- **Flexible I/O** — read from files or streams, output to files, streams, or console\n- **Pattern-based** — extensible pattern system (`existsTag`, `existsAttr`, `existsNoAttr`, `existsAttrVal`, `tagCountLessThan`)\n- **Selective validation** — include or exclude specific rules per run\n- **Web UI included** — paste HTML and get instant results via browser\n- **Vercel-ready** — one-click deploy to Vercel\n\n**[Live Demo](https://node-seo-for-fun.vercel.app)** | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fosisdie%2Fnode-seo-for-fun)\n\n## Quick Example\n\n```\n$ node -e \"\nconst fs = require('fs');\nconst { SEOValidator } = require('./lib/seo/seo_validator');\nconst { AppUtil } = require('./lib/app_util');\nconst { RuleInputEnum, RuleOutputEnum } = require('./lib/models/app_enum');\n\nlet readStream = fs.createReadStream('test/input/https___google_com_tw');\nlet validator = new SEOValidator()\n  .includeRules([1, 2, 3, 4, 5])\n  .setReader(AppUtil.createReader({ kind: RuleInputEnum.stream, stream: readStream }))\n  .setWriter(AppUtil.createWriter({ kind: RuleOutputEnum.console }));\n\nvalidator.validate().then(result =\u003e console.log(result.data));\n\"\n```\n\n**Output:**\n\n```\nThis HTML without \u003ca rel\u003e tag\nThis HTML without \u003cmeta name=\"description\"\u003e tag\nThis HTML without \u003cmeta name=\"keywords\"\u003e tag\n```\n\n## Built-in Rules\n\n| Rule | Checks | Severity |\n|------|--------|----------|\n| **Rule 1** | `\u003cimg\u003e` tags must have `alt` attribute | Accessibility + SEO |\n| **Rule 2** | `\u003ca\u003e` tags must have `rel` attribute | SEO link signals |\n| **Rule 3** | `\u003chead\u003e` must contain `\u003ctitle\u003e`, `\u003cmeta name=\"description\"\u003e`, `\u003cmeta name=\"keywords\"\u003e` | Critical SEO |\n| **Rule 4** | No more than 15 `\u003cstrong\u003e` tags | Content quality |\n| **Rule 5** | Only one `\u003cH1\u003e` tag allowed | SEO heading structure |\n| **Rule 101** | `\u003cmeta name=\"robots\"\u003e` must exist (custom example) | Crawl control |\n\n## Prerequisites\n\n- Node.js 20.0 or higher\n\n## Installation\n\n```sh\ngit clone https://github.com/osisdie/node-seo-for-fun.git\ncd node-seo-for-fun\nnpm install\n```\n\n### Run Locally (Web UI)\n\n```sh\nnpm start          # http://localhost:3000\n# or with hot-reload:\nnpm run dev\n```\n\nOpen `http://localhost:3000` to use the web-based SEO validator.\n\n### Deploy to Vercel\n\n```sh\nnpx vercel\n```\n\nOr click the **Deploy with Vercel** button above for one-click deployment.\n\n## Config Your Rules\n\nSEO rules are defined in the config file (**default**: `conf/config.json`)\n\n### Default SEO syntax patterns\n```json\n\"seo\": {\n    \"pattern\": {\n      \"existsTag\": {\n        \"xpath\": \"{{root}} {{tag}}\",\n        \"msg\": \"This HTML without \u003c{{tag}}\u003e tag\"\n      },\n      \"existsAttr\": {\n        \"xpath\": \"{{root}} {{tag}}[{{attr}}]\",\n        \"msg\": \"This HTML without \u003c{{tag}} {{attr}}\u003e tag\"\n      },\n      \"existsNoAttr\": {\n        \"xpath\": \"{{root}} {{tag}}:not([{{attr}}])\",\n        \"msg\": \"This HTML without \u003c{{tag}} {{attr}}\u003e tag\"\n      },\n      \"existsAttrVal\": {\n        \"xpath\": \"{{root}} {{tag}}[{{attr}}*={{value}}]\",\n        \"msg\": \"This HTML without \u003c{{tag}} {{attr}}=\\\"{{value}}\\\"\u003e tag\"\n      },\n      \"tagCountLessThan\": {\n        \"xpath\": \"{{root}} {{tag}}\",\n        \"msg\": \"This HTML have more than {{max}} \u003c{{tag}}\u003e tag\"\n      }\n    }\n}\n```\n\n### Predefined SEO rules 1~5 and custom rule 101\n\n```json\n\"seo\": {\n    \"rules\": {\n      \"rule1\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:existsNoAttr\",\n            \"fn\": \"checkShouldNotExist\",\n            \"root\": \"html\",\n            \"tag\": \"img\",\n            \"attr\": \"alt\"\n          }\n        ]\n      },\n      \"rule2\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:existsNoAttr\",\n            \"fn\": \"checkShouldNotExist\",\n            \"root\": \"html\",\n            \"tag\": \"a\",\n            \"attr\": \"rel\"\n          }\n        ]\n      },\n      \"rule3\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:existsTag\",\n            \"fn\": \"checkShouldExist\",\n            \"root\": \"head\",\n            \"tag\": \"title\",\n            \"min\": 1,\n            \"max\": 1\n          },\n          {\n            \"pattern\": \"seo:pattern:existsAttrVal\",\n            \"fn\": \"checkShouldExist\",\n            \"root\": \"head\",\n            \"tag\": \"meta\",\n            \"attr\": \"name\",\n            \"value\": \"description\",\n            \"min\": 1,\n            \"max\": 1\n          },\n          {\n            \"pattern\": \"seo:pattern:existsAttrVal\",\n            \"fn\": \"checkShouldExist\",\n            \"root\": \"head\",\n            \"tag\": \"meta\",\n            \"attr\": \"name\",\n            \"value\": \"keywords\",\n            \"min\": 1,\n            \"max\": 1\n          }\n        ]\n      },\n      \"rule4\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:tagCountLessThan\",\n            \"fn\": \"checkMaxOccurrence\",\n            \"root\": \"html\",\n            \"tag\": \"strong\",\n            \"max\": 15,\n            \"min\": 0\n          }\n        ]\n      },\n      \"rule5\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:tagCountLessThan\",\n            \"fn\": \"checkMaxOccurrence\",\n            \"root\": \"html\",\n            \"tag\": \"h1\",\n            \"max\": 1,\n            \"min\": 0\n          }\n        ]\n      },\n      \"rule101\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:existsAttrVal\",\n            \"fn\": \"checkShouldExist\",\n            \"root\": \"head\",\n            \"tag\": \"meta\",\n            \"attr\": \"name\",\n            \"value\": \"robots\",\n            \"min\": 1,\n            \"max\": 1\n          }\n        ]\n      }\n    }\n}\n```\n\n## Unit Test\n\nRun all tests:\n```sh\nnpm test\n```\n\n**Sample output** (57 tests):\n```\n  AppUtil() requires(/lib/app_util.js)\n    config\n      ✔ path conf/config.json should exist\n    Function getCfgVal()\n      version\n        ✔ app:version should be 0.1.0\n\n  SEOValidator() requires(/lib/seo/seo_validator.js)\n    Function validate()\n      pass, input:file, output:file\n        ✔ rule1 should have 0 warning(s)\n        ✔ rule2 should have 0 warning(s)\n        ...\n      NOT pass, input:stream, output:console\n        ✔ https://google.com.tw returns 3 warning(s)\n\n  SingleRuleParser() requires(/lib/seo/seo_validator.js)\n    Function checkConfigSyntax()\n      ✔ correctly syntax (×11)\n    Function analysis()\n      ✔ should return isSuccess with/without warnings (×14)\n\n  57 passing (2s)\n```\n\n### Test individual modules\n\n```sh\n# AppUtil config tests\nnpm test ./test/AppUtil_test.js\n\n# Reader/Writer I/O tests\nnpm test ./test/app_fs_test.js\n\n# Single rule syntax validation\nnpm test ./test/SingleRuleParser_test.js\n\n# Full SEO validator integration tests\nnpm test ./test/SEOValidator_test.js\n```\n\n## Usage Example\n\n```js\nconst fs = require('fs')\nconst { SEOValidator } = require('./lib/seo/seo_validator')\nconst { AppUtil } = require('./lib/app_util')\nconst { RuleInputEnum, RuleOutputEnum } = require('./lib/models/app_enum')\n\nlet readStream = fs.createReadStream('test/input/https___google_com_tw')\nlet validator = new SEOValidator()\n  .includeRules([1, 2, 3, 4, 5])\n  .setReader(AppUtil.createReader({ kind: RuleInputEnum.stream, stream: readStream }))\n  .setWriter(AppUtil.createWriter({ kind: RuleOutputEnum.file, path: 'test/output/result.out' }))\n\nvalidator.validate()\n  .then(result =\u003e {\n    console.log(result.data)\n    // ['This HTML without \u003ca rel\u003e tag',\n    //  'This HTML without \u003cmeta name=\"description\"\u003e tag',\n    //  'This HTML without \u003cmeta name=\"keywords\"\u003e tag']\n  })\n```\n\n## Architecture\n\n```\nnode-seo-for-fun/\n├── conf/config.json          # SEO rules \u0026 patterns configuration\n├── lib/\n│   ├── app_util.js           # Config loader \u0026 utility factory\n│   ├── core/app_fs.js        # ReaderBase / WriterBase (File, Stream, Console)\n│   ├── models/app_enum.js    # RuleInputEnum, RuleOutputEnum\n│   └── seo/\n│       ├── seo_validator.js  # SEOValidator (high-level orchestrator)\n│       └── seo_rule.js       # SingleRuleParser / SingleRuleParserBase\n├── test/\n│   ├── input/                # Test HTML files (pass / not_pass per rule)\n│   └── *.js                  # Mocha test suites\n└── .github/workflows/ci.yml  # CI: Node 20 + 22\n```\n\n## Create Your Own Custom Rule\n\nYou can easily create a new rule:\n\n- Rule number should start after 101 (1~100 are reserved for system default rules). Prefix with `rule`, e.g. `rule101`.\n- Combine your **tag**, **attribute**, **value**, or even **occurrences** as your new rule content.\n- The **pattern** property is a config path, e.g. `\"seo:pattern:existsAttrVal\"` points to the DOM selector and alert message template.\n- The **fn** property specifies the validation method in `SingleRuleParser` / `SingleRuleParserBase` (you can create custom validation functions if needed).\n\n```json\n\"seo\": {\n    \"rules\": {\n      \"rule101\": {\n        \"ruleFor\": [\n          {\n            \"pattern\": \"seo:pattern:existsAttrVal\",\n            \"fn\": \"checkShouldExist\",\n            \"root\": \"head\",\n            \"tag\": \"meta\",\n            \"attr\": \"name\",\n            \"value\": \"robots\",\n            \"min\": 1,\n            \"max\": 1\n          }\n        ]\n      }\n    }\n}\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on adding new rules and submitting PRs.\n\n## License\n\n[MIT](LICENSE)\n\n*Enjoy this **node-seo-for-fun** project!*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosisdie%2Fnode-seo-for-fun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fosisdie%2Fnode-seo-for-fun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosisdie%2Fnode-seo-for-fun/lists"}