agentimization 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +345 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4815,11 +4815,11 @@ var extractMetaTags = (html) => {
4815
4815
  const metaRegex = /<meta[^>]+(?:name|property)=["']([^"']+)["'][^>]+content=["']([^"']+)["']/gi;
4816
4816
  let match;
4817
4817
  while ((match = metaRegex.exec(html)) !== null) {
4818
- meta[match[1]] = match[2];
4818
+ meta[match[1].toLowerCase()] = match[2];
4819
4819
  }
4820
4820
  const metaRegex2 = /<meta[^>]+content=["']([^"']+)["'][^>]+(?:name|property)=["']([^"']+)["']/gi;
4821
4821
  while ((match = metaRegex2.exec(html)) !== null) {
4822
- meta[match[2]] = match[1];
4822
+ meta[match[2].toLowerCase()] = match[1];
4823
4823
  }
4824
4824
  return meta;
4825
4825
  };
@@ -4835,6 +4835,24 @@ var extractJsonLd = (html) => {
4835
4835
  }
4836
4836
  return results;
4837
4837
  };
4838
+ var readAttr = (attrs, name) => {
4839
+ const re = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`, "i");
4840
+ const m = attrs.match(re);
4841
+ if (!m) return void 0;
4842
+ return m[1] ?? m[2];
4843
+ };
4844
+ var extractImages = (html) => {
4845
+ const images = [];
4846
+ const imgRegex = /<img\b([^>]*)>/gi;
4847
+ let match;
4848
+ while ((match = imgRegex.exec(html)) !== null) {
4849
+ const attrs = match[1];
4850
+ const src = readAttr(attrs, "src");
4851
+ if (src === void 0) continue;
4852
+ images.push({ src, alt: readAttr(attrs, "alt") });
4853
+ }
4854
+ return images;
4855
+ };
4838
4856
  var extractHeadings = (html) => {
4839
4857
  const headings = [];
4840
4858
  const regex = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
@@ -4946,6 +4964,53 @@ var renderingStrategy = {
4946
4964
  };
4947
4965
  }
4948
4966
  };
4967
+ var substantialTextContent = {
4968
+ id: "substantial-text-content",
4969
+ name: "Substantial Text Content",
4970
+ category: "page-size",
4971
+ description: "Checks for at least 100 words of readable body text",
4972
+ weight: 0.8,
4973
+ run: async (ctx) => {
4974
+ const pages = ctx.sampledPages.slice(0, 10);
4975
+ if (pages.length === 0) {
4976
+ return {
4977
+ id: "substantial-text-content",
4978
+ name: "Substantial Text Content",
4979
+ category: "page-size",
4980
+ status: "skip",
4981
+ message: "No pages sampled"
4982
+ };
4983
+ }
4984
+ let withSubstantialContent = 0;
4985
+ let totalWords = 0;
4986
+ for (const page of pages) {
4987
+ const text = stripHtml(page.html);
4988
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
4989
+ totalWords += words;
4990
+ if (words >= 100) withSubstantialContent++;
4991
+ }
4992
+ const avgWords = Math.round(totalWords / pages.length);
4993
+ if (withSubstantialContent === pages.length) {
4994
+ return {
4995
+ id: "substantial-text-content",
4996
+ name: "Substantial Text Content",
4997
+ category: "page-size",
4998
+ status: "pass",
4999
+ message: `All ${pages.length} pages have \u2265100 words of body text (avg ${avgWords})`,
5000
+ metadata: { withSubstantialContent, avgWords }
5001
+ };
5002
+ }
5003
+ return {
5004
+ id: "substantial-text-content",
5005
+ name: "Substantial Text Content",
5006
+ category: "page-size",
5007
+ status: withSubstantialContent > 0 ? "warn" : "fail",
5008
+ message: `${withSubstantialContent}/${pages.length} pages have \u2265100 words of body text (avg ${avgWords})`,
5009
+ suggestion: "Generative engines can't cite pages that are mostly images or short copy. Add at least 100 words of substantive text content per page.",
5010
+ metadata: { withSubstantialContent, avgWords }
5011
+ };
5012
+ }
5013
+ };
4949
5014
  var pageSizeHtml = {
4950
5015
  id: "page-size-html",
4951
5016
  name: "Page Size (HTML)",
@@ -5080,6 +5145,7 @@ var contentStartPosition = {
5080
5145
  };
5081
5146
  var pageSizeChecks = [
5082
5147
  renderingStrategy,
5148
+ substantialTextContent,
5083
5149
  pageSizeHtml,
5084
5150
  pageSizeMarkdown,
5085
5151
  contentStartPosition
@@ -5251,11 +5317,103 @@ var tabbedContentSerialization = {
5251
5317
  };
5252
5318
  }
5253
5319
  };
5320
+ var imageAltText = {
5321
+ id: "image-alt-text",
5322
+ name: "Image Alt Text Coverage",
5323
+ category: "content-structure",
5324
+ description: "Checks that at least 50% of images have descriptive alt text",
5325
+ weight: 0.5,
5326
+ run: async (ctx) => {
5327
+ const pages = ctx.sampledPages.slice(0, 10);
5328
+ if (pages.length === 0) {
5329
+ return {
5330
+ id: "image-alt-text",
5331
+ name: "Image Alt Text Coverage",
5332
+ category: "content-structure",
5333
+ status: "skip",
5334
+ message: "No pages sampled"
5335
+ };
5336
+ }
5337
+ const allImages = pages.flatMap((p) => extractImages(p.html));
5338
+ const contentImages = allImages.filter((img) => img.alt === void 0 || img.alt.trim().length > 0);
5339
+ const decorativeImages = allImages.length - contentImages.length;
5340
+ const withAlt = contentImages.filter((img) => img.alt !== void 0 && img.alt.trim().length > 0).length;
5341
+ if (allImages.length === 0) {
5342
+ return {
5343
+ id: "image-alt-text",
5344
+ name: "Image Alt Text Coverage",
5345
+ category: "content-structure",
5346
+ status: "info",
5347
+ message: `No images found across ${pages.length} sampled pages`
5348
+ };
5349
+ }
5350
+ if (contentImages.length === 0) {
5351
+ return {
5352
+ id: "image-alt-text",
5353
+ name: "Image Alt Text Coverage",
5354
+ category: "content-structure",
5355
+ status: "info",
5356
+ message: `All ${allImages.length} sampled images are decorative (alt="")`,
5357
+ metadata: { decorativeImages, totalImages: allImages.length }
5358
+ };
5359
+ }
5360
+ const ratio = withAlt / contentImages.length;
5361
+ const pct = Math.round(ratio * 100);
5362
+ const summary = `${withAlt}/${contentImages.length} content images have descriptive alt text (${pct}%)${decorativeImages > 0 ? `; ${decorativeImages} decorative skipped` : ""}`;
5363
+ if (ratio >= 0.5) {
5364
+ return {
5365
+ id: "image-alt-text",
5366
+ name: "Image Alt Text Coverage",
5367
+ category: "content-structure",
5368
+ status: "pass",
5369
+ message: summary,
5370
+ metadata: { withAlt, contentImages: contentImages.length, decorativeImages, pct }
5371
+ };
5372
+ }
5373
+ return {
5374
+ id: "image-alt-text",
5375
+ name: "Image Alt Text Coverage",
5376
+ category: "content-structure",
5377
+ status: ratio >= 0.25 ? "warn" : "fail",
5378
+ message: summary,
5379
+ suggestion: `Add descriptive alt text to at least 50% of content images. AI agents and screen readers rely on alt text to understand visual content. Mark purely decorative images with alt="" so they don't dilute the ratio.`,
5380
+ metadata: { withAlt, contentImages: contentImages.length, decorativeImages, pct }
5381
+ };
5382
+ }
5383
+ };
5254
5384
  var contentStructureChecks = [
5255
5385
  markdownCodeFenceValidity,
5256
5386
  sectionHeaderQuality,
5257
- tabbedContentSerialization
5387
+ tabbedContentSerialization,
5388
+ imageAltText
5258
5389
  ];
