Starting from a Broken GitHub Link: The External Data Landmines That Types and Tests Can't Catch


When optimizing for SEO, satisfying Google’s E-E-A-T (Experience, Expertise, Authoritativeness, and Trustworthiness) guidelines is practically an essential task. To declare the author’s external authoritative identity to search engines, I output JSON-LD structured data containing a Person node on the blog’s article pages and provided the author’s GitHub link via the sameAs field.

The semantic meaning of sameAs is “this is the authoritative source for the same entity elsewhere”—you’re telling the search engine, “The author of this article is the person behind this GitHub account,” allowing it to connect the identities. The implementation isn’t complicated; in the project, it’s just a set of constants:

export const AUTHOR: { name: string; sameAs: string[] } = {
	name: 'Vincent C.K. Chen',
	sameAs: ['https://github.com/tp6xup6'],
};

Everything seemed to be going smoothly until one day, while reviewing progress, I stepped on a landmine that had been buried for a while.

The progress documentation clearly stated, “Author’s GitHub link added,” but looking at the codebase, sameAs was an empty array. The documentation and the code didn’t match.

Tracing back the commits, the most recent one was:

fix: remove broken github link

Just a single-line subject, no body. It was impossible to tell “why it was broken.” Only through git show could I see what was removed, and only with git log -S could I confirm when it was introduced—as it turned out, it had been there since the site’s first official launch.

Cross-referencing the ground truth using two independent sources:

$ git remote -v
origin  https://github.com/VincentCKChen/<repo>.git

$ gh api user --jq '.login'
VincentCKChen

The account name tp6xup6 was guessed based on the domain name tp6xup6.cc. No one had ever opened that URL to verify it. It didn’t exist.

Why the Entire Pipeline Failed to Catch It

Guessed identifiers share a common trait: they look perfectly reasonable.

The domain name is tp6xup6.cc, so the GitHub account is tp6xup6—this deduction is correct in the vast majority of cases, as many people do use the same handle everywhere. It’s not like a typo that looks blatantly wrong at first glance; it reads exactly like the correct answer.

And yet, not a single gate in the entire pipeline could validate it:

  • TypeScript only knew it was a string[]; it didn’t care about the string’s content.
  • Schema validation used z.string().url(), which only checked if the format was valid, and https://github.com/完全不存在的東西 is perfectly valid formatting.
  • The build process didn’t ping that URL.
  • Testing verified “whether sameAs was correctly inserted into the JSON-LD,” not “whether the entity the URL pointed to actually existed.”

These tools guarantee the validity of the data structure, not its consistency with reality. As long as the format is right, the system accepts it.

Wrong is Worse Than Missing

There is a subtle difference here that is easy to gloss over.

If sameAs is empty, Google simply doesn’t know the author’s other identities—a neutral absence.

If sameAs points to a non-existent account, it actively declares a fake identity link. You’re telling the search engine to verify the author, only to hand it a 404. On a site actively trying to prove “the author is a real person with verifiable expertise,” this goes in the exact opposite direction.

The Second Mistake: “Removing” It Looked Like Fixing It

After discovering the link was broken, the fix at the time was to remove it.

This merely swapped “incorrect data” for “no data”. The original goal—providing external verification for the author’s identity—was still unfulfilled; it just wasn’t glaringly obvious anymore. And because the progress documentation still read “added,” this gap remained quietly untouched.

What was more troublesome was that commit message. It recorded “what was done” but not “why it was broken.” As a result, when it was finally time to properly fix it a few days later, I had to git show again, check the remote again, and verify the account again—essentially performing an archaeological dig on something someone already knew.

If the message had just included two more sentences: “This account was guessed from the domain name, unverified, and doesn’t actually exist,” the entire subsequent investigation wouldn’t have been necessary.

A Few Habits Adjusted Since Then

1. Always Cross-Verify External Identifiers; Do Not Accept Assumptions

