The generator is data-driven: Go holds layout and templates, and everything
else comes from data under your -root. This document is the complete schema so
any project can supply its own data.
Everything is read relative to -root:
<root>/
_content/
portfolio/portfolio.json # product metadata (source of truth)
products/<slug>.md # one markdown file per product
sites/<site-file>.home.json # one homepage document per site
_content_<lang>/ # OPTIONAL: presence enables hreflang for <lang>
design-system/
diagrams/<slug>.svg # OPTIONAL: architecture figure injected into a product page
There are two layers:
- Site definitions — per-site identity (brand, domain, OG image, JSON-LD
kind). Compiled in; you edit the
sitesmap inmain.go. - Content data — per-page content (products, home). Pure data files; no code changes.
Site-level identity is not read from data files — it is defined in the sites
map in main.go and selected with -site <key>. To render for your own
project, add an entry to that map. Each Site has:
| Field | Type | Purpose |
|---|---|---|
Key |
string | The -site value that selects this definition. |
Brand |
string | Display brand; used in <title> suffixes, OG site_name, footers, JSON-LD name. |
CSSName |
string | Basename of the deployed brand stylesheet at <site>/assets/od/<CSSName>.css. |
HomeJSON |
string | Path (relative to -root) of the homepage document for this site. |
Dir |
string | Default output subdirectory (used when -out is omitted). |
Domain |
string | Production host (no scheme); every absolute URL, canonical, and sitemap <loc> is built from it. |
OGImage |
string | Root-relative Open Graph / letterhead image path (should exist on the deployed site). |
LDKind |
string | JSON-LD identity type: "Organization" or "Person". |
SameAs |
[]string | JSON-LD sameAs profile URLs for the identity node. |
Genericity note. Because site identity is compiled in, adapting this tool to a new brand means editing the
sitesmap (a few lines) in addition to supplying data files. The-sitekey must match a map entry; unknown keys exit with an error. Two example definitions ship inmain.go(vasic.digital, a standalone site;milosvasic.ru, a Jekyll-embedded site) that you can copy and adjust.
The single source of truth for product metadata, shared by the portfolio page, every product page, the homepage product cards, and the JSON-LD graph.
| Field | JSON key | Type | Notes |
|---|---|---|---|
| Name | name |
string | Display name. Used as page <h1>, card title, JSON-LD name. |
| Slug | slug |
string | URL slug. Product page is products/<slug>.html; also the design-system/diagrams/<slug>.svg lookup key. |
| Tier | tier |
string | Must be one of tiers. Groups the entry into a portfolio section. |
| Order | order |
int | Shown on the product page eyebrow (// tier: X · order N). |
| Status | status |
string | Rendered verbatim as a status badge and as the CSS modifier od-badge--status--<status> (e.g. beta, production, stable, scaffold). Keep it a single CSS-safe token. |
| License | license |
string | SPDX-style id (e.g. MIT). Values matching `^(unverified |
| Private | private |
bool | Marks a private product. A guard asserts a private:true entry carries no repos (suppression is expected upstream at your data gate). |
| Tech | tech |
[]string | Technology chips. Portfolio cards show the first 6 then a +N overflow chip; product pages show all. |
| Repos | repos |
[]string | Repository URLs. github.com/... URLs are shortened to owner/repo; others shown in full. |
| Summary | summary |
string | Primary description: meta description, card blurb (fallback), JSON-LD description. |
| Tagline | tagline |
string | Short blurb; preferred over summary for the portfolio card blurb. |
| Source | source |
string | Path (relative to -root) of the product markdown file for this entry. |
Description precedence for a product page's meta/OG: summary → tagline →
"<name> — <brand> product." (first non-empty).
Each entry's source points at a markdown file: YAML-ish frontmatter + a
markdown body.
---
name: HelixTrack
status: beta
license: MIT
tech:
- Go
- PostgreSQL
---
# HelixTrack
Intro paragraph with **bold** and `code`.
## A section
- a list item
- another item- Delimited by a leading
---line and a closing---line. key: value→ a scalar.key:with an empty value, followed by- itemlines → a string list.- Only scalars and simple string lists are supported (no nesting, no inline flow collections). A file with no frontmatter is a hard error for a product.
Note: page identity (name, status, license, tech, repos) for rendering comes from
portfolio.json, not the frontmatter. The frontmatter is parsed and available, and keeping it in sync with the portfolio entry is the recommended convention, but the body is what the product page renders.
| Markdown | Output |
|---|---|
# Title |
dropped (the page <h1> comes from the portfolio name) |
## Heading |
<h2> |
- item runs |
<ul><li>…</li></ul> |
| blank-line-separated | <p>…</p> (consecutive non-blank lines join with a space) |
**bold** |
<strong>…</strong> |
`code` |
<code>…</code> |
Anything else is treated as paragraph text. Text is HTML-escaped (&, <, >;
quotes are preserved in body content) before inline substitution.
One JSON document per site (Site.HomeJSON). It describes the homepage as an
ordered list of typed blocks.
{
"site": "vasic.digital",
"kind": "standalone", // "standalone" -> full HTML page; "jekyll" -> a Jekyll `default`-layout fragment
"brand": "Example Digital", // brand shown in the page body/footer
"lang": "en",
"title": "…", // <title> (standalone)
"description": "…", // meta description (standalone)
"css": "vasic-digital", // informational; deployed CSS basename
"footer": "…", // footer text (standalone)
"blocks": [ /* HomeBlock[] */ ]
}type |
Renders | Key fields |
|---|---|---|
hero |
Hero section | eyebrow, title, lede, ctas[], stats[] |
section |
Content section | id, optional section header (eyebrow/title/lede), groups[], optional trailing ctas[] (ctaWrap, ctaStyle) |
contact |
Contact section | id, eyebrow, title, lede, contacts[] |
I18nText — visible text plus its i18n key:
{ "i18n": "hero.title", "text": "Hello", "class": "", "html": false }.
When html is true, text is emitted as trusted (pre-escaped) HTML.
CTA — { "i18n", "label", "href", "variant" ("primary"|"secondary"|"ghost"), "dl", "icon" }.
If dl is set the CTA renders as a <button data-dl> (e.g. a download action);
otherwise an <a href>.
Stat — { "i18n", "value", "label" } (hero stat tile).
Group — a sub-block of a section:
{ "eyebrow", "title", "lede", "marginTop", "kind" ("prose"|"cards"|"tech"), "grid", "paras": I18nText[], "cards": Card[] }.
Card — a product/tech card:
{ "slug", "name", "nameI18n", "status", "noBadge", "tech": [], "blurb": I18nText, "readmore": I18nText }.
If slug is set and status is empty, the status badge is pulled from the
matching portfolio.json entry; readmore links to products/<slug>.html
(via Jekyll's relative_url filter when kind is jekyll).
Contact — { "i18n", "label", "html" } where html is trusted inner HTML
for the value line.
See examples/data/_content/sites/vasic-digital.home.json
for a complete, working document.
The generator discovers languages by scanning -root for _content_<lang>/
directories (e.g. _content_de/, _content_ru/). For every language found:
enis always present and canonical (unprefixed paths).- Directory names containing an underscore in the code (e.g.
_content_de_proof) are ignored as scratch dirs. - The discovered set (en first, remainder sorted) drives the reciprocal
hreflang matrix emitted in every page head, plus
x-default→ EN.
Per-language page output is produced by running with -lang <code> (see
USAGE.md); merely creating _content_<lang>/ enables the hreflang
alternates.
If a file <root>/design-system/diagrams/<slug>.svg exists, its SVG (with any
leading <?xml …?> declaration stripped) is injected into that product's page as
an <figure class="od-diagram">. Missing files are simply skipped.
{ "schema": "portfolio/v1", // free-form version string (informational) "count": 2, // MUST equal entries.length (a test asserts this on your data if you reuse the fixture guard) "tiers": ["helix-primary", "vasic-util-secondary"], // ordered tier keys; each becomes a portfolio section "excluded": [], // informational; slugs intentionally omitted "entries": [ /* PortfolioEntry[] */ ] }