CV Structured Data
WordPress plugin: schema.org JSON-LD for my CV page, plus /cv.md and /llms.txt served with correct media types.
by Mark Biek · github.com/markbiek/cv-structured-data · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/markbiek/cv-structured-data/archive/refs/heads/main.zipA small WordPress plugin that makes mark.biek.org/cv readable by machines as well as people.
It does four things:
- Prints a schema.org
PersonJSON-LD block in the<head>of the CV page. - Serves
/cv.mdand/llms.txtfrom the site root with correct media types. - Points
robots.txtat/llms.txt. - Adds print styles so the page saves cleanly as a PDF.
Built for a WordPress.com Atomic site (Business plan or higher). Nothing in it is WordPress.com specific, so it works on any WordPress install.
Why serve the files through PHP
A static cv.md uploaded to the web root comes back as
application/octet-stream on WordPress.com Atomic — nginx has no entry for .md
in its mime map, and there is no .htaccess to override it. Browsers download the
file instead of displaying it, and the type="text/markdown" on the alternate
link contradicts what the server sends.
Serving through parse_request lets the plugin set Content-Type itself. It also
keeps the files in version control with the code, on one deploy path.
The hook only fires when nginx finds no real file at the path and falls through to
index.php, which is the case for both paths here.
Print styles
assets/print.css loads with media="print" and only on the CV page, so it
cannot affect the screen render. It hides the site header, footer, navigation
and the Jetpack subscription block, drops the block theme's screen width
constraint so the content fills the page, sets @page margins, and keeps
headings from stranding at the foot of a page.
Link destinations are deliberately not appended with a:after. Most of the link
text in this CV is already a URL, so expanding would print every address twice.
The page header and footer on the printout, the URL, date and page numbers, come from the browser's print dialog and not from CSS. Turn them off there before saving the PDF.
Checking the hide list
The hide selectors are written against the live theme's markup, and a class that
looks like chrome can turn out to wrap the content. .subscription-block did:
the theme puts it on two nested groups and the outer one contains
.entry-content, so hiding the bare class printed a blank page.
Run this after any theme update, or after editing the hide list:
python3 tools/check-print-css.py
It fetches the CV page, works out which elements each hide selector matches, and
fails if any of them match .entry-content or one of its ancestors. Pass a local
HTML file instead of the URL to check a saved copy.
Install
Copy the directory into wp-content/plugins/ and activate it, or point
WordPress.com's GitHub deployment integration at this repository.
Configure
Everything lives in cv-structured-data.php:
PAGE_SLUG— the page that gets the JSON-LD. Currentlycv.SERVED_FILES— root paths mapped to media type and backing file.person_schema()— the schema.org record.assets/print.css— the print stylesheet. Versioned byfilemtime(), so an edit busts the cache without a plugin version bump.
The CV content itself is in files/cv.md and files/llms.txt.
Two copies of the CV exist: this markdown file and the WordPress page. They do not sync. Update both.
On the phone number
person_schema() deliberately omits telephone. The number is on the
human-readable CV page, but putting it in structured data makes it harvestable at
scale, which is a meaningfully different exposure. The line is present and
commented out if you want it.
Verify
Validate the JSON-LD at validator.schema.org.
Check the media types after deploying:
curl -sI https://mark.biek.org/cv.md | grep -i content-type
curl -sI https://mark.biek.org/llms.txt | grep -i content-type
Expect text/markdown; charset=utf-8 and text/plain; charset=utf-8.
If either still returns application/octet-stream, a real file exists at that
path on disk and nginx is serving it before PHP sees the request. Remove it.
Note on llms.txt
llms.txt is a convention, not a standard. No major crawler has committed to
reading it. It costs one file, so it is worth having, but the JSON-LD block is what
actually makes the CV machine-readable — it uses a vocabulary parsers already
consume.
License
GPL-2.0-or-later.