Skip to content

[Trace] Template de-dup: de-duplicate SDK doc boilerplate across docs/SDKs #173

Description

@nicolasiscoding

Change type

Planned — normal review → merge

Risk / impact

Low

Security impact assessed?

No — no security impact

Details — description & full context

Trace task: template-detector.mjs, config/anti-ai-rules.json, and the CONTENT SOP rule
for "Crawled - currently not indexed": de-duplicate before anything else.

Evidence: URL Inspection shows Google refusing to index substantial SDK pages:
docs/SDKs/javascript (4,186 words, crawled - not indexed), python, quote-javascript,
webhooks-java, deliverable-go, deliverable-php (crawled - not indexed), java
(discovered - not indexed). template-detector.mjs found the same 5-word phrases repeated
across 15 of the 27 live (non-Ruby) SDK pages (a first pass with the tool's own reporting
quirks read this as ~17; see "Template detector" below), concentrated in a generic "Error
Handling" intro sentence plus a 7-row status-code table with identical English wording
(Invalid or missing API key, Invalid request parameters, Too many requests, Network connectivity issues) copy-pasted regardless of language or product. queries.json shows
real developer demand: "python digital signature sdk" (433 impressions, currently ranking on
the dotcom guide /guides/sign-documents-with-python, not this docs page), "php digital
signature sdk" (378, same pattern), "javascript pdf signature integration" (337, same
pattern). Data source: queries.json was generated by
queries.py, which calls the Google Search Console API directly
(searchAnalytics.query on sc-domain:turbodocx.com via a gcloud auth application-default print-access-token bearer token), not PostHog's GSC warehouse tables;
re-checked all three figures against the file directly before citing them here.

Root cause: two layers of duplication.

  1. docs/SDKs/deliverable-{go,php,java,javascript,python}.md and
    docs/SDKs/partner-{go,php,java,javascript,python}.md (10 files) each carried a full copy
    of the generic error-type table that belongs, once, on that language's canonical page
    (javascript.md, python.md, go.md, php.md, java.md). quote-*.md and
    webhooks-*.md had no such generic error table to begin with; not part of this fix. (They
    have their own, separate cross-language duplication in their product-intro paragraphs,
    disclosed further down, unrelated to the error-table issue this fixes.)
  2. The 5 canonical pages themselves opened ## Error Handling with the identical sentence
    "The SDK provides typed error(s)/exception(s) for different ... scenarios", regardless of
    language. Three of those five (javascript.md, python.md, java.md) are themselves
    evidence pages Google won't index.

Fix, part 1 (10 deliverable/partner pages): replaced the generic intro + table with a
short paragraph naming an error each operation actually raises (verified against
/home/nicolas/repos/SDK, e.g. Go's NotFoundError on an unknown TemplateID, PHP's
AuthenticationException when the partner API key or partner ID is wrong), followed by a
link to the language's own Error Handling reference for the full table. Kept the genuinely useful,
already-differentiated code samples (Go's errors.As, PHP's typed exceptions, Java's nested
TurboDocxException.* classes, the partner-specific IOException/409-conflict notes).

