Source for reference.groupdocs.com — the GroupDocs API reference site. It publishes the class-library API references for 15 GroupDocs products across .NET, Java, Node.js, and Python (via .NET). The site is built with Hugo (extended, 0.101.0), is English-only, and deploys to AWS S3 + CloudFront.
Each product is an independent Hugo build that is published under its own path prefix
(/<product>/); the prefixes are merged into one bucket at the edge. There is also a separate
home/site-root build for the landing page.
config/sites/groupdocs/<product>/ # one config tree per product (+ a `home` tree)
_default/config.toml # shared settings, English-only, baseURL = "/<product>"
staging/config.toml # → reference2.groupdocs.com (QA) [Stage S3 target]
production/config.toml # → reference.groupdocs.com (prod) [Production S3 target]
content/sites/groupdocs/<product>/
english/ # all content (English only)
static/ # per-product static assets
themes/docs/ # vendored Geekdoc-based theme (no submodule)
data/products.toml # home page product grid — single source of truth
The 15 products: annotation, assembly, classification, comparison, conversion, editor, markdown, merger, metadata, parser, redaction, search, signature, viewer, watermark.
- All content is authored as
_index.mdsection bundles undercontent/sites/groupdocs/<product>/english/. Pages carry an expliciturl:in front matter, so permalinks are independent of the theme. - The bulk of the content (the API reference itself) is auto-generated from the per-product
source repos
GroupDocs.<Product>-References(GitHub orggroupdocs-<product>) and synced in. - Each product's landing page is a family page:
layout: familywith structured front matter (platforms, per-platformversion/versionUrl/install,capabilities,formats,resources). Rendered bythemes/docs/layouts/_default/family.html.
themes/docs/ is a vendored, Geekdoc-derived theme. Two deliberate customizations to know about:
- Scoped left navigation (
layouts/partials/menu-filetree.html) renders only the current branch of the tree (current page + ancestors + their children) rather than the full ~1,400-page tree. This keeps each page small. Do not revert it to the stock full-tree menu. - Markdown /
llms.txtoutputs (see below).
Alongside HTML, every page emits a Markdown (.md) rendition, and each build's home emits
llms.txt + llms-full.txt (the llms.txt convention), for AI/LLM
consumption. Configured per product in config/sites/groupdocs/<product>/_default/config.toml
([outputFormats.MD|LLMSTXT|LLMSFULL], [outputs]).
At the site root, llms.txt is a per-product directory (each product's full-reference,
section-index, and Markdown links), and /llms-full.txt is the entire API reference for all 15
products in one compacted file. Because each product is a separate Hugo build, Hugo can't
aggregate them — so scripts/build_llms_full.py builds /llms-full.txt from source
(stripping navigation boilerplate, keeping signatures + summaries + member/parameter tables) and CI
uploads it to the bucket root (deploy_all + refresh_search_index.yml).
The .md files are post-processed so they're clean and portable:
- No front matter, real Markdown —
single.md/list.mdemit.RawContent(Hugo strips the YAML front matter), via the partiallayouts/partials/md/abs-content.txt. The product family landing page stores its content in front matter (platforms, capabilities, formats, resources) with an empty body, solayouts/index.mdrenders that data as Markdown instead (matchingfamily.html). Its.mdis published at the bucket root as/<product>.md(not/<product>/index.md) bydeploy_product.yml. - Absolute links — links are made absolute so they work no matter where the
.mdis served:abs-content.txt(in-template) absolutizes root-relative links (/path/→BASE/path.md).scripts/resolve_md_links.py(post-build) absolutizes relative links (../ns/class,./x, of any depth) by resolving each against its page's own URL. It must run before the ugly-URL rename.
- Ugly URLs —
scripts/move_md_to_ugly_urls.shrenamespublic/<path>/index.md→public/<path>.md, so/viewer/net.mdis served (not/viewer/net/index.md).
The post-build order is always: build → scripts/resolve_md_links.py → scripts/move_md_to_ugly_urls.sh.
hugo server is avoided (a Hugo 0.101 concurrency bug panics on this content); use a static
build + a static file server instead:
./build-local.sh # home + annotation (default)
./build-local.sh viewer signature # home + the listed products
python -m http.server 1313 --directory public-local --bind 127.0.0.1build-local.sh builds the home at / plus each product under /<product>/, merged into
./public-local/, running the resolver + ugly-URL rename per product (mirroring deploy). The
home-only config is config-local.toml.
CI is a thin, reusable pipeline:
.github/workflows/resolve_targets.yml— the single place the branch/marker routing rule lives; every deploy workflow calls it..github/workflows/deploy_product.yml— reusable build + deploy for one product (Hugo build → resolve links → ugly URLs →hugo deployto S3 → CloudFront invalidation)..github/workflows/deploy_<product>.yml— one thin runner per product (path-filtered triggers)..github/workflows/deploy_all.yml— orchestrator (matrix over all products; also triggered bythemes/**changes).
There is one branch, main. The deploy target comes from the commit message, not the branch:
Commit on main |
Environment | Host | S3 target |
|---|---|---|---|
no marker, or [deploy-qa] |
staging | reference2.groupdocs.com (QA) | Stage |
contains [deploy-prod] |
staging, then production | reference2 → reference.groupdocs.com | Stage → Production |
| manual run | whichever you pick | — | — |
Production is opt-in — nothing goes live without an explicit marker or a manual run. The marker matches anywhere in any commit message of the push, so don't write the literal token unless you mean it.
To promote content that is already on main (nothing changed, so there is no commit to mark), run a
workflow manually:
# one product
gh workflow run deploy_viewer.yml --repo groupdocs/reference.groupdocs.com --ref main -f environment=production
# the whole site — pages AND the bucket-root aggregates together (the promotion unit)
gh workflow run deploy_all.yml --repo groupdocs/reference.groupdocs.com --ref main -f environment=productionPage redirects live in redirects/redirects.map and are published as S3 per-object 301s
(x-amz-website-redirect-location, served via the bucket's static-website endpoint through CloudFront)
by scripts/deploy_redirects.sh — run the manual Deploy Redirects workflow (QA first, then prod).
Regenerate the broken-link batch with scripts/gen_redirects_map.py (reads a 404 crawl + the content
tree, every target verified).
Rewrite rules are a separate, older mechanism: <host>.redirects.txt at the repo root is read by
the Lambda@Edge function on the domain's CloudFront distribution, with <host>.ignore_list.txt listing
paths no rule may touch. The path and name are part of that contract - moving these files into redirects/
in July 2026 is what stopped the rules from being applied. Edit them in place; there is nothing to build.
- Update a product's content — edit under
content/sites/groupdocs/<product>/english/(or let theGroupDocs.<Product>-Referencessync bring in regenerated API content) and pushmain; the per-product workflow deploys it to staging. Add the production marker to the commit message, or run the workflow manually, to publish it live. Note the canonical source for API content is the product repo — edits made only here are overwritten by the next sync. - Change a family page (versions, capabilities, etc.) — edit that product's
english/_index.mdfront matter. - Add/adjust the home product grid — edit
data/products.toml(drives{{< products-grid >}}).
config/sites/groupdocs/<product>/ per-product Hugo config (_default/staging/production)
content/sites/groupdocs/<product>/ english/ content + static/ assets
themes/docs/ vendored theme (layouts, assets/custom.css, partials)
data/products.toml home product grid
build-local.sh, config-local.toml local combined preview (kept in repo root)
scripts/ build/deploy + preview helpers: resolve_md_links.py,
move_md_to_ugly_urls.sh, build_search_index.py,
build_llms_full.py, update_versions.py, serve-local.py, build_refs.cmd
<host>.redirects.txt rewrite rules applied by Lambda@Edge (repo root; the path is a contract)
<host>.ignore_list.txt paths no rule may redirect (live pages a rule would otherwise match)
redirects/redirects.map page redirects -> S3 per-object 301s (deploy_redirects.sh)
scripts/gen_redirects_map.py build redirects.map from a 404 crawl + content tree
scripts/deploy_redirects.sh publish redirects.map as S3 per-object 301s + CloudFront invalidation
.github/workflows/ deploy_product.yml (reusable) + per-product runners + deploy_all.yml
+ deploy_redirects.yml (manual redirect publish)