{"id":24912319,"url":"https://github.com/forge-42/seo-tools","last_synced_at":"2025-06-17T11:35:57.025Z","repository":{"id":249840231,"uuid":"832179385","full_name":"forge-42/seo-tools","owner":"forge-42","description":"Set of helpers designed to help you create, maintain and develop your SEO","archived":false,"fork":false,"pushed_at":"2024-07-30T08:02:04.000Z","size":202,"stargazers_count":40,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-30T17:33:29.578Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","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/forge-42.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.MD","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-22T13:38:35.000Z","updated_at":"2025-01-23T21:04:43.000Z","dependencies_parsed_at":"2024-07-29T09:11:08.694Z","dependency_job_id":"fc44cea6-5032-4e92-bc8f-f5fc42c78b23","html_url":"https://github.com/forge-42/seo-tools","commit_stats":null,"previous_names":["forge42dev/seo-tools","forge-42/seo-tools"],"tags_count":6,"template":false,"template_full_name":"forge-42/open-source-stack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-42%2Fseo-tools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-42%2Fseo-tools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-42%2Fseo-tools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-42%2Fseo-tools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/forge-42","download_url":"https://codeload.github.com/forge-42/seo-tools/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236752334,"owners_count":19199230,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2025-02-02T05:11:59.325Z","updated_at":"2025-02-02T05:11:59.961Z","avatar_url":"https://github.com/forge-42.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"#  @forge42/seo-tools\n![GitHub Repo stars](https://img.shields.io/github/stars/forge42dev/seo-tools?style=social)\n![npm](https://img.shields.io/npm/v/@forge42/seo-tools?style=plastic)\n![GitHub](https://img.shields.io/github/license/forge42dev/seo-tools?style=plastic)\n![npm](https://img.shields.io/npm/dy/@forge42/seo-tools?style=plastic)\n![npm](https://img.shields.io/npm/dw/@forge42/seo-tools?style=plastic)\n![GitHub top language](https://img.shields.io/github/languages/top/forge42dev/seo-tools?style=plastic)\n\nSEO Tools is a collection of tools to help you with your SEO efforts. It includes tools to help you with sitemaps, robots.txt, canonical links, structured data, and metadata. The package is split into smaller submodules so you can only import the parts you need, in spirit of good SEO we want your bundle as small as possible.\n\n\n## Installation\n\nUse the package manager of your choice to install the package.\n\n```bash\nnpm install @forge42/seo-tools\n```\n\n## Usage\n\nThe package is split into smaller submodules so you can only import the parts you need, in spirit of good SEO we want your bundle as\nsmall as possible.\n\n```javascript\nimport { generateCanonicalLinks } from '@forge42/seo-tools/canonical';\n```\n\nThis means we do not include a barrel export and you need to import the specific module you need. We do this so only the parts you need\nare actually used in your bundle as mentioned above. Now we will go over each subimport and what they do.\n\n## Canonical \u0026 alternate links\n\nThe canonical link is a link that tells search engines that a certain URL represents the master copy of a page. This is useful for SEO because it helps search engines avoid duplicate content issues and tell it for alternative languages/content.\n\nThe transformer function will return the canonical url and the current alternative, the alternative can be a string, object or anything else that you can pass to the transformer.\n\n```typescript\nimport { generateCanonicalLinks } from '@forge42/seo-tools/canonical';\n\nconst canonicalLinks = generateCanonicalLinks({\n\t// Used to generate the final url, it passes your alternatives, url and domain to the function for you to create whatever link you need\n\turlTransformer: ({ url, domain, alternative, canonicalUrl }) =\u003e `${domain}/${url}?lng=${alternative}`,\n\t// Used to generate the final attributes\n\taltAttributesTransformer: ({ url, domain, alternative, canonicalUrl }) =\u003e attributes,\n\t// This takes a generic type and returns it in your transformers\n\talternatives: [\"de\", \"es\"],\n\tdomain: \"https://example.com\",\n\turl: \"current-url\",\n\t// Used to add additional attributes\n\tcanonicalAttributes: {\n\t\t// These are included by default but you can add additional attributes\n\t\trel: 'canonical',\n\t\t// These are included by default but you can add additional attributes\n\t\thref: 'https://example.com'\n\t}\n},\n// Second argument tells the function if it should generate the output as string or as an array of json objects\nfalse\n);\n\nconsole.log(canonicalLinks);\n// \u003clink rel=\"canonical\" href=\"https://example.com/current-url\"\u003e\n// \u003clink rel=\"alternate\" href=\"https://example.com/current-url?lng=de\" hreflang=\"de\"\u003e\n// \u003clink rel=\"alternate\" href=\"https://example.com/current-url?lng=es\" hreflang=\"es\"\u003e\n// or as an array of json objects\n// [\n// \t{ rel: 'canonical', href: 'https://example.com/current-url' },\n// \t{ rel: 'alternate', href: 'https://example.com/current-url?lng=de', hreflang: 'de' },\n// \t{ rel: 'alternate', href: 'https://example.com/current-url?lng=es', hreflang: 'es' }\n// ]\n```\n\n## Robots.txt\n\nThe robots.txt file is a file that tells search engines which pages they can and cannot index. This is useful for SEO because it helps search engines avoid indexing pages that you don't want them to index.\n\n```typescript\nimport { generateRobotsTxt } from '@forge42/seo-tools/robots';\n\nconst robotsTxt = generateRobotsTxt([\n\t{\n\t\tuserAgent: '*',\n\t\tallow: ['/'],\n\t\tdisallow: ['/admin', '/login'],\n\t\tcrawlDelay: 1,\n\t\tsitemap: 'https://example.com/sitemap.xml'\n\t},\n\t{\n\t\tuserAgent: 'Googlebot',\n\t\tallow: ['/'],\n\t\tdisallow: ['/admin', '/login'],\n\t\tcrawlDelay: 1,\n\t\tsitemap: 'https://example.com/sitemap.xml'\n\t}\n]\n);\n\nconsole.log(robotsTxt);\n// User-agent: *\n// Allow: /\n// Disallow: /admin\n// Disallow: /login\n// Crawl-delay: 1\n// Sitemap: https://example.com/sitemap.xml\n// User-agent: Googlebot\n// Allow: /\n// Disallow: /admin\n// Disallow: /login\n// Crawl-delay: 1\n// Sitemap: https://example.com/sitemap.xml\n```\n\n## Sitemap.xml\n\nThe sitemap.xml file is a file that tells search engines which pages they should index. This is useful for SEO because it helps search engines find all of the pages on your site.\n\n```typescript\nimport { generateSitemap } from '@forge42/seo-tools/sitemap';\n\nconst sitemap = generateSitemap(\n\t{\n\t\tdomain: \"https://example.com\",\n\t\t// Defines the routes you want to exclude from the sitemap (useful if routes are dynamic or auto-generated)\n\t\tignore: [\"/dashboard*\"]\n\t\t// Defines the routes you want to include in the sitemap\n\t  routes: [\n\t\t\t{ url: \"/\", lastmod: \"2020-02-02\", changefreq: \"monthly\", priority: 0.8 },\n\t\t\t{ url: \"/about\", lastmod: \"2020-02-02\", changefreq: \"monthly\", priority: 0.8 },\n\t\t\t{ url: \"/contact\", lastmod: \"2020-02-02\", changefreq: \"monthly\", priority: 0.8 }\n\t\t],\n\t\t// This is a transformer that allows you to generate the url you need\n\t\ttransformer: ({ url, domain }) =\u003e `${domain}${url}`\n\n\t}\n);\nconsole.log(sitemap);\n\n// \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n// \u003curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003e\n// \t\u003curl\u003e\n// \t\t\u003cloc\u003ehttps://example.com/\u003c/loc\u003e\n// \t\t\u003clastmod\u003e2020-02-02\u003c/lastmod\u003e\n// \t\t\u003cchangefreq\u003emonthly\u003c/changefreq\u003e\n// \t\t\u003cpriority\u003e0.8\u003c/priority\u003e\n// \t\u003c/url\u003e\n// \t\u003curl\u003e\n// \t\t\u003cloc\u003ehttps://example.com/about\u003c/loc\u003e\n// \t\t\u003clastmod\u003e2020-02-02\u003c/lastmod\u003e\n// \t\t\u003cchangefreq\u003emonthly\u003c/changefreq\u003e\n// \t\t\u003cpriority\u003e0.8\u003c/priority\u003e\n// \t\u003c/url\u003e\n// \t\u003curl\u003e\n// \t\t\u003cloc\u003ehttps://example.com/contact\u003c/loc\u003e\n// \t\t\u003clastmod\u003e2020-02-02\u003c/lastmod\u003e\n// \t\t\u003cchangefreq\u003emonthly\u003c/changefreq\u003e\n// \t\t\u003cpriority\u003e0.8\u003c/priority\u003e\n// \t\u003c/url\u003e\n// \u003c/urlset\u003e\n```\n\n## Sitemap index\n\nThe sitemap index is a file that tells search engines where to find all of the sitemaps on your site. This is useful for SEO because it helps search engines find all of the sitemaps on your site.\n\n```typescript\nimport { generateSitemapIndex } from '@forge42/seo-tools/sitemap';\n\nconst sitemapIndex = generateSitemapIndex([\n\t{\n\t\turl: 'https://example.com/sitemap1.xml',\n\t\tlastmod: '2022-01-01'\n\t},\n\t{\n\t\turl: 'https://example.com/sitemap2.xml',\n\t\tlastmod: '2022-01-01'\n\t}\n]\n);\n\nconsole.log(sitemapIndex);\n\n// \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n// \u003csitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003e\n// \t\u003csitemap\u003e\n// \t\t\u003cloc\u003ehttps://example.com/sitemap1.xml\u003c/loc\u003e\n// \t\t\u003clastmod\u003e2022-01-01\u003c/lastmod\u003e\n// \t\u003c/sitemap\u003e\n// \t\u003csitemap\u003e\n// \t\t\u003cloc\u003ehttps://example.com/sitemap2.xml\u003c/loc\u003e\n// \t\t\u003clastmod\u003e2022-01-01\u003c/lastmod\u003e\n// \t\u003c/sitemap\u003e\n// \u003c/sitemapindex\u003e\n\n```\n## Structured data\n\nStructured data is a way to provide search engines with information about the content on your site. This is useful for SEO because it helps search engines understand the content on your site and display it in search results.\n\nTo better learn about structured data you can find all the information you will need here:\nhttps://developers.google.com/search/docs/appearance/structured-data/intro-structured-data\n\nWe offer the following utilities to generate structured data:\n- breadcrumb - ``` import { breadcrumbs } from '@forge42/seo-tools/structured-data/breadcrumb'; ```\n- article - ``` import { article } from '@forge42/seo-tools/structured-data/article'; ```\n- car - ``` import { car } from '@forge42/seo-tools/structured-data/car'; ```\n- course - ``` import { course } from '@forge42/seo-tools/structured-data/course'; ```\n- dataset - ``` import { dataset } from '@forge42/seo-tools/structured-data/dataset'; ```\n- discussion-forum - ``` import { discussionForum } from '@forge42/seo-tools/structured-data/discussion-forum'; ```\n- employer-rating - ``` import { employerRating } from '@forge42/seo-tools/structured-data/employer-rating'; ```\n- event - ``` import { event } from '@forge42/seo-tools/structured-data/event'; ```\n- faq - ``` import { faq } from '@forge42/seo-tools/structured-data/faq'; ```\n- image - ``` import { image } from '@forge42/seo-tools/structured-data/image'; ```\n- item-list - ``` import { itemList } from '@forge42/seo-tools/structured-data/item-list'; ```\n- job-posting - ``` import { jobPosting } from '@forge42/seo-tools/structured-data/job-posting'; ```\n- occupation - ``` import { occupation } from '@forge42/seo-tools/structured-data/occupation'; ```\n- organization - ``` import { organization } from '@forge42/seo-tools/structured-data/organization'; ```\n- product - ``` import { product } from '@forge42/seo-tools/structured-data/product'; ```\n- profile - ``` import { profile } from '@forge42/seo-tools/structured-data/profile'; ```\n- qa - ``` import { qa } from '@forge42/seo-tools/structured-data/qa'; ```\n- recipe - ``` import { recipe } from '@forge42/seo-tools/structured-data/recipe'; ```\n- software-app - ``` import { softwareApp } from '@forge42/seo-tools/structured-data/software-app'; ```\n- video - ``` import { video } from '@forge42/seo-tools/structured-data/video'; ```\n\n```typescript\nimport { article } from '@forge42/seo-tools/structured-data/article';\n\nconst structuredData = article({\n\t\"@type\": \"Article\",\n\t\"headline\": \"Article headline\",\n\t\"image\": \"https://example.com/image.jpg\",\n\t\"datePublished\": \"2022-01-01\",\n});\n// Set it somehow in your html\n\u003chead\u003e\n\t\u003cscript type=\"application/ld+json\"\u003e\n\t\t{structuredData}\n\t\u003c/script\u003e\n\u003c/head\u003e\n```\n\nThe example above will show an article when a relevant google search is made on top of the search results.\n\n# Remix.run / React Router v7\n\nWe have a dedicated module for Remix.run/React Router v7 that will help you with SEO generation. It's all located in the remix module and is compatible\nwith any runtime you are using.\n\n## Metadata\n\nMeta data is a way to provide search engines with information about the content on your site. This is useful for SEO because it helps search engines understand the content on your site and display it in search results.\n\nWe have a lightweight utility that helps you avoid writing the same tags multiple times for different platforms. It will generate the twitter \u0026 og title and description tags for you. You can also add structured data to the meta tags like in the example below.\n\n```typescript\nimport { generateMeta } from \"@forge42/seo-tools/remix/metadata\";\nimport { article } from \"@forge42/seo-tools/structured-data/article\";\nimport { course } from \"@forge42/seo-tools/structured-data/course\";\n\nexport const meta: MetaFunction = () =\u003e {\n\t// This utility will under the hood generate the twitter \u0026 og title and description tags for you.\n  const meta = generateMeta({\n    title: \"test\",\n    description: \"test\",\n    url: \"test\",\n  }, [\n    {\n      \"script:ld+json\": article({\n        \"@type\": \"Article\",\n        headline: \"Article headline\",\n        image: \"https://example.com/image.jpg\",\n        datePublished: \"2021-01-01T00:00:00Z\",\n      })\n    },\n    {\n      \"script:ld+json\": course({\n        \"@type\": \"Course\",\n        name: \"Course name\",\n        description: \"Course description\",\n      })\n    }\n  ])\n  return meta\n};\n```\n\n## Sitemap\n\nThis sitemap utility is a superset of the sitemap utility above. It will generate the sitemap for you based on all your Remix routes. It ignores\nby default the root route, anything with sitemap in the name and robots.txt. You can also pass a custom transformer to generate the url you need.\nRefer to the sitemap utility above for more information.\n\n```typescript\n// routes/sitemap[.]xml.ts\nimport { generateRemixSitemap } from \"@forge42/seo-tools/remix/sitemap\"\n\nexport const loader = async() =\u003e {\n\tconst sitemap = await generateRemixSitemap({\n\t\t domain: \"https://example.com\",\n\n\t})\n\n\treturn new Response(sitemap, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/xml\",\n\t\t},\n\t})\n}\n```\n\n### Handling dynamic routes\n\nIf you want to generate different entries in the sitemap by creating dynamic routes at runtime you can use the following approach:\n```typescript\n// routes/sitemap[.]xml.ts\n\nimport { generateRemixSitemap } from \"@forge42/seo-tools/remix/sitemap\"\n\nexport type SitemapData = {\n\tlang: Language\n}\n\nexport const loader = async ({ request }) =\u003e {\n\tconst sitemap = await generateRemixSitemap({\n\t\t// This gets passed to every handler\n\t\t sitemapData: {\n\t\t\t \"lang\": request.query.get(\"lng\") as Language\n\t\t } satisfies SitemapData\n\t})\n\n\treturn new Response(sitemap, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/xml\",\n\t\t},\n\t})\n}\n// routes/index.tsx\nimport type { SitemapHandle } from \"@forge42/seo-tools/remix/sitemap\";\nimport type { SitemapData } from \"~/routes/sitemap[.]xml\";\n// This utility trumps the default url generation so it's important to at least return the current route from here.\nexport const handle: SitemapHandle\u003cSitemapData\u003e = {\n\tsitemap: async (domain, url, { lang }) =\u003e {\n\t\tconst alternateLanguages = supportedLanguages.filter((language) =\u003e language !== lang)\n\t\treturn [\n\t\t\t{\n\t\t\t\troute: `${domain}${url}?lng=${lang}`,\n\t\t\t\tchangefreq: \"monthly\",\n\t\t\t\tpriority: 1.0,\n\t\t\t\t// Create alternate links for each language\n\t\t\t\talternateLinks: alternateLanguages.map((lang) =\u003e ({\n\t\t\t\t\threflang: lang,\n\t\t\t\t\thref: `${domain}${url}?lng=${lang}`,\n\t\t\t\t})),\n\t\t\t},\n\t\t]\n\t},\n}\n```\n\n### Handling sitemap index + dynamic sitemaps + robots.txt\n\nIf you want to generate different sitemaps based on the language you can use the following approach:\n```typescript\n// routes/sitemap.$lang[.]xml.ts\nimport type { LoaderFunctionArgs } from \"@remix-run/node\"\nimport { generateRemixSitemap } from \"@forge42/seo-tools/remix/sitemap\"\n// Optionally import routes from the remix build to be consumed by the sitemap generator if the default one throws an error\nimport { routes } from \"virtual:remix/server-build\";\nexport const loader = async ({ request, params }: LoaderFunctionArgs) =\u003e {\n\tconst domain = `${new URL(request.url).origin}`\n\n\tconst sitemap = await generateRemixSitemap({\n\t\t// Domain to append urls to\n\t\tdomain,\n\t\troutes,\n\t\t// Ignores all dashboard routes\n\t\tignore: [\"/status\"],\n\t\t// Transforms the url before adding it to the sitemap\n\t\turlTransformer: (url) =\u003e `${url}?lng=${params.lang}`,\n\t\tsitemapData: {\n\t\t\tlang: params.lang,\n\t\t},\n\t})\n\n\treturn new Response(sitemap, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/xml; charset=utf-8\",\n\t\t},\n\t})\n}\n\n\n// routes/sitemap-index[.]xml.ts\nimport type { LoaderFunctionArgs } from \"@remix-run/node\"\nimport { generateSitemapIndex } from \"@forge42/seo-tools/sitemap\"\n\nexport const loader = async ({ request }: LoaderFunctionArgs) =\u003e {\n\tconst domain = new URL(request.url).origin\n\tconst sitemaps = generateSitemapIndex([\n\t\t{\n\t\t\turl: `${domain}/sitemap/en.xml`,\n\t\t\tlastmod: \"2024-07-17\",\n\t\t},\n\t\t{\n\t\t\turl: `${domain}/sitemap/bs.xml`,\n\t\t\tlastmod: \"2024-07-17\",\n\t\t},\n\t])\n\n\treturn new Response(sitemaps, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/xml; charset=utf-8\",\n\t\t},\n\t})\n}\n// routes/robots[.]txt.ts\nimport type { LoaderFunctionArgs } from \"@remix-run/node\"\nimport { generateRobotsTxt } from \"@forge42/seo-tools/robots\"\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n\tconst isProductionDeployment = process.env.DEPLOYMENT_ENV === \"production\"\n\tconst domain = new URL(request.url).origin\n\tconst robotsTxt = generateRobotsTxt([\n\t\t{\n\t\t\tuserAgent: \"*\",\n\t\t\t[isProductionDeployment ? \"allow\": \"disallow\"]:[\"/\"],\n\t\t\tsitemap: [`${domain}/sitemap-index.xml`],\n\t\t},\n\t])\n\n\treturn new Response(robotsTxt, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/plain\",\n\t\t},\n\t})\n}\n```\n\n## License\n[MIT](https://choosealicense.com/licenses/mit/)\n\n## Contributing\nPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\nPlease make sure to update tests as appropriate.\n\n## Support\n\nIf you like the project and want to support it you can sponsor it on GitHub, or even better, open up PR's and contribute to the project.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforge-42%2Fseo-tools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fforge-42%2Fseo-tools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforge-42%2Fseo-tools/lists"}