metafetch

Metafetch fills a tool's GitHub, LinkedIn, and X links from its homepage

Nine of 1,200 Tooling notes had a LinkedIn URL. The homepages had them all along; Metafetch now copies them over, and won't guess.

Why Care?

A Tooling note is a profile of a company or project, and the first things a reader wants are its links: the GitHub org, the LinkedIn page, the X account. In the lossless vault, about 1,200 Tooling notes have a URL; 117 have a GitHub repo link, 9 a LinkedIn URL, and 7 an X URL. Probing 80 of those homepages found X on 73% and LinkedIn on 63%. The links were on the page the whole time.

What's New?

  • Fill frontmatter from folder profile. Reads the company's homepage and fills the note's empty social fields: github_profile_url (the org), github_repo_url, linkedin_url, x_url, youtube_channel_url, discord_url, instagram_url, facebook_url, bluesky_url, crunchbase_url, plus page values like og_image.

  • Folder profiles. A note in zz-cf-lib/frontmatter/ with applies-to-paths globs and a metafetch-profile block decides which fields a folder gets. Same shape as Perplexed's templates.

  • Create example frontmatter profile writes the Tooling one to start from.

  • It never overwrites. A field that has a value, under its own name or an alias like github_profle_url, is left alone. github_url is never read or written.

Choosing the company's own profile

Finding links is easy. A homepage links lots of profiles: its own, its customers' (logo walls), its partners', its investors', a blog author's, plus share buttons. Each candidate is scored by where it appears:

SignalWhy
JSON-LD sameAs on the OrganizationThe site saying "these are mine"
Footer linkWhere sites put their own socials
Header or nav linkSame, less often
Handle resembles the domain or site nameglideapps.com → @glideapps
Appears more than onceHeader and footer agreeing

A link found only in the page body that doesn't resemble the brand is never used. When the top two candidates tie, nothing is written and the notice names both. An empty field is better than a confidently wrong LinkedIn URL.

Two cases needed special handling. A GitHub repo link names both a repo and its org, and an org that owns a brand-named repo counts as the brand's even when its name differs: zod.dev links github.com/colinhacks/zod. And a homepage that serves a bot check contributes no links at all; otherwise its footer offers the CDN's own X account.

What a real run looked like

Dry run on 20 random Tooling notes (pages fetched live, nothing written to the vault):

  • 16 notes gained links. DigitalOcean got its GitHub, LinkedIn, X, Instagram, and Facebook; OpenClaw, six profiles; Neovim, its org, repo, X, and Bluesky.

  • One had nothing to find.

  • Three sites refused the fetch (Uiverse 403, Contentful 429, Mocha 525). Those are what the planned Firecrawl fallback is for.

  • Podman showed the tie rule working. Its homepage links three repos equally, so github_repo_url stayed empty and the notice listed all three; the org, containers, was filled.

The run also caught two bugs before they reached a note. Rewriting the whole frontmatter block re-quoted every value, so writes now go through Obsidian's processFrontMatter and add only the new keys. And Facebook profile.php?id=… links were losing their id.

Under the hood

  • src/services/socialLinks.ts: classifySocialUrl() recognizes and canonicalizes one URL per platform (twitter.com → x.com, discord.com/invite/x → discord.gg/x, LinkedIn sub-pages trimmed). collectSocialCandidates() places each link (sameAs, footer, nav, body) and scores it. chooseSocialLinks() applies the minimum score and the tie rule.

  • src/services/frontmatterProfiles.ts: parseProfile() reads the file and reports bad fields without failing the profile; resolveProfiles() picks the most specific glob; planFill() decides what to write.

  • src/commands/fillFromProfile.ts: fetches the note's URL for page values and the site root for socials (one fetch when they're the same), then writes through processFrontMatter, re-checking each key against the live frontmatter.

  • 34 new tests (137 total). The test stub's parseYaml uses the yaml package as a devDependency; it isn't in main.js.

What's Next

  • The three refused sites argue for slice 2 of the exploration: a Firecrawl fallback through a data-only recipe, with its key in Obsidian's secret storage.

  • A folder batch, so the 1,200 Tooling notes don't need 1,200 runs.

  • runFetchScript (the older Direct Fetch command) still rewrites the whole frontmatter block. Moving it to processFrontMatter too would stop the re-quoting churn there.