# SEO Meta Tag Audit - Titles, Descriptions, Canonicals & Schema (`wood-between-studio/seo-meta-audit`) Actor

Bulk on-page SEO audit. Checks titles, meta descriptions, canonicals, H1s, Open Graph, hreflang, JSON-LD and alt text, and scores every page 0-100.

- **URL**: https://apify.com/wood-between-studio/seo-meta-audit.md
- **Developed by:** [Wood Between Studio](https://apify.com/wood-between-studio) (community)
- **Categories:** SEO tools, Marketing, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 page auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SEO Meta Tag Audit: Titles, Descriptions, Canonicals & Schema

**Audit every page's title, meta description, canonical, H1, Open Graph tags, hreflang and schema in one run, and get a worst-first list of fixes with a 0-100 score per page.**

This is a bulk on-page SEO check. Give it a site (or a sitemap, or a list of pages) and it checks the things that usually go wrong: missing or overlong titles, duplicate meta descriptions, canonicals that point somewhere else, pages with no H1 or three of them, missing Open Graph images, invalid JSON-LD and images without alt text. Every page gets a 0–100 score and a list of issues written in plain English.

There's no browser and no AI, just quick HTTP fetches and rule-based checks. That keeps it cheap enough to run on every page of a site every week.

### Who it's for

- **Freelancers and agencies** who need an audit before quoting a client. The HTML report in the key-value store is ready to send.
- **Site owners** who want to know which pages need new titles and descriptions, sorted worst first.
- **Developers** running a check before or after a launch, a theme change or a migration. Compare two runs to catch regressions such as a lost canonical or a new `noindex`.
- **Content teams** hunting duplicate titles and descriptions across hundreds of posts.

### What it checks

| Area | Checks |
|---|---|
| Title | missing, under 30 or over 60 characters, multiple title tags, **duplicates across pages** |
| Meta description | missing, under 70 or over 160 characters, **duplicates across pages** |
| Indexing | `noindex` / `nofollow` in meta robots or the `X-Robots-Tag` header, HTTP errors, redirects |
| Canonical | missing, more than one, pointing to another URL, invalid |
| Headings | no H1, empty H1, several H1s, H2 count |
| Mobile & basics | viewport tag, `lang` attribute, charset, favicon |
| Social | `og:title`, `og:description`, `og:image` (and whether it's absolute), `twitter:card` |
| International | hreflang list, invalid language codes, missing `x-default` |
| Structured data | JSON-LD and microdata types found, JSON-LD that doesn't parse |
| Images | images without an `alt` attribute |
| Other | mixed http content on https pages, thin pages (under 200 words), HTML size, slow server response |

Scoring: start at 100, subtract 15 per error, 5 per warning and 1 per notice. Pages that return an HTTP error score 0. The score is for sorting and tracking over time. It isn't a ranking prediction.

### Input

```json
{
  "startUrls": [{ "url": "https://www.woodbetweenstudio.com" }],
  "mode": "crawl",
  "maxPages": 100,
  "excludeUrlPattern": "/tag/|/page/\\d+"
}
```

| Field | What it does | Default |
|---|---|---|
| `startUrls` | Homepage, sitemap URL or list of pages | (required) |
| `mode` | `crawl` follows internal links, `sitemap` reads the XML sitemap, `list` audits only your URLs | `crawl` |
| `maxPages` | Maximum pages to audit (each is a billed event) | `100` |
| `includeUrlPattern` / `excludeUrlPattern` | Regex filters | empty |
| `respectRobotsTxt` | Skip disallowed pages while crawling | `true` |
| `concurrency` / `perHostConcurrency` | Parallel fetches overall and per server | `5` / `2` |
| `timeoutSecs` | Per-page timeout | `30` |

### Output

One row per page, sorted from the lowest score up:

```json
{
  "url": "https://www.woodbetweenstudio.com/about/",
  "finalUrl": null,
  "statusCode": 200,
  "responseTimeMs": 187,
  "title": "About Wood Between Studio",
  "titleLength": 25,
  "metaDescription": "Make room for what matters. Wood Between Studio is a small, independent studio making planners, budget spreadsheets, vintage wall art, Frame TV art and calm videos.",
  "metaDescriptionLength": 164,
  "indexable": true,
  "canonical": "https://www.woodbetweenstudio.com/about/",
  "canonicalIsSelf": true,
  "h1": "About Wood Between Studio",
  "h1Count": 1,
  "h2Count": 4,
  "lang": "en",
  "favicon": "https://www.woodbetweenstudio.com/favicon.ico",
  "openGraph": {
    "title": "About Wood Between Studio",
    "description": "Make room for what matters. Wood Between Studio is a small, independent studio making planners, budget spreadsheets, vintage wall art, Frame TV art and calm videos.",
    "image": "https://www.woodbetweenstudio.com/img/og-default.jpg",
    "type": "website",
    "url": "https://www.woodbetweenstudio.com/about/",
    "siteName": "Wood Between Studio"
  },
  "twitter": {
    "card": "summary_large_image",
    "title": "About Wood Between Studio",
    "image": "https://www.woodbetweenstudio.com/img/og-default.jpg"
  },
  "hreflang": [],
  "structuredDataTypes": [
    "Organization"
  ],
  "images": 2,
  "imagesMissingAlt": 0,
  "internalLinks": 27,
  "externalLinks": 14,
  "wordCount": 498,
  "htmlSizeKb": 8.6,
  "issues": [
    {
      "code": "title-short",
      "severity": "warning",
      "message": "Title is short (25 chars; aim for 30-60)"
    },
    {
      "code": "description-long",
      "severity": "warning",
      "message": "Meta description is long (164 chars; likely truncated past ~160)"
    }
  ],
  "score": 90,
  "errors": 0,
  "warnings": 2,
  "notices": 0,
  "issueCodes": [
    "title-short",
    "description-long"
  ]
}
```

The key-value store also gets:

- `SUMMARY`: pages audited, average score, and how many pages have each issue.
- `REPORT`: an HTML report with issues by type and every page, worst first.

### Pricing

In plain words: **$4 per 1,000 pages audited** (that's $0.004 per page), plus Apify's small standard Actor start fee. A 100-page site costs about $0.40.

- You only pay for pages that are actually audited and saved.
- Set `maxPages`, or a maximum cost per run, and the Actor won't go past it.
- No headless browser is used, so platform usage stays very low.

### FAQ

**Does it render JavaScript?**
No. It reads the HTML the server returns, which is also what most search engines see first. If your titles or meta tags are added only by JavaScript, the audit reports them as missing, and you'll want to know that anyway.

**Are the character limits exact?**
No. Google truncates by pixel width, not by character count. 60 characters for titles and 160 for descriptions are safe rules of thumb, so long titles are flagged as warnings, not errors.

**Does it check page speed or Core Web Vitals?**
It records server response time and HTML size. For full Lighthouse metrics, use a dedicated tool. This Actor sticks to meta tags and markup.

**Does it validate structured data against Google's rich-result rules?**
It lists the schema types it finds and flags JSON-LD that doesn't parse. It doesn't run Google's rich-result eligibility rules.

**Can I audit a site I don't own?**
Auditing public pages is like viewing the page source in your browser. The Actor respects robots.txt and keeps load low by default. Follow each site's terms.

**Crawl or sitemap mode?**
Sitemap mode audits what you tell search engines exists. Crawl mode audits what a visitor can reach by clicking. Running both and comparing them is a quick way to find orphan pages.

### Related tools

These work well together for a quick technical SEO check:

- **[Broken Link Checker](https://apify.com/wood-between-studio/broken-link-checker)**: find 404 errors, dead links and redirect chains across a whole site.
- **[Sitemap & RSS Feed Exporter](https://apify.com/wood-between-studio/sitemap-feed-exporter)**: export a sitemap.xml or RSS feed to CSV, Excel or JSON.

Typical workflow: export the sitemap first, then run this Actor in list mode on exactly the pages you care about.

***

Made by Wood Between Studio.

# Actor input Schema

## `startUrls` (type: `array`):

Crawl mode: a homepage or section to start from. Sitemap mode: a homepage or a sitemap.xml URL. List mode: the exact pages to audit.

## `mode` (type: `string`):

Crawl follows internal links. Sitemap reads the site's XML sitemap. List audits only the URLs you entered.

## `maxPages` (type: `integer`):

Maximum pages to audit. Each audited page is one billed event.

## `includeUrlPattern` (type: `string`):

Optional. Example: /blog/

## `excludeUrlPattern` (type: `string`):

Optional. Example: /tag/|/page/\d+

## `respectRobotsTxt` (type: `boolean`):

Skip pages robots.txt disallows (crawl mode).

## `concurrency` (type: `integer`):

Pages fetched in parallel.

## `perHostConcurrency` (type: `integer`):

Keeps the load on the audited server low.

## `timeoutSecs` (type: `integer`):

How long to wait for each page.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.woodbetweenstudio.com"
    }
  ],
  "mode": "crawl",
  "maxPages": 100,
  "respectRobotsTxt": true,
  "concurrency": 5,
  "perHostConcurrency": 2,
  "timeoutSecs": 30
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `report` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://www.woodbetweenstudio.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("wood-between-studio/seo-meta-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "startUrls": [{ "url": "https://www.woodbetweenstudio.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("wood-between-studio/seo-meta-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://www.woodbetweenstudio.com"
    }
  ]
}' |
apify call wood-between-studio/seo-meta-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,wood-between-studio/seo-meta-audit"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/nGeeLo8AnCI5iWwvz/builds/mIR9f5uMIJi6i0y7h/openapi.json