5390
+ var httpsEnabled = {
5391
+ id: "https-enabled",
5392
+ name: "HTTPS Enabled",
5393
+ category: "url-stability",
5394
+ description: "Checks if the site is served over HTTPS",
5395
+ weight: 0.7,
5396
+ requiresNetwork: true,
5397
+ run: async (ctx) => {
5398
+ if (ctx.baseUrl.protocol === "https:") {
5399
+ return {
5400
+ id: "https-enabled",
5401
+ name: "HTTPS Enabled",
5402
+ category: "url-stability",
5403
+ status: "pass",
5404
+ message: "Site is served over HTTPS"
5405
+ };
5406
+ }
5407
+ return {
5408
+ id: "https-enabled",
5409
+ name: "HTTPS Enabled",
5410
+ category: "url-stability",
5411
+ status: "fail",
5412
+ message: `Site is served over ${ctx.baseUrl.protocol.replace(":", "")} \u2014 AI crawlers de-prioritize non-HTTPS sources`,
5413
+ suggestion: "Serve your site over HTTPS. AI crawlers like GPTBot, ClaudeBot, and PerplexityBot strongly prefer HTTPS and may skip plain HTTP entirely."
5414
+ };
5415
+ }
5416
+ };
5259
5417
  var httpStatusCodes = {
5260
5418
  id: "http-status-codes",
5261
5419
  name: "HTTP Status Codes",
@@ -5367,6 +5525,7 @@ var cacheHeaderHygiene = {
5367
5525
  }
5368
5526
  };
