dout.dev - Vanilla-first static blog with WCAG 2.2 AA accessibility and zero runtime dependencies
dout.dev is a modern static blog built with vanilla JavaScript, CSS, and HTML, focusing on performance, accessibility, and maintainability. The project follows a custom SSG approach with zero runtime dependencies.
- 🎯 Vanilla-first: Zero runtime dependencies, pure JS/CSS/HTML
- ♿ Accessibility: WCAG 2.2 AA compliant with semantic markup
- ⚡ Performance: Optimized for Core Web Vitals with PWA capabilities
- 🔍 SEO: Complete meta tags, JSON-LD, OG images, RSS feeds
- 📱 Progressive: Modern CSS with progressive enhancement
- 🎨 Design System: Proprietary vanilla CSS system with design tokens
- 🔧 Developer Experience: TypeScript support, live reload, comprehensive tooling
- CMS: Custom content management system for Markdown processing
- Template Engine: Proprietary template system with include/extend/blocks/expressions
- Build Pipeline: Vite-based bundling with PostCSS optimization
- Deploy: GitHub Pages with automated CI/CD pipeline
- PWA: Service Worker caching with offline support
This repository already contains the generator. The workflow is split into two phases: content generation with the in-repo CMS, then asset bundling with Vite.
pnpm install- Add markdown posts in
data/posts/. - Use front matter for title, date, description, tags, and optional cover fields.
- Reusable HTML lives in
src/components/and page templates live insrc/templates/.
Minimal example:
---
title: 'Your Post Title'
date: '2026-04-04'
published: true
tags: ['css', 'accessibility']
description: 'Short summary used for cards and metadata'
cover_image: ../assets/images/example-cover.jpg
cover_alt: A descriptive alternative text for the cover
canonical_url: false
---
## Start writing
Your markdown content goes here.pnpm -s cms:buildThis step parses markdown, normalizes metadata, generates archive/search data, and writes the derived HTML files into src/.
pnpm -s devDo not open files inside src/ directly in the browser when validating the UI. The project relies on root-relative
asset paths such as /styles/main.css, and Vite rewrites production assets during build. Use the dev server or the
production preview instead.
If Safari shows TLS failures for local assets while you are on http://127.0.0.1:3000/, check that the generated HTML
does not contain upgrade-insecure-requests in the meta CSP. That directive belongs in response headers for production,
not in the dev HTML served over plain HTTP.
If you open src/*.html or dist/*.html through file://, Chromium will also block linked CSS and JS as CORS
requests from origin null. That failure is expected browser behavior, not a missing stylesheet in the repo.
For content-heavy work you can also run the CMS watcher in a second terminal:
pnpm -s cms:watchpnpm -s test
pnpm -s format:check
pnpm -s validate:allIf you only need the full gate in one shot:
pnpm -s quality:checkpnpm -s buildThis runs the image pipeline, rebuilds CMS output, bundles the site with Vite, copies static assets, and verifies the final dist/ artifact.
pnpm -s preview- Create or edit markdown in
data/posts/. - Update templates/components in
src/templates/orsrc/components/when structure changes. - Run
pnpm -s cms:buildafter content or template edits. - Use
pnpm -s devto inspect the generated site. - Run
pnpm -s quality:checkbefore merging or publishing.
data/posts/: source articles in markdown.scripts/cms/: normalization, page generation, archive indexes, image metadata.scripts/template-engine/: custom HTML-oriented rendering engine.src/templates/: source page templates.src/components/: reusable HTML fragments.src/styles/: global design system and component styling.docs/: project notes, roadmap, and auxiliary design/color references.
- Edit and keep:
data/posts/,src/components/,src/layouts/,src/templates/,src/styles/,src/scripts/,scripts/cms/,scripts/template-engine/,_headers,cspell.config.json,package.json,vite.config.js. - Generated by the CMS, not by hand:
src/posts/,src/tags/,src/months/,src/series/,src/data/*.json,src/feed.json,src/feed.rss,src/feed.xml,src/sitemap.xml. - Disposable build or test artifacts:
dist/,test-results/,playwright-report/. - Reference or development support:
tests/,docs/,design/,custom/.
When cleaning the repository, start from disposable artifacts first. Remove reference or development folders only if you are sure you no longer need their documentation, fixtures, or design assets.
# Install dependencies
pnpm install
# Build content from Markdown
pnpm -s cms:build
# Start development server
pnpm -s dev
# Build for production
pnpm -s build
# Preview production build
pnpm -s previewpnpm buildnow produces a GitHub Pages-readydist/artifact, includingCNAME, RSS feeds,sitemap.xml, search indexes, andsw.js..github/workflows/deploy-pages.ymldeploys automatically on pushes tomainand on manual dispatch.- In repository settings, set Pages to use
GitHub Actionsand configure the custom domain asdout.dev. CNAMEremains in the repo for the apex domain build artifact.
Giscus integrates GitHub Discussions for post comments. Configuration is driven by environment variables read at build time.
-
Get Giscus values from giscus.app configuration wizard. You need:
- Repository (owner/repo)
- Discussion category
- Both IDs (from GitHub API)
-
Copy the template:
cp .env.example .env.local
-
Update
.env.localwith your Giscus values:GISCUS_REPO=your-owner/your-repo GISCUS_REPO_ID=<from giscus.app> GISCUS_CATEGORY=General GISCUS_CATEGORY_ID=<from giscus.app> GISCUS_MAPPING=url GISCUS_STRICT=0 # ... other settings
(
.env.localis git-ignored and stays local) -
For production (GitHub Actions): set these as repository secrets in GitHub:
GISCUS_REPOGISCUS_REPO_IDGISCUS_CATEGORYGISCUS_CATEGORY_ID
GitHub Actions will inject them at build time via
${{ secrets.GISCUS_REPO }}etc.
GISCUS_REPO– owner/repo format (e.g.,pixu1980/dout-dev)GISCUS_REPO_ID– repository ID from GitHub APIGISCUS_CATEGORY– discussion category nameGISCUS_CATEGORY_ID– category ID from GitHub APIGISCUS_MAPPING–url(default),pathname,title,og:title,specificGISCUS_STRICT–0(allow unlisted URLs) or1(strict mode)GISCUS_REACTIONS_ENABLED–1(enable emoji reactions)GISCUS_EMIT_METADATA–1(emit title/URL metadata to discussions)GISCUS_INPUT_POSITION–toporbottom(comment form placement)GISCUS_THEME–dark,light,preferred_color_scheme, or customGISCUS_LANG– language code (e.g.,en)GISCUS_LOADING–lazyoreager
- Comments are enabled/disabled based on presence of all 4 required env vars
- Build-time rendering: Giscus config is baked into the
<script>tag in generated post pages - Discussions are keyed to the post's published URL (
GISCUS_MAPPING=url) - Each discussion thread title includes the source markdown path for reference
├── scripts/
│ ├── cms/ # Content management pipeline
│ │ ├── build.js # Main CMS build script
│ │ ├── posts.js # Post generation
│ │ ├── months.js # Month archive generation
│ │ └── tags.js # Tag page generation
│ └── template-engine/ # Custom template engine
├── data/ # Markdown source files
│ └── posts/ # Blog posts in Markdown
├── src/ # Generated static HTML files
│ ├── templates/ # HTML templates
│ │ ├── layouts/ # Base layouts
│ │ └── components/ # Reusable components
│ ├── styles/ # CSS with proprietary design system
│ ├── posts/ # Generated post pages
│ ├── months/ # Generated month archives
│ ├── tags/ # Generated tag pages
│ ├── sw.js # Service Worker for PWA
│ ├── manifest.json # Web App Manifest
│ └── performance-test.html # Performance testing suite
├── dist/ # Final build output
└── .github/ # CI/CD workflows
The CMS system processes Markdown files with YAML front-matter:
# Build all content
pnpm -s cms:build
# Watch for content changes (development)
pnpm -s cms:watch
# Clean generated files
pnpm -s cms:cleanCreate Markdown files in data/posts/ with this front-matter:
---
title: 'Your Post Title'
date: '2025-08-15'
tags: ['javascript', 'css', 'performance']
description: 'Brief description for SEO and social sharing'
published: true
---
Your content here...Templates use a custom syntax with powerful features:
<extends src="./layouts/base.html">
<block name="title">{{ title }} - dout.dev</block>
<block name="content">
<h1>{{ title | capitalize }}</h1>
<p>{{ description | default:"No description available" }}</p>
<if condition="tags.length > 0">
<ul>
<for each="tag in tags">
<li><a href="/tags/{{ tag.key }}.html">{{ tag.label }}</a></li>
</for>
</ul>
</if>
</block>
</extends>- Inheritance:
<extends>and<block>for layout structure - Composition:
<include>for reusable components - Expressions:
{{ variable | filter }}with built-in filters - Control Flow:
<if>,<for>,<switch>statements - Security: Expression sandboxing without
eval()
- Do NOT place
<if>elements inside an opening tag to conditionally add attributes.- Instead, use JavaScript expressions (ternary or logical OR) inside attribute values.
- Examples:
- width="{{ post.coverWidth ? post.coverWidth : '' }}"
- height="{{ post.coverHeight || '' }}"
- Avoid:
<img <if condition="post.coverWidth">width="{{ post.coverWidth }}"</if> />
- PWA Ready: Service Worker with caching strategies
- Critical CSS: Inlined above-the-fold styles
- Lazy Loading: Images and below-the-fold content
- Code Splitting: Per-page CSS and JavaScript bundles
- Compression: Gzip optimization and minification
The Markdown renderer supports responsive images with lazy loading, <picture> with WebP+raster sources, and <noscript> fallback.
- Title meta syntax (segments separated by
|):srcset=...candidates list (path 320w, path 640w)sizes=...sizes descriptor ((max-width: 640px) 100vw, 640px)loading=eager|lazy(default: lazy)priority=high|lowsetsfetchpriority(default: low). Ifpriority=highorloading=eager, lazy/noscript are disabled and attributes are inlined for LCP.
Example:
 100vw, 640px")
Notes:
- For local PNG/JPEG assets the engine tries to add
width/heightto reduce CLS. - If
srcsetisn’t provided, it’s built automatically fromsrc/assets/images-manifest.json. - Output uses
<picture>: WebP<source>+ raster<source>; eager mode emits realsrcset, lazy usesdata-srcsetand a<noscript><img>fallback. - For LCP images (e.g., covers) prefer
loading=eager | priority=high.
- Theme modes: auto (default), light, dark. The toggle in the header cycles through Auto → Dark → Light → Auto.
- Persistence: user choice is stored in localStorage under
themeand applied by settingdocumentElement.dataset.theme. - System preference: when set to Auto,
prefers-color-schemedecides between light/dark; switching OS theme updates the site live. - Accent: choose among Default, Violet, Green. Stored as
accentin localStorage and applied asbody[data-accent]. - A11y: header menu is keyboard accessible with a focus trap when open; Escape closes it; outside click closes it on touch/mouse.
Run pnpm -s images:generate to create responsive variants for images under src/assets/images:
- Resized variants for JPG/PNG:
-320,-640,-960,-1280(no upscaling) - WebP base and matching WebP variants
- A manifest written to
src/assets/images-manifest.json
The pnpm build script runs this step automatically before CMS and Vite.
- URL scheme:
- Page 1 flat:
/tags/slug.html,/months/YYYY-MM.html(and/series/<name>.html) - Pages 2+: subfolder with
index.html:/tags/slug/2/,/months/YYYY-MM/2/
- Page 1 flat:
- A11y/SEO:
rel="prev"/"next",aria-current="page", ellipses non-clickable
- Archives generated:
- Tags:
src/tags/<slug>.html+ RSSsrc/tags/<slug>.xml - Months:
src/months/<YYYY-MM>.html+ RSSsrc/months/<YYYY-MM>.xml - Series:
src/series/<slug>.html
- Tags:
Quick use:
- In a listing template, include the shared UI:
<include src="../components/pagination.html"></include>
- Expose
paginationfrom your generator to drive the component. - Add RSS link in
<head>of tag/month pages:<link rel="alternate" type="application/rss+xml" href="{{ canonicalUrl.replace('.html', '.xml') }}" />
- Use fenced code blocks in Markdown (
js,css, ```html, etc.). - Renderer outputs
<pre is="pix-highlighter" lang="..."><code>…</code></pre>. - Supported lexers: js, ts, css, html, json, md, bash, python, go, rust, c, cpp, php, csharp, yaml.
title,date,description,tags,published- Optional:
coverImage,pinned,series,keywords,layout coverImagelocal (PNG/JPG): width/height inferred automatically when possible; benefits responsive pipeline.
Built with proprietary vanilla CSS system and custom design tokens:
/* Spacing system */
padding: var(--space-4);
margin: var(--space-6);
/* Typography scale */
font-size: var(--text-lg);
line-height: var(--font-lineheight-3);
/* Color system */
color: var(--text-primary);
background: var(--surface-1);Performance testing suite available at /performance-test.html:
- PWA functionality validation
- Service Worker cache testing
- Performance metrics monitoring
- Design system verification
- Modern Browsers: Chrome 90+, Firefox 90+, Safari 14+, Edge 90+
- Progressive Enhancement: Graceful degradation for older browsers
- Accessibility: Screen reader compatible, keyboard navigation
Automated deployment to GitHub Pages:
- Push to
mainbranch - GitHub Actions runs build process
- Deploys to
https://dout.dev
Manual deployment:
pnpm build
# Upload dist/ to your hosting providerIl processo di build si aspetta che alcuni asset di favicon/manifest siano presenti alla radice del progetto. I nomi attesi (come riferiti in favicon.data.json) sono, ad esempio:
favicon-96x96.pngfavicon.svgfavicon.icoapple-touch-icon.png(180x180 consigliato)site.webmanifest(web manifest)
Dove posizionarli
- Copia i file nella root del repository (stesso livello di
package.json). Lo scriptscripts/build-assets.jscercherà i percorsi esattamente come indicati infavicon.data.json.
Cosa fa lo script di build
- Se i file sono mancanti, lo script ora genera dei placeholder (file PNG/SVG/manifest minimi) dentro
dist/in modo da permettere preview e debug. - Nonostante i placeholder vengano creati, la build è comunque progettata per fallire quando mancano i file reali: questo provoca un errore chiaro in CI così da prevenire pubblicazioni incomplete.
Test locale
- Per verificare localmente:
- Installa dipendenze:
pnpm install - Esegui:
pnpm build - Se mancano i favicon reali, vedrai un errore come
Missing favicon assetse i placeholder saranno comunque creati indist/.
- Installa dipendenze:
Come risolvere il fallimento
- Aggiungi i file reali nella root con i nomi attesi.
- In alternativa (temporaneo) puoi creare dei file vuoti con i nomi corretti prima di eseguire la build:
touch favicon-96x96.png favicon.svg favicon.ico apple-touch-icon.png site.webmanifest- Se preferisci cambiare il comportamento (es. trasformare il fallimento in warning), modifica
scripts/build-assets.jsnella funzioneprocessFaviconsrimuovendo ilthrowdopo la creazione dei placeholder.
Suggerimenti
- For production usa immagini reali (PNG/SVG/ICO) alle risoluzioni consigliate: 48–512px per PNG, SVG per scalabilità e
apple-touch-icona 180x180. - Aggiorna
favicon.data.jsonse cambi i nomi o i percorsi.
- Fork the repository
- Create a feature branch
- Make your changes
- Test with
pnpm build - Submit a pull request
MIT License - see LICENSE for details.
Live Site: https://dout.dev
Repository: https://github.com/pixu1980/dout-dev
- The search page (
/search.html) loads static JSON datasets from/data/posts.json,/data/tags.json,/data/months.json,/data/series.json. - URL parameters:
q: the search termpage: the page number (1-based)type: optional, repeated param to filter result types. Allowed values:post,tag,series,month. Example:?q=css&type=post&type=tag
- Accessibility: the form has
role="search"; results summary usesaria-live="polite"and announces page changes. - Filters: a fieldset of checkboxes lets you include/exclude types (posts, tags, series, months). Selection is reflected in the URL (
type=...). - Pagination: client-side (10 items/page). UI and semantics aligned with the shared pagination component (Prev/Next,
aria-current, links preserveqandtype). - Ranking: simple text matching with light boosts for title/tags; extra boost applied on exact keyword matches (keywords are extracted at build time).