Gohyde reads _config.yml from the site root. Keys mirror Jekyll’s, in
snake_case. A _config.local.yml, if present, is merged on top (last wins) for
local-only overrides that you don’t commit.
title: My Site
description: A site built with Gohyde.
url: "https://example.com" # host + protocol — used by {{ site.url }}
baseurl: "/blog" # sub-path the site is served under
lang: en
source: . # default
destination: ./_site # output dir (some sites use ./_builds)
permalink: pretty # date | pretty | ordinal | none | custom pattern
timezone: Europe/Sarajevo # IANA zone for dates and permalinks (default: the machine's)
markdown: goldmark # ignored — see note below
markdown_ext: "markdown,mkdown,mkdn,mkd,md" # default
collections_dir: "" # put every collection (incl. _posts, _drafts) under a subdir
strict_front_matter: false # true: invalid front-matter YAML fails the build
liquid:
strict_filters: false # true: an unknown filter is an error (default: input passes through)
strict_variables: false # true: an undefined variable is an error
kramdown:
hard_wrap: false # true: every newline in a paragraph becomes <br />
timezone: works like Jekyll setting TZ: post dates written with a
different offset are shown in this zone, and a post dated near midnight can
land on a different day in its permalink. Zone-less dates are read in it.
Markdown output follows kramdown’s defaults: smart quotes and dashes
("x" → “x”, -- → –, --- → —, ... → …), no hard wraps, kramdown’s
task-list and footnote markup, and Rouge-shaped code blocks
(<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>).
Sites that relied on gohyde’s older hard-wrap behavior (a newline inside a
paragraph rendering as <br />) set kramdown: {hard_wrap: true}.
markdown: is parsed but has no effect — Gohyde only ever renders with
goldmark (Go, no Ruby). There’s nothing to
switch between. A ported site’s real Jekyll config almost always says
markdown: kramdown (Jekyll’s own default) — leave it as-is, no edit needed;
Gohyde ignores the value either way. What actually gets you kramdown-flavored
Markdown is a set of goldmark extensions built specifically to match
kramdown’s own syntax and quirks — Attribute Lists ({: .class #id}),
markdown="1" HTML blocks, the 1. TOC/{:toc} auto-TOC idiom, and more —
see Liquid & Jekyll Compatibility and the engine notes
in internal/markdown/ for the full list. These aren’t a real kramdown
implementation, just goldmark taught to understand kramdown’s syntax; that’s
enough for real kramdown-authored content to render correctly without
actually running kramdown.
url vs baseurlThese are distinct and both matter:
baseurl — the path prefix the site lives under (/blog). Prepended to page
URLs and asset URLs.url — the absolute origin (https://example.com). Templates prepend it
explicitly for absolute links.<link rel="canonical" href="{{ site.url }}{{ site.baseurl }}{{ page.url }}" />
<!-- → https://example.com/blog/about/ -->
If url is blank (e.g. commented out for local dev), {{ site.url }} renders
empty and links resolve relative — this is expected, not a bug.
Collections use list syntax (preferred):
collections:
- name: projects
label: Projects
output: true # write pages to the site (default true)
layout: project # default layout for items
blueprint: showcase # optional: share a field schema (see CMS docs)
src: _projects # source dir (default _<name>)
Jekyll’s map syntax is accepted too, with Jekyll’s defaults — output
defaults to false and the permalink to /:collection/:path:output_ext
(_projects/alpha.md → /projects/alpha.html):
collections:
projects:
output: true
output: false collections are loaded (available as site.<name>) but not
written to the output directory.
Posts can also live in _posts directories under ordinary folders, as in
Jekyll: news/tech/_posts/2024-01-02-x.md gets the categories news and
tech (before any front-matter categories).
Every collection is exposed two ways in Liquid:
{% for item in site.projects %}{{ item.title }}{% endfor %}
{% for collection in site.collections %}
{{ collection.label }}: {{ collection.docs | size }} docs
{% endfor %}
site.<name> (e.g. site.projects) is the array of that collection’s docs.
site.collections is an array of {label, docs, output, relative_directory, directory, permalink} — one entry per collection (posts plus every custom
collection), sorted by label, matching Jekyll’s real shape (not a bare
{name: [...]} map). site.documents is every collection’s docs, posts
included.
The :path permalink token is relative to the collection’s own directory and
excludes the source extension: _projects/alpha.md → alpha, not
_projects/alpha.md.
collections:
- name: projects
permalink: /work/:path/ # → /work/alpha/, not /work/alpha.md/
Jekyll-compatible jekyll-paginate (v1) behavior:
paginate: 5 # posts per page
paginate_path: /page:num/ # default; /blog/page:num/ paginates blog/index.html
The index.html in paginate_path’s directory expands into one page per
chunk, each with a top-level paginator object:
{% for post in paginator.posts %}
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
{% endfor %}
{% if paginator.previous_page %}
<a href="{{ paginator.previous_page_path }}">Newer</a>
{% endif %}
{% if paginator.next_page %}
<a href="{{ paginator.next_page_path }}">Older</a>
{% endif %}
<span>Page {{ paginator.page }} of {{ paginator.total_pages }}</span>
Page 1 is the index itself; page N lives at the paginate_path URL. As in
Jekyll, previous_page/next_page are nil on the first/last page, so plain
{% if %} guards work.
Per-page pagination: front matter (jekyll-paginate-v2 style) overrides the
globals — any page can opt in, wherever it lives:
---
title: Blog
permalink: /blog/
pagination:
enabled: true # false opts a page out of global pagination
per_page: 6 # overrides site `paginate:`
permalink: /blog/:num/ # overrides site `paginate_path:`
---
Posts with hidden: true front matter are excluded from pagination chunks and
the page count (they remain in site.posts).
Per-path front-matter defaults, exactly like Jekyll:
defaults:
- scope: { path: "", type: posts }
values: { layout: post, author: Vedad }
See Assets for the full pipeline. Key block:
Fingerprinting is on by default only when _config.yml has an assets:
block — a plain Jekyll site’s hard-coded /assets/... links keep working.
assets:
digest: false # content-hash fingerprinting (true in production; always off under `gohyde serve`)
destination: assets # output subdir
sources: # dirs scanned for assets
- _assets
- _assets/css
- _assets/js
- assets
cdn:
url: "https://cdn.example.com" # when set, overrides baseurl on asset URLs
sass:
sass_dir: _sass
style: expanded # default, like jekyll-sass-converter; or compressed
source_maps: false
Gohyde uses Dart Sass when a genuine sass/dart-sass binary is on PATH. The
legacy Ruby sass gem is detected and skipped (it can’t take --no-source-map);
if no real compiler is found, SCSS passes through unchanged with a stderr
warning — the build never hard-fails.
tailwind:
enabled: true
input: assets/css/input.css # default
output: assets/css/tailwind.css
minify: true # default; serve always compiles unminified
bundles: # optional: extra css compilations
- input: _assets/css/admin.css
output: assets/admin.min.css
Each bundles entry gets its own CLI run — and its own watcher under
gohyde serve; a failing bundle doesn’t block the others.
Under gohyde serve, the Tailwind --watch subprocess starts only when
tailwind.enabled: true or the input file exists. Merely having the
tailwindcss CLI on PATH does not start it.
exclude:
- Gemfile
- node_modules
- README.md
- "*.psd"
include: # default: [.htaccess]
- .htaccess
- .well-known # dot/underscore names are skipped unless listed here
keep_files: # default: [.git, .svn]
- .git
- downloads # left alone by the output cleanup
Like Jekyll, a full build removes destination files it didn’t write this
time (the output of deleted or renamed pages), except keep_files entries.
--incremental builds skip the cleanup.
Gohyde writes sitemap.xml, feed.xml, robots.txt and a fallback
404.html unless the site has its own. Core Jekyll writes none of them;
switch any off:
generators:
sitemap: false
feed: false
robots: false
"404": false
GOHYDE_ENV (or JEKYLL_ENV) sets {{ jekyll.environment }} and
{{ gohyde.environment }} (default development). The convention is to
gate production behavior:
assets:
digest: false # flip to true on JEKYLL_ENV=production in your build script
JEKYLL_ENV=production gohyde build