Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
/vendor/
__pycache__/
*.pyc

# Article drafts stay local until release; see README "Drafts".
/_drafts/
2 changes: 2 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Page not found
permalink: /404.html
sitemap: false
robots: noindex
---
<p class="eyebrow">404</p>
<h1>Page not found.</h1>
Expand Down
70 changes: 59 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<slug>.md` and add it
in the release pull request. The URL stays `/blog/<slug>/`.

## Publishing

Expand All @@ -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 <https://radiusdeck.github.io/>
was confirmed by the repository owner.

## Troubleshooting and rollback

Expand All @@ -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/<slug>/
```

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/<slug>/`.
`robots.txt` allows all crawlers, including AI search crawlers.
10 changes: 10 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions _includes/head.html
Original file line number Diff line number Diff line change
@@ -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 -%}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title | escape }} | {{ site.title | escape }}</title>
<meta name="description" content="{{ description | escape }}">
<link rel="canonical" href="{{ canonical }}">
{%- if page.robots %}
<meta name="robots" content="{{ page.robots }}">
{%- endif %}
<meta property="og:site_name" content="{{ site.title | escape }}">
<meta property="og:title" content="{{ page.title | escape }}">
<meta property="og:description" content="{{ description | escape }}">
<meta property="og:url" content="{{ canonical }}">
<meta property="og:locale" content="en_US">
{%- if is_post %}
<meta property="og:type" content="article">
<meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}">
<meta property="article:modified_time" content="{{ modified | date_to_xmlschema }}">
{%- else %}
<meta property="og:type" content="website">
{%- endif %}
<meta name="twitter:card" content="summary">
<link rel="alternate" type="application/atom+xml" title="{{ site.title | escape }}" href="{{ '/feed.xml' | relative_url }}">
<link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
{%- if is_post %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": {{ page.title | jsonify }},
"description": {{ description | jsonify }},
"inLanguage": {{ site.lang | jsonify }},
"datePublished": {{ page.date | date_to_xmlschema | jsonify }},
"dateModified": {{ modified | date_to_xmlschema | jsonify }},
"mainEntityOfPage": {{ canonical | jsonify }},
"author": {"@type": "Organization", "name": {{ site.author.name | jsonify }}, "url": {{ site.author.url | absolute_url | jsonify }}},
"publisher": {"@type": "Organization", "name": {{ site.author.name | jsonify }}, "url": {{ site.author.url | absolute_url | jsonify }}}
}
</script>
{%- elsif page.url == "/" %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": {{ site.title | jsonify }},
"url": {{ "/" | absolute_url | jsonify }},
"inLanguage": {{ site.lang | jsonify }},
"publisher": {"@type": "Organization", "name": {{ site.author.name | jsonify }}, "url": {{ site.author.url | absolute_url | jsonify }}}
}
</script>
{%- endif %}
17 changes: 9 additions & 8 deletions _layouts/default.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
<!doctype html>
<html lang="{{ site.lang | escape }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title | escape }} | {{ site.title | escape }}</title>
<meta name="description" content="{{ page.description | default: site.description | escape }}">
<link rel="canonical" href="{{ page.url | absolute_url }}">
<link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
{% include head.html %}
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header wrapper">
<a class="brand" href="{{ '/' | relative_url }}">RadiusDeck<span> / Blog</span></a>
<a href="https://github.com/radiusdeck/radiusdeck">View project on GitHub ↗</a>
<nav class="site-nav" aria-label="Main">
<a href="{{ '/blog/' | relative_url }}"{% if page.url == '/blog/' or page.collection == 'posts' %} aria-current="page"{% endif %}>Guides</a>
<a href="{{ '/about/' | relative_url }}"{% if page.url == '/about/' %} aria-current="page"{% endif %}>About</a>
<a href="{{ site.product_url }}">GitHub ↗</a>
</nav>
</header>
<main id="main" class="wrapper" tabindex="-1">
{{ content }}
</main>
<footer class="site-footer wrapper">
Practical notes for FreeRADIUS administrators.
<p>Practical notes for FreeRADIUS administrators by the
<a href="{{ '/about/' | relative_url }}">{{ site.author.name }}</a>.
<a href="{{ '/feed.xml' | relative_url }}">RSS feed</a>.</p>
</footer>
</body>
</html>
37 changes: 37 additions & 0 deletions _layouts/post.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: default
---
{%- assign date_format = "%B %-d, %Y" -%}
<article class="post">
<header class="post-header">
<h1>{{ page.title | escape }}</h1>
<p class="post-meta">
By <a href="{{ site.author.url | relative_url }}">{{ site.author.name }}</a>
· Published <time datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date: date_format }}</time>
{%- if page.last_modified_at %}
· Updated <time datetime="{{ page.last_modified_at | date_to_xmlschema }}">{{ page.last_modified_at | date: date_format }}</time>
{%- endif %}
{%- if page.tested.date %}
· Last tested <time datetime="{{ page.tested.date | date: '%Y-%m-%d' }}">{{ page.tested.date | date: date_format }}</time>
{%- endif %}
</p>
</header>

{{ 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 %}
<aside class="related" aria-labelledby="related-heading">
<h2 id="related-heading">Related guides</h2>
<ul>
{%- for post in related_posts %}
<li><a href="{{ post.url | relative_url }}">{{ post.title | escape }}</a></li>
{%- endfor %}
</ul>
</aside>
{%- endif %}
</article>
44 changes: 44 additions & 0 deletions about.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
28 changes: 28 additions & 0 deletions blog.md
Original file line number Diff line number Diff line change
@@ -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 -%}
<ul class="post-list">
{%- for post in site.posts %}
<li>
<h2><a href="{{ post.url | relative_url }}">{{ post.title | escape }}</a></h2>
<p>{{ post.description | strip_html | normalize_whitespace | escape }}</p>
<p class="post-meta"><time datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date: "%B %-d, %Y" }}</time></p>
</li>
{%- endfor %}
</ul>
{%- else -%}
The first guides are in preparation. In the meantime, see the
[RadiusDeck documentation]({{ site.product_url }}/tree/main/docs/community).
{%- endif %}
24 changes: 24 additions & 0 deletions feed.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
layout: null
sitemap: false
---
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ site.lang }}">
<title>{{ site.title | xml_escape }}</title>
<subtitle>{{ site.description | strip | xml_escape }}</subtitle>
<link href="{{ '/feed.xml' | absolute_url }}" rel="self" type="application/atom+xml"/>
<link href="{{ '/' | absolute_url }}" rel="alternate" type="text/html"/>
<id>{{ '/' | absolute_url }}</id>
<updated>{{ site.time | date_to_xmlschema }}</updated>
<author><name>{{ site.author.name | xml_escape }}</name><uri>{{ site.author.url | absolute_url }}</uri></author>
{%- for post in site.posts limit: 20 %}
<entry>
<title>{{ post.title | xml_escape }}</title>
<link href="{{ post.url | absolute_url }}" rel="alternate" type="text/html"/>
<id>{{ post.url | absolute_url }}</id>
<published>{{ post.date | date_to_xmlschema }}</published>
<updated>{{ post.last_modified_at | default: post.date | date_to_xmlschema }}</updated>
<summary>{{ post.description | strip_html | normalize_whitespace | xml_escape }}</summary>
</entry>
{%- endfor %}
</feed>
Loading