seo: blog index H1 + HowTo schema for tutorial posts#383
Conversation
Audit findings on /blog (index page):
- Missing H1 (only H2/H3 cards present).
- Title ran 25 characters: "Engineering | Keploy Blog" — too short, missed
primary keywords. Replaced with H1 "Keploy Engineering Blog" and title
"Keploy Blog — API Testing, Test Automation & eBPF Deep-Dives" (57c).
HowTo schema integration:
- New lib/howToSchema.ts builds a HowTo JSON-LD object from a post when:
1. The post has an explicit `howto_steps` post-meta JSON array, OR
2. The post is tagged howto / how-to / tutorial OR has `is_tutorial: true`,
in which case steps are extracted from h2/h3 + first <p> in the body.
- Returns null for non-tutorial posts and for posts where fewer than 2 steps
could be derived (avoids invalid HowTo emission).
- Wired into both community and technology post templates: appended into the
existing structuredData[] array which the Meta component already maps to
JSON-LD <script> tags. No template plumbing changes.
- Stable selection of 5 candidate posts to tag on the WordPress side — see PR
description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds two SEO-focused changes to the blog: a stronger homepage heading/title and automatic HowTo JSON-LD generation for tutorial posts on article pages.
Changes:
- Replaces the homepage hero heading with an H1 and updates the browser
<title>copy. - Introduces
lib/howToSchema.tsto derive HowTo structured data from post content or tutorial metadata. - Appends the new HowTo schema to community and technology post pages when applicable.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
pages/index.tsx |
Updates homepage title tag and hero heading level/copy. |
pages/community/[slug].tsx |
Adds HowTo schema generation to community post structured data. |
pages/technology/[slug].tsx |
Adds HowTo schema generation to technology post structured data. |
lib/howToSchema.ts |
New helper that detects tutorial posts and builds HowTo JSON-LD. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- pages/index.tsx: pass the canonical SEO title via Layout's Title
prop (was still passing "Blog - Keploy") so og:title and
twitter:title match the rendered <title> instead of the stale
marketing string. Drops the duplicate <Head><title> override.
- lib/howToSchema.ts:
* remove dead activation paths. The `is_tutorial` and
`howto_steps` fields are not exposed by WPGraphQL on this
site, so the two custom-meta branches never fired. Detection
is now tag-based only (which rides on the existing fragment).
* drop step.url. Heading anchor ids are attached client-side in
components/post-body.tsx — the SSR HTML crawlers see has no
such ids, so any `#slug` we emitted was an invalid anchor.
- pages/index.tsx: pass the canonical SEO title via Layout's Title
prop (was still passing "Blog - Keploy") so og:title and
twitter:title match the rendered <title> instead of the stale
marketing string. Drops the duplicate <Head><title> override.
- lib/howToSchema.ts:
* remove dead activation paths. The `is_tutorial` and
`howto_steps` fields are not exposed by WPGraphQL on this
site, so the two custom-meta branches never fired. Detection
is now tag-based only (which rides on the existing fragment).
* drop step.url. Heading anchor ids are attached client-side in
components/post-body.tsx — the SSR HTML crawlers see has no
such ids, so any `#slug` we emitted was an invalid anchor.
9d85c66 to
955bbcd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- pages/index.tsx: restore <Head><title> for /blog. Removing it earlier
was a regression — components/meta.tsx renders og/twitter title from
the Layout `Title` prop but does NOT emit a <title> tag (same gap
the LIVE-11 author-page comment documents). Both sources now use the
same canonical SEO string.
- lib/howToSchema.ts: fix tag matching — slug isn't queried, only
`tags.edges.node.name`. The `name || slug` fallback was dead and
WordPress display-name tags like "How To" never matched the candidate
bucket. Tag names are now normalized (lowercase + whitespace→`-`)
before checking against {howto, how-to, tutorial}.
- lib/howToSchema.ts: HowTo `name` and `description` now come from
caller-sanitized values (sanitizeTitle / getSafeDescription) instead
of raw WordPress fields, matching the BlogPosting schema on the same
pages so encoded entities and inline markup don't ship in JSON-LD.
- pages/{community,technology}/[slug].tsx: pass safeTitle/safeDescription
through to the new helper signature.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The Layout `Title` prop now feeds og:title with the canonical `Keploy Blog — API Testing, Test Automation & eBPF Deep-Dives` SEO string, replacing the prior `Blog - Keploy`. The Playwright homepage assertion is updated to match.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
stripHtml() was only decoding the named entity set (&, <, etc.) plus ', so curly apostrophes (’), en/em dashes (– / —), and curly quotes (“ / ”) leaked into HowTo step text as literal entity strings. Search engines and the rich-results test treat that as broken markup. Mirror the decode list from utils/seo.ts so the JSON-LD and the meta description stay consistent.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The HowTo schema is serialized via JSON.stringify and injected into a <script type="application/ld+json"> via dangerouslySetInnerHTML. Decoding </> back to raw < / > inside stripHtml meant a tutorial paragraph containing </script> would emit a literal </script> in the JSON-LD body and prematurely close the script tag — a markup-integrity hole that defeats the page's XSS defences. Stop decoding those two entities; tags were already stripped upstream so any remaining angle brackets came from content and are safer left as < in the script payload. Also collapse the two parallel decode lists. utils/seo.ts already owns the WordPress entity sanitizer used by titles and descriptions; export it and reuse it from lib/howToSchema.ts so future entity changes don't have to be made in two places.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The Playwright SEO suite was asserting `og:title` matches /^Keploy Blog\b/ — a prefix that would still pass if the keyword-rich suffix were dropped or the new "Keploy Engineering Blog" H1 reverted. Replace with exact-string assertions on og:title, document title, and the visible H1 so a regression in the homepage SEO rewrite trips CI. Add tests/lib/howToSchema.test.ts covering getHowToSchema directly: tutorial-tag detection (case/whitespace variants), non-tutorial null, < 2-step null, WordPress numeric-entity decoding, mainEntityOfPage, and missing-input null handling. The e2e suite couldn't reach this path because there are no tutorial-tagged posts in WP today, so the helper could silently stop emitting (or start emitting on every post) without anything failing. Wire `npm run test:unit` to run the new tsx-based tests via node:test. Signed-off-by: Neha Gupta <gneha21@yahoo.in>
| // ’ (curly apostrophe), – (en dash), and the structural | ||
| // </> pair WP emits for inline code blocks. Assert the entities | ||
| // are replaced with proper Unicode and that no `<`/`>` literals | ||
| // leak into the JSON-LD payload. |
Summary
Two fixes from the SEO/GEO/AEO audit on
keploy.io/blog.Blog index H1 + title rewrite
/blogwas rendering zero H1 (only H2/H3 cards). Title ran 25 characters:Engineering | Keploy Blog— too short, missing primary keywords like "API testing" or "test automation".Keploy Engineering Blog. New title:Keploy Blog — API Testing, Test Automation & eBPF Deep-Dives(57c).<title>(in<Head>onpages/index.tsx) and Layout'sTitleprop — whichcomponents/meta.tsxuses to buildog:title/twitter:title— now use this same canonical string, so social/SEO metadata and the document title agree.HowTo schema for tutorial posts
lib/howToSchema.tsbuilds aHowToJSON-LD object from a post when one of its tags normalizes tohowto/how-to/tutorial(the normalize step lowercases + replaces whitespace with-so a WordPress display tag like "How To" matches).<p>that follows. Capped at 110 chars forname, 480 fortext.safeTitle/safeDescriptionfrom the caller (the same values used forBlogPosting), so encoded WordPress entities and inline markup never end up in the JSON-LD.nullfor non-tutorial posts (and for posts where fewer than 2 steps could be derived) — no invalid schema is emitted.pages/community/[slug].tsxandpages/technology/[slug].tsx: appended to the existingstructuredData[]array.Note: the helper does not consume custom WordPress meta (
howto_steps/is_tutorial). Exposing those over WPGraphQL would need a server-sideregister_graphql_fieldregistration that doesn't exist on this site today; if/when WP adds it, the helper can be extended in a follow-up to read authored steps.Test plan
npx tsc --noEmitreports no new errors — verified (1 pre-existing PNG-import error unchanged)tests/e2e/SeoMeta.spec.ts—og:titleregex updated to match the new title; suite still passeshowto(or "How To") tag (their slugs):community/how-to-do-java-unit-testing-effectivelycommunity/how-to-generate-test-cases-with-automation-toolscommunity/how-to-mock-backend-of-selenium-tests-using-keploycommunity/getting-started-with-keploycommunity/how-to-do-frontend-test-automation-using-selenium"@type":"HowTo"JSON-LD/blogpage source shows<h1>Keploy Engineering Blog</h1>and<title>Keploy Blog — API Testing, Test Automation & eBPF Deep-Dives</title>🤖 Generated with Claude Code