diff --git a/.gitignore b/.gitignore
index adb0b1b..1dccbbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,6 @@
/vendor/
__pycache__/
*.pyc
+
+# Article drafts stay local until release; see README "Drafts".
+/_drafts/
diff --git a/404.html b/404.html
index 4ed1dd1..77286e4 100644
--- a/404.html
+++ b/404.html
@@ -1,6 +1,8 @@
---
title: Page not found
permalink: /404.html
+sitemap: false
+robots: noindex
---
404
Page not found.
diff --git a/README.md b/README.md
index e2c1e71..c77b4d5 100644
--- a/README.md
+++ b/README.md
@@ -37,13 +37,34 @@ JEKYLL_ENV=production bundle _2.6.9_ exec jekyll build --strict_front_matter
python3 scripts/check_site.py
```
-The checker requires the homepage and 404 page, then verifies local `href` and
-`src` targets and HTML fragment IDs. It does not check external URLs or CSS URLs.
-Also inspect the homepage on a narrow screen before publishing.
+The checker requires the homepage, 404 page, `sitemap.xml`, `feed.xml`, and
+`robots.txt`. It verifies local `href` and `src` targets and HTML fragment IDs.
+For every page except 404, it requires a title, a meta description, a canonical
+URL that matches the page, and no `noindex`; the 404 page must be `noindex`.
+The sitemap must list exactly the indexable pages, `robots.txt` must point to it,
+and JSON-LD blocks must parse. External URLs and CSS URLs are not checked.
+Also inspect new or changed pages at phone width before publishing.
-The initial production build, frozen dependency check, and generated local
-links passed on 2026-10-23 with Ruby 3.4.12 and Bundler 2.6.9. Browser rendering
-and GitHub-hosted workflows have not yet been verified.
+The production build, frozen dependency check, and checker passed on
+2026-10-23 with Ruby 3.4.12 and Bundler 2.6.9. The same day, the homepage,
+`/blog/`, `/about/`, 404, and a draft article were rendered in headless Chromium
+at 375 px and 1280 px without horizontal page scrolling. The GitHub-hosted
+workflow runs have not yet been reviewed.
+
+## Drafts
+
+Article drafts live in `_drafts/`, which is ignored by Git, so unfinished text
+does not appear in this public repository. Preview and check them locally:
+
+```bash
+bundle _2.6.9_ exec jekyll serve --drafts
+JEKYLL_ENV=production bundle _2.6.9_ exec jekyll build --strict_front_matter --drafts
+python3 scripts/check_site.py
+```
+
+The workflows build without `--drafts`, so CI does not check draft content.
+To publish, move the reviewed file to `_posts/YYYY-MM-DD-.md` and add it
+in the release pull request. The URL stays `/blog//`.
## Publishing
@@ -57,8 +78,8 @@ GitHub Actions**. This setting has already been selected by the repository owner
- The deployment uses the `github-pages` environment and the built-in GitHub
token. Configure the environment's deployment branch rule to allow only `main`.
-Pushing this scaffold to `main` will attempt to publish the initial homepage.
-The first public deployment has not yet been verified.
+The first deployment of the initial homepage to
+was confirmed by the repository owner.
## Troubleshooting and rollback
@@ -69,7 +90,34 @@ deployment fails, check the Pages source and `github-pages` environment rules.
To undo a published change, revert its commit, verify the resulting build, and
push the revert to `main`. This triggers publication of the restored content.
-## Next stage
+## Site structure
+
+| Path | Purpose |
+| --- | --- |
+| `_layouts/default.html` | Page shell: header navigation, footer |
+| `_layouts/post.html` | Article: title, author, publication, update and test dates, related guides |
+| `_includes/head.html` | Title, description, canonical URL, Open Graph, feed link, JSON-LD (`TechArticle` for articles, `WebSite` for the homepage) |
+| `blog.md`, `about.md` | `/blog/` list of guides and `/about/` page |
+| `sitemap.xml`, `feed.xml`, `robots.txt` | Liquid templates; no Jekyll plugins are used |
+
+Article front matter:
+
+```yaml
+title: "FreeRADIUS clients.conf: Examples and Best Practices"
+description: One or two sentences for search results and the guide list.
+last_modified_at: 2026-11-02 # only after a substantial update
+tested:
+ date: 2026-10-23
+ environment: Official FreeRADIUS Docker image
+ freeradius: "3.2.10"
+ radiusdeck: "0.1.0"
+related:
+ - /blog//
+```
+
+The author (`RadiusDeck team`) and the `post` layout come from `_config.yml`
+defaults. The layout prints the title as the page heading, so the article body
+starts without an H1. Related guides appear only when the target article is
+published.
-Add the article layout, blog index, About page, sitemap, RSS, and the first three
-reviewed guides. Article URLs follow `/blog//`.
+`robots.txt` allows all crawlers, including AI search crawlers.
diff --git a/_config.yml b/_config.yml
index 48d7ef2..c886102 100644
--- a/_config.yml
+++ b/_config.yml
@@ -13,11 +13,21 @@ strict_front_matter: true
future: false
show_drafts: false
+author:
+ name: RadiusDeck team
+ url: /about/
+product_url: https://github.com/radiusdeck/radiusdeck
+
defaults:
- scope:
path: ""
values:
layout: default
+ - scope:
+ path: ""
+ type: posts
+ values:
+ layout: post
exclude:
- README.md
diff --git a/_includes/head.html b/_includes/head.html
new file mode 100644
index 0000000..ea5cc4d
--- /dev/null
+++ b/_includes/head.html
@@ -0,0 +1,55 @@
+{%- assign description = page.description | default: site.description | strip_html | normalize_whitespace -%}
+{%- assign canonical = page.url | replace: "index.html", "" | absolute_url -%}
+{%- assign is_post = false -%}
+{%- if page.collection == "posts" -%}{%- assign is_post = true -%}{%- endif -%}
+{%- assign modified = page.last_modified_at | default: page.date -%}
+
+
+{{ page.title | escape }} | {{ site.title | escape }}
+
+
+{%- if page.robots %}
+
+{%- endif %}
+
+
+
+
+
+{%- if is_post %}
+
+
+
+{%- else %}
+
+{%- endif %}
+
+
+
+{%- if is_post %}
+
+{%- elsif page.url == "/" %}
+
+{%- endif %}
diff --git a/_layouts/default.html b/_layouts/default.html
index 563872c..e2b4cae 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -1,24 +1,25 @@
-
-
- {{ page.title | escape }} | {{ site.title | escape }}
-
-
-
+ {% include head.html %}
Skip to contentRadiusDeck / Blog
- View project on GitHub ↗
+
{{ content }}
diff --git a/_layouts/post.html b/_layouts/post.html
new file mode 100644
index 0000000..d4d9cf0
--- /dev/null
+++ b/_layouts/post.html
@@ -0,0 +1,37 @@
+---
+layout: default
+---
+{%- assign date_format = "%B %-d, %Y" -%}
+
+
+
{{ page.title | escape }}
+
+ By {{ site.author.name }}
+ · Published
+ {%- if page.last_modified_at %}
+ · Updated
+ {%- endif %}
+ {%- if page.tested.date %}
+ · Last tested
+ {%- endif %}
+
+
+
+ {{ content }}
+
+ {%- assign related_posts = "" | split: "" -%}
+ {%- for url in page.related -%}
+ {%- assign match = site.posts | where: "url", url | first -%}
+ {%- if match -%}{%- assign related_posts = related_posts | push: match -%}{%- endif -%}
+ {%- endfor -%}
+ {%- if related_posts.size > 0 %}
+
+ {%- endif %}
+
diff --git a/about.md b/about.md
new file mode 100644
index 0000000..f1f7ca2
--- /dev/null
+++ b/about.md
@@ -0,0 +1,44 @@
+---
+title: About
+description: >-
+ Who writes the RadiusDeck Blog, how the FreeRADIUS guides are tested, and how
+ to report a correction.
+permalink: /about/
+---
+
+# About this blog
+
+The RadiusDeck Blog publishes practical guides for administrators who connect
+network devices, such as access points and VPN gateways, to FreeRADIUS. The articles are
+written by the RadiusDeck team, the maintainers of
+[RadiusDeck]({{ site.product_url }}), an open-source web interface for
+managing FreeRADIUS clients in `clients.conf`.
+
+## How the guides are tested
+
+- Commands and configuration examples are run on an isolated test setup before
+ publication. Each article lists the software versions and the date of the
+ last technical check.
+- Behavior that depends on a version, distribution, or deployment is labeled as
+ such. Defaults can change between FreeRADIUS releases, so check them against
+ your own installation.
+- Addresses come from documentation ranges such as `192.0.2.0/24`, and device
+ names and shared secrets are fictional. Replace them with your own values.
+- Technical claims link to primary sources, such as FreeRADIUS configuration
+ comments, manual pages, and project announcements.
+
+When a guide is revised, it keeps its address and shows the date of the update.
+
+## What RadiusDeck covers
+
+RadiusDeck manages RADIUS clients: the network devices that send requests to
+FreeRADIUS. End-user accounts, SQL, and authentication policies are configured
+in FreeRADIUS itself. The guides say when a step is done in FreeRADIUS and when
+RadiusDeck can help with it.
+
+## Corrections
+
+If a guide is wrong or outdated for your version, open an issue in the
+[blog repository](https://github.com/radiusdeck/radiusdeck.github.io/issues).
+Include the FreeRADIUS version and remove shared secrets, passwords, and
+private addresses before posting.
diff --git a/assets/css/main.css b/assets/css/main.css
index de4afba..8e99caf 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -42,3 +42,44 @@ code { overflow-wrap: anywhere; background: #e7ede8; padding: .1em .25em; border
:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }
.skip-link { position: absolute; top: -5rem; left: 1rem; padding: .5rem 1rem; background: white; }
.skip-link:focus { top: .5rem; }
+
+/* Navigation */
+.site-nav { display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; }
+.site-nav a { color: var(--ink); text-decoration: none; }
+.site-nav a:hover, .site-nav a[aria-current="page"] { color: var(--accent); text-decoration: underline; }
+.site-footer p { margin: 0; }
+
+/* Articles */
+.post > * { max-width: 46rem; }
+.post-header h1 { font-size: clamp(2rem, 5vw, 3.2rem); letter-spacing: -.03em; }
+.post-meta { color: var(--muted); font-size: .9rem; }
+h3 { font-size: 1.1rem; line-height: 1.35; margin-top: 2rem; }
+#markdown-toc { padding: 1rem 1rem 1rem 2.25rem; border: 1px solid var(--line); border-radius: .3rem; font-size: .95rem; }
+#markdown-toc ul { padding-left: 1.25rem; }
+pre {
+ overflow-x: auto;
+ padding: .9rem 1rem;
+ background: #e7ede8;
+ border-radius: .3rem;
+ font-size: .88rem;
+ line-height: 1.55;
+ tab-size: 4;
+}
+pre code { padding: 0; background: none; overflow-wrap: normal; white-space: pre; }
+table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; font-size: .95rem; }
+th, td { padding: .45rem .75rem; border: 1px solid var(--line); text-align: left; vertical-align: top; }
+th { background: #e7ede8; }
+td code { overflow-wrap: normal; white-space: nowrap; }
+li > p { margin-block: .5rem; }
+.related { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); }
+
+/* Lists of guides */
+.post-list { list-style: none; padding: 0; }
+.post-list li { padding-block: 1rem; border-bottom: 1px solid var(--line); }
+.post-list h2, .post-list h3 { margin: 0; }
+.post-list p { margin-block: .35rem; }
+
+@media (max-width: 30rem) {
+ .wrapper { width: min(100% - 2rem, 68rem); }
+ body { font-size: 1rem; }
+}
diff --git a/blog.md b/blog.md
new file mode 100644
index 0000000..0fa72b4
--- /dev/null
+++ b/blog.md
@@ -0,0 +1,28 @@
+---
+title: FreeRADIUS guides
+description: >-
+ Tested FreeRADIUS guides on clients.conf, RADIUS client troubleshooting,
+ and managing clients with the RadiusDeck web interface.
+permalink: /blog/
+---
+
+# FreeRADIUS guides
+
+Each guide is checked on an isolated test setup. The tested versions and the
+date of the last check are listed at the top of every article.
+{: .lead }
+
+{% if site.posts.size > 0 -%}
+