This repo is an experiment to create a static (Hugo) home for foojay.io, replacing the current WordPress site. It's built and run in parallel with the live WordPress site during a trial/transition period, then cut over once it proves to be solid and better.
Content is a mix of converted pages (scraped from the live WordPress site by
the Convert* scripts) and a few hand-written pages. Blog posts and author
profiles are Hugo leaf bundles —
a folder per item, with the Markdown in index.md and any images co-located in
the same folder.
content/posts/— blog posts (from/today/), one leaf bundle per post at<year>/<month>/<day>/<slug>/index.md, bucketed by original publish date (repo organization only — the public URL stays/today/<slug>/, since thepostspermalink is:slugorcontentbasename, i.e. the folder name is the slug). Posts with no parseable date land in anundated/bucket.content/authors/— author profile bundles at<first-letter>/<slug>/index.md, bucketed A–Z, with the small + large avatar images alongside. URL:/today/author/<slug>/.content/pedia/— the/pedia/glossary of Java terms (<slug>.md).content/pages/— everything else (About, Java Quick Start tree, Meet the Team, Where to find us, JUGs, Java Champions, etc.). Each keeps its legacy URL via an expliciturl:in frontmatter.content/search.md— on-site search page (see "Search" below).data/jugs.yaml— generated (see "External data" below), never hand-edited. Rendered at/jugs/with a Leaflet world map + a client-side name/country filter.data/java-champions.yaml— generated, rendered at/java-champions/with a client-side filter.data/events.json— generated daily by the Meetup sync workflow.themes/foojay/— the Hugo theme (structural recreation of the current site; see "Known limitations" below).scripts/— jbang/Java conversion, external-data, and validation scripts (see below).template/— a starterindex.md(all frontmatter documented) + a categories list for authors writing a new post; seeCONTRIBUTING.md..github/workflows/— CI: PR checks, Pages deploy, daily external-data + Meetup sync.
Requires JBang and Java 21+ (ConvertPosts.java
uses virtual threads; the rest need 17+).
Content conversion — scrape the live WordPress site into content/:
jbang scripts/ConvertPosts.java # /today/ posts -> content/posts/
jbang scripts/ConvertAuthors.java # /today/author/ -> content/authors/These are idempotent — safe to re-run on a schedule during the trial. They
update existing bundles rather than duplicating them, look posts/authors up by
slug so a bundle stays put across re-runs, and skip any file whose frontmatter is
hand-marked frozen: true. Body-HTML→Markdown conversion (image localization,
YouTube/{{< img >}} shortcodes, widget preservation) is shared via
HtmlToMarkdown.java.
The one-off page and glossary scrapers (ConvertPages.java, ConvertPedia.java)
have been removed — content/pages/ and content/pedia/ are done and now
maintained by hand. Only posts and authors still get re-scraped, since those keep
growing on the live site.
Each Convert* script also supports --url <single page URL> to test/tune its
scraping against one real page, and ConvertPosts supports --days N / --since <date> to convert only a recent window.
External data — regenerate the data/* files from community-run upstreams
(run at every deploy and daily; see "Workflows"):
jbang scripts/FetchJugs.java # -> data/jugs.yaml (World-Wide-JUGs/GlobalWWJugs)
jbang scripts/FetchJavaChampions.java # -> data/java-champions.yaml (aalmiray/java-champions)
jbang scripts/FetchMeetupEvents.java # -> data/events.json (Meetup GraphQL; needs Pro + OAuth)The data/* files are generated — never hand-edit them. Add or fix a JUG or
a Java Champion upstream (a PR against World-Wide-JUGs/GlobalWWJugs or an edit
to aalmiray/java-champions's java-champions.yml), and it flows in on the next
sync.
Validation / one-offs:
jbang scripts/ValidateFrontmatter.java # PR-time content check (also runs in CI)MigratePostsToBundles.java, MigrateAuthorsToBundles.java, and
SanitizeSlugs.java are one-time migrations that have already been run — kept for
reference, not part of the normal loop.
hugo server -D(Note: the site-wide search box needs a real build — see "Search".)
No external service: Pagefind indexes the built
public/ output at deploy time (npx pagefind --site public, a step in
build-deploy.yml) and content/search.md loads that index client-side.
hugo server alone won't have a working search box, since the index only exists
after a real build. To test locally:
# --baseURL override matters: hugo.toml's baseURL is the real deploy URL
# (a GitHub Pages subpath during the trial), but `serve` serves public/ at the
# root of localhost:3000 -- without the override every asset and link 404s.
hugo --baseURL "http://localhost:3000/"
npx pagefind --site public
npx serve publicbuild-deploy.yml— on push tomain: refreshesdata/jugs.yaml+data/java-champions.yaml(commits them back with[skip ci]), builds with Hugo, runs Pagefind, and deploys to GitHub Pages.meetup-sync.yml— daily cron: refreshesdata/jugs.yaml,data/java-champions.yaml, anddata/events.json, committing the results.pr-check.yml— on PRs: runsValidateFrontmatter.javaand a Hugo build (GitHub Pages has no per-PR preview URL).
- Scraping selectors (in the
Convert*scripts) are best-effort, based on foojay.io's current theme markup + standard WordPress/Yoast conventions. Title/description/canonical/image are reliable (standard meta tags + JSON-LD). Categories, tags, author links, and related-posts links use configurable CSS selectors at the top of each file — run with--urlagainst a few real posts and adjust if a field comes back empty. - Cloudflare cache: the
/today/listing is CDN-cached, so a just-published post can be missing from a crawl for a while even though its own page is live. Convert it directly with--urlif you need it immediately. - Meetup GraphQL query (in
FetchMeetupEvents.java) needs verifying against Meetup's current schema once Pro access + an OAuth token (MEETUP_OAUTH_TOKEN) are set up. - Theme (
themes/foojay/) recreates the current site's structure (nav, post grid, sidebar widgets, footer) but not its exact visual design — treatstatic/css/style.cssas a starting point to refine against real brand assets. - Related posts are picked by the author, not computed — see
related_posts:in each post's frontmatter. - URLs are load-bearing: permalinks match the legacy
/today/slug/and/today/author/slug/patterns, and every converted post/author/page also gets an explicitaliases:entry with its legacy path as a belt-and-suspenders redirect.
See CONTRIBUTING.md for the author PR workflow, and CLAUDE.md for
implementation context if you're picking this up with Claude Code.