Skip to content

Knoji · Demand.io/Professional

Adding structured data to Knoji's legacy codebase

How I expanded structured data in an SEO-sensitive procedural PHP platform without rewriting its page pipeline

Role
Technical lead and primary implementer
Period
October 2024 to June 2025; team follow-up through November 2025

Context and constraints

Knoji's merchant pages combine coupon codes, offers, merchant policies, ratings, FAQs, and checkout guidance. The same information serves two audiences: people deciding whether to trust and use a promotion, and machines trying to understand the page.

I led the team responsible for this area of the platform and was the primary implementer of the structured-data work described here. The implementation happened in two phases: FAQ consolidation in October 2024, then merchant-schema expansion from May to June 2025. The team continued maintaining adjacent SEO and schema behavior through November 2025.

This was not a greenfield application. Merchant pages were assembled from large procedural PHP templates, shared variables, direct database results, and many conditional commercial modules. During the first phase, two near-duplicate merchant layouts were active. Promotion ordering was also business logic: a code's position depended on health, type, placement rules, and the modules already rendered above it.

Rewriting that pipeline would have mixed an SEO change with a much larger product migration. I instead needed to improve the machine-readable output while preserving the visible page and its ordering behavior.

The problem

The initial problem was duplicated FAQ semantics. Merchant templates contained a static coupon FAQ, while dynamic Merchant Information Questions, or MIQs, were fetched and rendered separately. Structured markup reflected those separate paths instead of presenting one FAQ entity.

The first attempt exposed the real maintenance problem. Adding an FAQPage wrapper was easy; determining when all questions were available, keeping both layouts aligned, and safely serializing dynamic answers was not. Hand-interpolated JSON could be broken by punctuation or HTML in merchant content, and emitting too early meant the MIQ records had not yet been collected.

That work exposed missing coverage. Knoji already emitted some page-level schema, but its structured data did not include merchant policies, normalized offers, coupon tables, or checkout instructions.

Investigation

I traced the rendered page backward from views/header-head.php, which globally included views/schema.php, into both merchant layouts and their shared components. The useful domain model was implicit in template variables rather than represented by classes or services:

  • Static questions were defined where their HTML was rendered.
  • MIQ answers arrived from a separate query and component.
  • Promotion groups such as healthy codes, old codes, link deals, and sticky placements were mutated while the page was assembled.
  • Merchant policies, customer discounts, ratings, sidebar products, and coupon-table data each used different structures.

I decided to adapt the structured data to the existing rendering pipeline. Shared FAQ records could feed both visible and machine-readable output. Promotions needed a normalized intermediate representation before they could map to Offer entities.

Before

static FAQ template --------> visible FAQ
MIQ query + component ------> visible MIQ markup
promotion arrays -----------> procedural placement
schema.php -----------------> limited independent JSON-LD blocks

After

static FAQ records --+------> visible FAQ
MIQ records ---------+------> one FAQPage encoder

promotion arrays ---> normalized display records ---> Offer / ItemList schemas
merchant data -------------------------------------> Organization / Product
visible tables and instructions -------------------> Dataset / HowTo

Options considered

I considered three boundaries for the work:

  1. Patch each template independently. This minimized initial movement but preserved duplicate FAQ logic and would make every schema addition layout-specific.
  2. Replace the merchant-page pipeline. This could produce a cleaner architecture, but it dramatically increased the regression surface around promotion ordering, seasonal modules, tracking links, and SEO-sensitive output.
  3. Add a compatibility layer. Keep the established rendering flow, normalize the data needed by structured output, and centralize JSON-LD serialization.

I chose the third option. It improved one cross-cutting concern without pretending the surrounding codebase had already been modernized.

Architecture decision

The decision had two parts.

First, represent FAQs as records with a question and an answer, where an answer could be a string or a closure over page data. Static records still drove their visible HTML. MIQ records were collected during rendering, then both sets were passed to a shared faq_lds.php emitter after the required data existed.

Second, introduce a normalization step for promotions. _code_sorter.php copied heterogeneous promotion objects and added common schema-facing fields such as name, description, url, couponCode, discount values, validity dates, display type, and display order. schema.php could then map those records into Schema.org arrays without rewriting the visible placement system.

All blocks flowed through an outputJsonLd() helper backed by json_encode, rather than assembling JSON fragments by string interpolation. A separate numeric formatter forced a dot decimal separator and removed locale-dependent thousands separators from schema prices.

I left the visible rendering pipeline, database access, and existing merchant variables in place to limit regressions. Schema generation still depends on template globals, which a later migration should remove.