5369
5527
  var urlStabilityChecks = [
5528
+ httpsEnabled,
5370
5529
  httpStatusCodes,
5371
5530
  redirectBehavior,
5372
5531
  cacheHeaderHygiene
@@ -5772,6 +5931,185 @@ var faqSchema = {
5772
5931
  };
5773
5932
  }
5774
5933
  };
5934
+ var metaDescription = {
5935
+ id: "meta-description",
5936
+ name: "Meta Description",
5937
+ category: "geo-signals",
5938
+ description: "Checks for a meta description between 50 and 160 characters",
5939
+ weight: 0.5,
5940
+ run: async (ctx) => {
5941
+ const pages = ctx.sampledPages.slice(0, 10);
5942
+ if (pages.length === 0) {
5943
+ return {
5944
+ id: "meta-description",
5945
+ name: "Meta Description",
5946
+ category: "geo-signals",
5947
+ status: "skip",
5948
+ message: "No pages sampled"
5949
+ };
5950
+ }
5951
+ let withGoodDescription = 0;
5952
+ let missing = 0;
5953
+ let tooShort = 0;
5954
+ let tooLong = 0;
5955
+ for (const page of pages) {
5956
+ const meta = extractMetaTags(page.html);
5957
+ const description = meta["description"]?.trim();
5958
+ if (!description) {
5959
+ missing++;
5960
+ continue;
5961
+ }
5962
+ const len = description.length;
5963
+ if (len >= 50 && len <= 160) withGoodDescription++;
5964
+ else if (len < 50) tooShort++;
5965
+ else tooLong++;
5966
+ }
5967
+ if (withGoodDescription === pages.length) {
5968
+ return {
5969
+ id: "meta-description",
5970
+ name: "Meta Description",
5971
+ category: "geo-signals",
5972
+ status: "pass",
5973
+ message: `All ${pages.length} pages have a meta description between 50\u2013160 characters`,
5974
+ metadata: { withGoodDescription }
5975
+ };
5976
+ }
5977
+ if (missing === pages.length) {
5978
+ return {
5979
+ id: "meta-description",
5980
+ name: "Meta Description",
5981
+ category: "geo-signals",
5982
+ status: "fail",
5983
+ message: "No meta description found on any sampled page",
5984
+ suggestion: 'Add a <meta name="description"> between 50 and 160 characters to every page. Generative engines quote meta descriptions when summarizing your content.'
5985
+ };
5986
+ }
5987
+ const detail = [
5988
+ missing > 0 ? `${missing} missing` : null,
5989
+ tooShort > 0 ? `${tooShort} too short` : null,
5990
+ tooLong > 0 ? `${tooLong} too long` : null
5991
+ ].filter(Boolean).join(" \xB7 ");
5992
+ return {
5993
+ id: "meta-description",
5994
+ name: "Meta Description",
5995
+ category: "geo-signals",
5996
+ status: missing >= pages.length / 2 ? "fail" : "warn",
5997
+ message: `${withGoodDescription}/${pages.length} pages have meta descriptions in the 50\u2013160 char range${detail ? ` \xB7 ${detail}` : ""}`,
5998
+ suggestion: missing > 0 ? 'Add a <meta name="description"> between 50 and 160 characters to every page. Some pages are missing it entirely.' : "Aim for 50\u2013160 characters. Shorter descriptions lack context for AI; longer ones get truncated.",
5999
+ metadata: { withGoodDescription, missing, tooShort, tooLong }
6000
+ };
6001
+ }
6002
+ };
6003
+ var openGraphTags = {
6004
+ id: "open-graph-tags",
6005
+ name: "Open Graph Tags",
6006
+ category: "geo-signals",
6007
+ description: "Checks for og:title, og:description, og:image, and og:url",
6008
+ weight: 0.5,
6009
+ run: async (ctx) => {
6010
+ const pages = ctx.sampledPages.slice(0, 10);
6011
+ if (pages.length === 0) {
6012
+ return {
6013
+ id: "open-graph-tags",
6014
+ name: "Open Graph Tags",
6015
+ category: "geo-signals",
6016
+ status: "skip",
6017
+ message: "No pages sampled"
6018
+ };
6019
+ }
6020
+ const required = ["og:title", "og:description", "og:image", "og:url"];
6021
+ let fullCoverage = 0;
6022
+ let partialCoverage = 0;
6023
+ const missingCounts = { "og:title": 0, "og:description": 0, "og:image": 0, "og:url": 0 };
6024
+ for (const page of pages) {
6025
+ const meta = extractMetaTags(page.html);
6026
+ const missing = required.filter((tag) => !meta[tag]);
6027
+ for (const tag of missing) missingCounts[tag] = (missingCounts[tag] ?? 0) + 1;
6028
+ if (missing.length === 0) fullCoverage++;
6029
+ else if (missing.length < required.length) partialCoverage++;
6030
+ }
6031
+ if (fullCoverage === pages.length) {
6032
+ return {
6033
+ id: "open-graph-tags",
6034
+ name: "Open Graph Tags",
6035
+ category: "geo-signals",
6036
+ status: "pass",
6037
+ message: `All ${pages.length} pages have complete Open Graph tags`
6038
+ };
6039
+ }
6040
+ const mostMissing = Object.entries(missingCounts).filter(([, n]) => n > 0).sort(([, a], [, b]) => b - a).map(([tag]) => tag);
6041
+ const noneCovered = pages.length - fullCoverage - partialCoverage;
6042
+ return {
6043
+ id: "open-graph-tags",
6044
+ name: "Open Graph Tags",
6045
+ category: "geo-signals",
6046
+ status: fullCoverage + partialCoverage === 0 ? "fail" : "warn",
6047
+ message: `${fullCoverage}/${pages.length} pages have complete Open Graph tags${partialCoverage > 0 ? ` \xB7 ${partialCoverage} partial` : ""}${noneCovered > 0 ? ` \xB7 ${noneCovered} with none` : ""}${mostMissing.length > 0 ? ` \xB7 most often missing: ${mostMissing.slice(0, 2).join(", ")}` : ""}`,
6048
+ suggestion: "Add og:title, og:description, og:image, and og:url to every page. AI engines and link previews use these to render rich citations of your content.",
6049
+ metadata: { fullCoverage, partialCoverage, noneCovered, missingCounts }
6050
+ };
6051
+ }
6052
+ };
6053
+ var externalCitations = {
6054
+ id: "external-citations",
6055
+ name: "External Citations",
6056
+ category: "geo-signals",
6057
+ description: "Checks for at least 2 outbound links to external sources per page",
6058
+ weight: 0.5,
6059
+ run: async (ctx) => {
6060
+ if (ctx.mode === "local") {
6061
+ return {
6062
+ id: "external-citations",
6063
+ name: "External Citations",
6064
+ category: "geo-signals",
6065
+ status: "info",
6066
+ message: "External link detection requires a live origin to compare against"
6067
+ };
6068
+ }
6069
+ const pages = ctx.sampledPages.slice(0, 10);
6070
+ if (pages.length === 0) {
6071
+ return {
6072
+ id: "external-citations",
6073
+ name: "External Citations",
6074
+ category: "geo-signals",
6075
+ status: "skip",
6076
+ message: "No pages sampled"
6077
+ };
6078
+ }
6079
+ const origin = ctx.baseUrl.origin;
6080
+ let pagesWithCitations = 0;
6081
+ let totalExternal = 0;
6082
+ for (const page of pages) {
6083
+ const links = extractLinks(page.html, origin);
6084
+ const external = links.filter((l) => {
6085
+ const u = new URL(l);
6086
+ return u.protocol.startsWith("http") && u.origin !== origin;
6087
+ });
6088
+ totalExternal += external.length;
6089
+ if (external.length >= 2) pagesWithCitations++;
6090
+ }
6091
+ const avgExternal = Math.round(totalExternal / pages.length);
6092
+ if (pagesWithCitations >= pages.length * 0.7) {
6093
+ return {
6094
+ id: "external-citations",
6095
+ name: "External Citations",
6096
+ category: "geo-signals",
6097
+ status: "pass",
6098
+ message: `${pagesWithCitations}/${pages.length} pages have \u22652 outbound links (avg ${avgExternal}/page)`,
6099
+ metadata: { pagesWithCitations, avgExternal }
6100
+ };
6101
+ }
6102
+ return {
6103
+ id: "external-citations",
6104
+ name: "External Citations",
6105
+ category: "geo-signals",
6106
+ status: pagesWithCitations > 0 ? "warn" : "fail",
6107
+ message: `Only ${pagesWithCitations}/${pages.length} pages have \u22652 outbound links (avg ${avgExternal}/page)`,
6108
+ suggestion: "Add at least 2 outbound links to authoritative external sources per page. Citing sources signals credibility to generative engines, which weigh outbound links when deciding what to cite.",
6109
+ metadata: { pagesWithCitations, avgExternal }
6110
+ };
6111
+ }
6112
+ };
5775
6113
  var canonicalUrlConsistency = {
5776
6114
  id: "canonical-url-consistency",
5777
6115
  name: "Canonical URL Consistency",
@@ -5842,6 +6180,9 @@ var geoSignalChecks = [
5842
6180
  contentFreshness,
5843
6181
  eeatSignals,
5844
6182
  faqSchema,
6183
+ metaDescription,
6184
+ openGraphTags,
6185
+ externalCitations,
5845
6186
  canonicalUrlConsistency
5846
6187
  ];
5847
6188
  var mcpServerCard = {
@@ -7512,7 +7853,7 @@ var App = ({
7512
7853
  const config = { categories, sampleSize, onEvent };
7513
7854
  const res = isLocal ? await auditLocal(target, config) : await audit(target, config);
7514
7855
  if (isLocal) {
7515
- setNetworkSkipped(35 - res.summary.total);
7856
+ setNetworkSkipped(ALL_CHECKS.length - res.summary.total);
7516
7857
  }
7517
7858
  setResult(res);
7518
7859
  setPhase("done");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentimization",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "GEO audit CLI — check if your website is agent-ready",
5
5
  "license": "MIT",
6
6
  "author": "Anthony Lio <hello@antl.io>",