Account names, profile URLs, external service IDs—there is no “should be” for these things, only “verified” and “unverified.” For something like GitHub, there are at least two independent sources you can check against:

git remote -v                         # who owns the repo
gh api user --jq '.login, .html_url'  # what is the logged-in identity

2. Ping It Once After Hardcoding It

curl -s -o /dev/null -w '%{http_code}' https://github.com/<account>

It only counts if it returns a 200. This step takes three seconds, but it’s the only action that can genuinely distinguish between “looks right” and “is right.”

3. Leave Markers Where the Questions Will Be Asked

Removal merely changes the shape of the problem; it doesn’t solve it. If you genuinely can’t find the ground truth at that moment, leave it as a visible gap rather than a quiet empty array.

There was a flaw in my initial thinking here. My original line of defense was documentation—archiving the incident, recording the conclusion in the CHANGELOG with a link to the archive, and digging in if details were needed. This approach isn’t wrong, but it completely failed this time, and the way it failed is worth noting:

The progress documentation stated “GitHub link added,” yet the code contained an empty array. Not only was the documentation unhelpful, but it was also wrong—and persuasively so, because it read as though the task was completed. Anyone trusting that document would simply skip over the issue.

The real crux of the issue isn’t which is better between “documentation vs. comments,” it’s that these two answer different questions, and the timing of the questions differs:

  • “Why does it look like this here?” — When you ask this, you are staring right at that line of code. The answer needs to be right there.
  • “Why was that decision made back then? What happened?” — When you ask this, you are reflecting. At this point, you will actively seek out the archives.

Archives are only ever found by people who have “already decided to go looking.” On-site markers, however, are seen by anyone who happens to be standing there. The reason this gap persisted for so long was that it only existed in the second format, and no one had a reason to go digging.

Therefore, leave a pointer on the scene, but keep the details in the archive:

// ❌ Just removing it without leaving a clue—three months later, no one will know what's missing here
sameAs: [],

// ✅ One or two lines explaining the current state + pointing to details, without reiterating the investigation process
// sameAs is currently empty: the original link was guessed from the domain name, and the account doesn't exist (see CHANGELOG 2026-07-29).
// Leaving this empty means the author's identity lacks external verification; to be restored once the correct account is confirmed.
sameAs: [],

Note that it’s a pointer, not a replica. Moving the entire investigation process into a comment just turns it into a massive block of text no one reads, which will likely be deleted as noise during a future cleanup.

4. Commit Messages Should Record “Why,” Not Just “What”

The criteria is simple: if three months from now you would ask “why was this done,” write it down now. This is especially true for fix commits—messages like fix: remove X are almost guaranteed to trigger an archaeological dig in the future.

The most intuitive automated solution would be to make HTTP requests for all sameAs links during the build process, failing the CI if it returns anything other than a 200. This way, this kind of error would never make it through.

After some thought, I decided against it because of asymmetrical costs: the CI would now rely on the external internet. GitHub going down, rate limits, or an unstable office network could all cause a build—completely unrelated to the codebase—to fail. Trading an occasional verification check for chronic instability isn’t worth it. Furthermore, once it fails frequently enough, the next step is someone setting that check to continue-on-error, rendering it virtually nonexistent.

A more sensible place for this is “at the moment of writing,” rather than “every build”—this is a matter of habit, not automation. Not all errors are worth preventing with CI.

Conclusion

No matter how rigorous the system architecture or how strict the type system is, they ultimately cannot stop data that “conforms to the format but contradicts reality.” Type checks validate shapes, and schemas validate formats; neither will verify that the thing actually exists.

And this kind of “reasonable-looking guesswork” will happen far more frequently in the context of AI-assisted development. Not because models are prone to wild guessing, but because models are exceptionally good at generating things that look perfectly reasonable—an account name derived from a domain name is exactly the kind of fill-in-the-blank that reads flawlessly and that a reviewer won’t even bat an eye at.

The more a field looks “right at first glance,” the more it desperately needs to be pinged in reality.