Implementation and rollout

Phase 1: one FAQ representation

Across six commits from October 7 to October 10, 2024, I moved from an initial MIQ Microdata wrapper to a shared FAQPage JSON-LD emitter. The sequence matters because it records the corrections:

  • Consolidated static and MIQ entries instead of emitting competing FAQ blocks.
  • Moved emission to the bottom of each layout so all dynamic answers were available.
  • Added the shared path to the legacy layout after initially covering only the newer variant.
  • Replaced interpolated entry fragments with json_encode.
  • Removed a temporary 12-question cap so the output represented the full combined FAQ.

The experimental layout was later removed when the team rolled back its split test. Because the old layout already used the shared emitter, the active page retained the consolidated behavior.

Phase 2: merchant schema expansion

The May 2025 consolidation extended the same approach beyond FAQs. Merchant pages gained structured representations for:

  • the merchant as an Organization;
  • coupon availability as a Product with AggregateOffer and individual Offer records;
  • ordered promotions as an ItemList;
  • page identity as a WebPage;
  • related sidebar products and their offers.

Follow-up work mapped merchant policies and customer discounts to PropertyValue records, modeled the two visible coupon tables as Dataset structures, and mirrored the three visible checkout instructions as HowToStep records. The Dataset and HowTo changes were co-authored with GitHub Copilot; I remained responsible for integrating and reviewing them in the legacy page flow.

We did not rewrite all schema in one release. Small follow-up commits corrected output structure. Commit 22669787 hardened decimal serialization after we found locale-sensitive prices.

Team follow-up

The work continued as team ownership rather than ending with my last implementation commit. Teammates added seasonal content that flowed through the shared FAQ representation, separated sitemap inclusion from page indexability, corrected visible coupon counts, and revised reliability language for compliance. Because visible FAQs and JSON-LD shared data, the August 2025 copy correction updated both representations together. In November, the team also hardened date handling for Article schema in the central schema file.

Outcome

The changes expanded and standardized machine-readable coverage of merchant pages without replacing the procedural page system:

  • FAQs could be updated once and reflected in visible and structured representations.
  • Heterogeneous promotions passed through a normalized shape before schema mapping.
  • JSON-LD encoding and decimal formatting had shared output rules.

This is an architecture and coverage outcome, not a claim about search performance. I do not yet have Search Console, rich-result, traffic, conversion, or revenue evidence that would support attributing an external SEO result to these changes.

What did not work

Several early choices failed once we tested them against the complete page lifecycle:

  • The first commit called the change JSON-LD but implemented a Microdata wrapper.
  • The first combined JSON block was hand-built and vulnerable to invalid punctuation and trailing commas.
  • The emitter initially ran before every dynamic FAQ was available.
  • The newer merchant layout was covered before the legacy layout, requiring a follow-up.
  • A temporary FAQ limit was introduced and then removed.
  • The promotion normalizer duplicated only part of a much larger placement pipeline, so equivalence with every visible ordering rule still needs rendered verification.

The current evidence review also found issues that prevent me from presenting the implementation as fully validated: an unsafe outer FAQ interpolation, mismatched FAQ closure arguments, an ItemList construction mismatch, and defects in the Dataset description helper. These defects need follow-up work. The case study remains a draft until representative pages and every emitted block are revalidated.

Validation still required

Before publishing this case study, I need to recover the original PR and ENG ticket context and test a matrix of merchant pages across present, missing, and edge-case data. The rendered JSON-LD should be extracted and checked with Schema.org and relevant Google tooling, including values above and below 1,000, duplicate entities, and parity with visible FAQs, policies, coupon tables, offer order, and instructions.

Only Search Console or comparable production evidence could support claims about indexing or search appearance. Google also limits the practical visibility of FAQ and HowTo rich results, so syntactic validity and semantic coverage should not be confused with guaranteed presentation in search.

Reflection

Producing JSON-LD was straightforward. The hard part was improving machine-readable output without destabilizing a revenue- and SEO-sensitive page assembled from years of implicit rules.

A new system would likely start with typed domain objects and derive HTML and structured data from them. At Knoji, I had to find the domain model hidden in template state, normalize data at the narrowest useful point, and preserve behavior until we could verify each dependency. The team's later corrections are part of this work, not separate from the commits under my name.

Technologies

  • PHP
  • JSON-LD
  • Schema.org
  • MySQL
  • ezSQL
  • Server-rendered HTML

Skills used

  • Structured data
  • Technical SEO
  • Legacy modernization
  • Data normalization
  • Defensive serialization
WorkAll case studies

More