Fix, part 2 (5 canonical pages): rewrote each intro to state that language's real,
source-verified error-handling idiom instead of the shared sentence:

  • go.md: errors embed TurboDocxError by value; match with errors.As, read fields
    directly (no getters). Added the ConflictError row/case that was missing from the table
    (it exists in packages/go-sdk/http.go but wasn't documented).
  • php.md: statusCode/errorCode are public readonly properties; PHP's inherited
    Exception::getCode() is hardcoded to 0 by the constructor, so it never carries the
    error code, a genuine footgun worth calling out. Added the AuthorizationException and
    ConflictException rows/catches that were missing (both exist in
    packages/php-sdk/src/Exceptions/) but weren't documented, even though partner-php.md
    and deliverable-php.md already referenced them.
  • java.md: every exception is a nested static class of TurboDocxException
    (TurboDocxException.ValidationException, not a top-level import); each of the 7 named
    subclasses falls back to its own DEFAULT_CODE via a shared orDefault helper
    (TurboDocxException.java) so getCode() is populated for those, though the bare
    TurboDocxException thrown for an unmapped status has no such fallback.
  • javascript.md: code is a plain string the HTTP client passes through from the API
    response when present (e.g. QUOTE_NOT_FOUND) and only falls back to the class default
    otherwise, per the docstring in packages/js-sdk/src/utils/errors.ts.
  • python.md: every subclass sets its own DEFAULT_CODE class attribute
    (packages/py-sdk/src/turbodocx_sdk/http.py); catch the most specific exception first
    since except TurboDocxError also matches its subclasses.

Bugs found while verifying code samples against SDK source, both in index.md:

  • The PHP error-handling example called $e->getCode(), but PHP's Exception::getCode() is
    hardcoded to 0 by TurboDocxException's constructor (parent::__construct($message, 0, $previous) in packages/php-sdk/src/Exceptions/TurboDocxException.php); the real field is
    the public readonly $e->errorCode. Fixed.
  • The Java example imported com.turbodocx.sdk.* (the SDK's real package is com.turbodocx,
    no .sdk), returned a SigningResult type that doesn't exist in the SDK, and called
    turboSign.sendSignature(...) on a bare variable instead of the actual builder pattern,
    client.turboSign().sendSignature(...). All three didn't match java.md's own correct
    examples elsewhere in this same repo. Fixed to match; also added the
    com.turbodocx.models.* import SendSignatureResponse needs.
  • The Go example matched errors.As(err, &turboErr) against *sdk.TurboDocxError only. Go's
    errors.As requires the target's concrete type to match the error's; *sdk.ValidationError
    (and the 5 other named error types) is a distinct type from *sdk.TurboDocxError even
    though it embeds it, and neither implements Unwrap/As, so this check silently failed to
    match any of the 6 named error types and only caught the generic unmapped-status case; the
    VALIDATION_ERROR branch shown in the example was dead code. Fixed to match
    *sdk.ValidationError directly.
  • The Python example printed e.message. TurboDocxError.__init__ never sets
    self.message (nor does Python 3's Exception), so this raises AttributeError at
    runtime. Fixed to {e} (str(e)), matching python.md's own examples.
  • The "code is always populated" line just above these tabs had the same gap the
    language-page correction below describes. Qualified it the same way.

Correction found on a second pass (5 deliverable-*.md pages): my first commit claimed
ValidationError fires when a variable is missing "placeholder or mimeType". Checked
RapidDocxBackend/src/handlers/Deliverable/DeliverableGenerationHandlers.ts: the actual
Variable interface used by the generate-deliverable request body has placeholder: string
(required, no ?) but mimeType?: string (optional, has ?), the opposite emphasis from
what I'd written. Couldn't find the runtime validation that actually rejects a missing
placeholder (the handler doesn't appear to check it before using it), so rather than assert
an unverified 400, all 5 pages now say "missing a required field" without naming one.

Correction found on a second pass ("always populated" in go.md/java.md/python.md):
my first commit's Code-field descriptions said the machine-readable code is always populated.
True only for the 7 named error subclasses, each with its own default. The bare base error
returned for an unmapped HTTP status (an unexpected 5xx, for example) can have an empty/null
code: verified Go's defaultErrorCode() returns "" in its default case
(packages/go-sdk/http.go), Java's HttpClient.java falls through to
new TurboDocxException(message, response.code(), code) with no fallback for the unmapped
case, and Python's base TurboDocxError class has DEFAULT_CODE = None
(packages/py-sdk/src/turbodocx_sdk/http.py). Qualified all three descriptions.

Broken links found and fixed (Docusaurus file-relative .md links resolve against the
linking file's directory and were left alone unless truly broken):

  • go.md and java.md linked to /docs/TurboSign/API-Signatures (hyphen); the real page is
    /docs/TurboSign/API Signatures.md, served at .../API%20Signatures (confirmed against
    the other, correctly-encoded links in the same two files). Fixed both.
  • partner-php.md and partner-javascript.md linked to #orguserrole-organization-users, but
    the actual heading is ### OrgUserRole (Organization Users and Org API Keys), which
    Docusaurus slugifies to #orguserrole-organization-users-and-org-api-keys. Fixed both.

Frontmatter descriptions over 160 chars (11 files: agent-skills.md and all 5
quote-*/5 webhooks-* non-Ruby pages): rewritten to ≤137 chars, leading with the language
and product per the CONTENT SOP.

Noted, not fixed: docs/SDKs/ruby.md and deliverable-ruby.md (and the other three
Ruby product pages) 404 live but still get search impressions. Confirmed the removal was
intentional: git log origin/main -- docs/SDKs/ruby.md shows commit 40efd12 ("hide the
Ruby SDK pages until the gem is published") set draft: true on all five because
turbodocx-sdk is not yet on RubyGems (still blocked per the SDK release-traps reference: no
RUBYGEMS_API_KEY, gem 404s on RubyGems). Docusaurus excludes draft: true pages from the
production build entirely, so the 404s are by design, not an accident. Per the task's own
criterion, intentional removal that still draws impressions needs a redirect. The owner
picks the target and timing: javascript.md/python.md are the closest topical peers, but
both are also on the not-indexed list themselves, so neither is an obviously "safe" redirect
target either.

Out of scope, left as-is (no content changed, low content value, avoided disproportionate
diffs on files this CR did not otherwise touch):

  • The one-line :::caution API Credentials Required callout ("To get your credentials, follow
    the Get Your Credentials steps...") repeated across 15 files, and the ## Resources →
    GitHub Repository footer repeated across 18 files. Both are short, functional
    cross-reference text (already linking to the single canonical source rather than repeating
    it) rather than templated content Google would flag.
  • Pre-existing em-dashes in quote-*.md/webhooks-*.md/agent-skills.md body content
    (46-82 per file): this CR only touched their frontmatter description, and a full-file
    em-dash sweep on content this task did not otherwise change would be a large, unrelated diff.
    All prose actually added or edited by this CR is em-dash-free.

Template detector, before → after. The stock template-detector.mjs truncates its
5-gram report to the top 30 matches and its heading-strip regex fuses a heading straight into
the next paragraph (## Error Handling\n\nThe SDK provides... becomes one run-on "sentence"
for n-gram purposes), which understates the fix. Re-ran with an uncapped copy
(/home/nicolas/.claude/jobs/4fe2c970/tmp/template-detector-uncapped.mjs) that also strips
fenced code blocks and forces a sentence boundary after each heading, against both the
pre-change tree (git archive origin/develop) and the final result, counting the same 27
non-Ruby files on both sides:

  • provides typed error(s)/provides typed exception(s) (the shared intro sentence):
    15 files → 0 (verified: grep -il "provides typed" docs/SDKs/*.md, excluding the
    5 draft Ruby pages, returns nothing)
  • Generic status-row wording (Invalid or missing API..., Invalid request parameters,
    Too many requests, Network connectivity issues): 15 files → 5 (verified by direct
    grep: only go.md, java.md, javascript.md, php.md, python.md still carry it: five
    per-language tables, wording intentionally shared across them, instead of duplicated
    across 10 additional product pages too). The raw uncapped n-gram tool's per-phrase counts
    land at 7-8 rather than exactly 5 because it scans all 32 files, including the 5 draft
    Ruby pages this PR didn't touch, 3 of which still carry the same phrase
    (grep -il "Invalid request parameters" docs/SDKs/*ruby*.md); the 27-file, non-Ruby grep
    count is the number that matters and is 5.
  • Whole-directory uncapped total (every 5-gram shared by ≥3 files, all 32 files,
    threshold 3): 5,494 → 5,388. That residue includes quote-*.md and webhooks-*.md,
    disclosed below, plus the untouched draft Ruby pages and ordinary structural boilerplate
    (table headers, MDX imports) that no rewrite eliminates.
  • The credentials callout and Resources footer counts (15 and 18 files) are unchanged by
    design (see "out of scope" above); they're short, functional, already-canonicalized text.

Disclosed, not fixed: quote-*.md/webhooks-*.md still duplicate a per-product paragraph
across languages.
Running the uncapped detector on just the 5 quote-*.md files (excluding
Ruby) finds 960 five-grams shared by all 5; on just the 5 webhooks-*.md files, 700 shared by
all 5. Some of that is unavoidable component boilerplate (<QuickstartSkillNudge .../>), but
part of it is real: quote-php.md, quote-python.md, and the draft quote-ruby.md open with
a byte-identical "What is TurboQuote?" paragraph, while quote-go.md and quote-javascript.md
already have distinct wording. quote-javascript.md and webhooks-java.md are themselves
evidence pages (crawled/discovered, not indexed) and got no body changes in this CR, only a
frontmatter description fix: this CR fixed the Error Handling duplication investigated first,
but the quote/webhooks product-intro duplication shown above is real and unaddressed here, not
something already covered. Rewriting the quote/webhooks per-language intros the way this CR
rewrote Error Handling is a legitimate follow-up; flagging as an owner question below rather
than expanding this CR further and risking a harder review.

Config check: re-read config/title-engineering.json per the owner's "playbook configs
win over TurboDocx SOP where they conflict" note. Its maxLength: 60 and number-first
guidance target page titles for listicle-style content; it has no description-length rule
and doesn't apply to SDK reference pages, so the ≤155-char description rewrites above don't
conflict with it.

Owner questions:

  1. Ruby redirect: javascript.md or python.md as the target for ruby.md/
    deliverable-ruby.md (and the other 3 Ruby pages), or leave the 404s until the gem ships?
  2. Whether to do a follow-up pass on quote-*.md/webhooks-*.md rewriting the per-language
    product-intro paragraphs the way this PR rewrote Error Handling (960 and 700 shared
    5-grams respectively, see above), including the two remaining evidence pages
    quote-javascript.md and webhooks-java.md.
  3. Not a docs issue, an SDK one: every one of the 5 SDKs leaves code empty/null for an error
    on an HTTP status outside the 7 mapped ones (an unexpected 5xx, for example), even though
    the Java javadoc for orDefault says code "must still be branchable" and is "kept
    identical across all six SDKs." Worth a TurboDocx/SDK issue?

Component / area

Documentation: docs/SDKs/* (Go, PHP, Java, JavaScript/TypeScript, Python, and the
cross-language index.md)

Testing & validation

  • Re-ran template-detector.mjs (uncapped, before/after against origin/develop);
    confirmed the flagged boilerplate is eliminated, cross-checked with direct grep since the
    tool's own 30-match cap and heading/paragraph fusion were misleading on the first pass
  • Verified every changed code sample against the actual SDK source: read and cited
    packages/go-sdk/{http,deliverable,turbowebhooks}.go, packages/php-sdk/src/Exceptions/ {TurboDocxException,AuthorizationException,ConflictException}.php,
    packages/java-sdk/src/main/java/com/turbodocx/{TurboDocxException,TurboDocxClient, HttpClient}.java, packages/java-sdk/.../models/SendSignatureResponse.java,
    packages/js-sdk/src/utils/errors.ts, and packages/py-sdk/src/turbodocx_sdk/http.py
  • Verified the 5 deliverable-*.md claims about the generate-deliverable request body
    against RapidDocxBackend/src/handlers/Deliverable/DeliverableGenerationHandlers.ts (its
    Variable interface: placeholder required, mimeType optional, the opposite of what an
    earlier commit here claimed); confirmed that file is byte-identical to origin/staging
    despite the local checkout being on feature/nscale-epic
  • Verified every changed/added link resolves to an existing file and heading slug with a
    script that mirrors Docusaurus's github-slugger anchor algorithm
  • grep -c "—" returns 0 on every file this CR added or edited prose in (the 5 canonical
    pages, the 10 deliverable/partner pages, index.md, go.md, java.md)
  • /home/nicolas/repos/SDK was checked out on feature/turbosign-embedded-identity
    (unmerged SDK PR docs(turboquote): bulk spreadsheet import guide #77) while writing this; re-verified with git diff origin/main that the
    5 source files actually cited above are byte-identical to origin/main, and confirmed no
    branch-only symbol (createSigningUrl, getEmbeddedSigningSettings,
    identityVerification, embedded signing, OTP) appears anywhere in this PR's diff
  • Local build
  • Staging deploy

Rollback plan

Revert the merge commit.

Breaking change for consumers?

No

Reviewer / approver

A PR review approval satisfies this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

changeSOC 2 change management recorddocumentationImprovements or additions to documentation

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions