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/index.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:allOpenGraph smoke checks are part of the validation flow. For a manual localhost preview, run:
pnpm -s og:check http://127.0.0.1:4173/The internal checker is inspired by Simon Hartcher's localhost OpenGraph workflow and the og-check tool from deevus/neutils, but implemented here in plain Node.js for local validation, CI, and deploy gates.
If 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 providerThe build process expects specific favicon/manifest assets at the project root. Expected names (as defined in favicon.data.json) include:
favicon-96x96.pngfavicon.svgfavicon.icoapple-touch-icon.png(recommended size: 180x180)site.webmanifest(web manifest)
Where to place them:
- Copy these files to the repository root (same level as
package.json).scripts/build-assets.jsresolves paths exactly as listed infavicon.data.json.
What the build script does:
- If files are missing, it generates placeholder files (minimal PNG/SVG/manifest) in
dist/to support local preview and debugging. - Even with placeholders generated, the build is still designed to fail when real files are missing, so CI reports a clear error and prevents incomplete releases.
Local check:
- Install dependencies:
pnpm install - Run build:
pnpm build - If real favicon assets are missing, you will see an error like
Missing favicon assets, and placeholders will still be generated indist/.
How to fix build failure:
- Add the real files to the root with expected names.
- Temporary workaround before build: create empty files with expected names.
touch favicon-96x96.png favicon.svg favicon.ico apple-touch-icon.png site.webmanifest- If you want different behavior (for example downgrade failure to warning), update
scripts/build-assets.jsinprocessFaviconsand remove thethrowafter placeholder generation.
Recommendations:
- In production, use real PNG/SVG/ICO files at recommended sizes (PNG 48-512px, scalable SVG, and
apple-touch-iconat 180x180). - Update
favicon.data.jsonwhen filenames or paths change.
Use this map as the entry point for repository policies, contribution flow, moderation, and publishing operations.
Core community and contribution:
README_COMMUNITY.md: Community-first summary and contributor path overview.CONTRIBUTING.md: How to contribute articles and source code, including review and voting expectations.CODE_OF_CONDUCT.md: Expected behavior, unacceptable behavior, reporting, enforcement, and appeals.SUPPORT.md: Where to ask questions, report bugs, and route security/moderation concerns.
Editorial policy and publishing:
CONTENT_GUIDELINES.md: Relevance criteria, article quality standards, attribution rules, and AI disclosure.ARTICLE_TEMPLATE.md: Ready-to-use article structure and frontmatter template.PUBLISHING_STRATEGY.md: End-to-end publishing and promotion workflow, including Discussion and LinkedIn patterns.
Governance and roles:
GOVERNANCE.md: Decision model, voting rules, veto boundaries, conflict-of-interest handling, and governance updates.CONTRIBUTION_TIERS.md: Transparent progression from reader to contributor and maintainer roles.docs/maintainer-vote-examples.md: Practical voting scenarios for article and maintainer decisions.
Safety, moderation, and legal:
SECURITY.md: Private vulnerability reporting scope, process, and disclosure guidance.MODERATION_POLICY.md: Moderation levels, escalation path, and appeal model.LICENSE_POLICY.md: Proposed dual-license strategy for source code and editorial content.OPEN_SOURCE_CHECKLIST.md: Operational checklist for community health, GitHub settings, legal baseline, and moderation readiness.
GitHub collaboration templates and ownership:
.github/ISSUE_TEMPLATE/article-proposal.yml: Structured article proposal intake..github/ISSUE_TEMPLATE/bug-report.yml: Bug reports for site/repository issues..github/ISSUE_TEMPLATE/feature-request.yml: Feature and improvement proposals..github/ISSUE_TEMPLATE/config.yml: Issue template defaults and contact links..github/PULL_REQUEST_TEMPLATE.md: PR structure, article/code checklists, impact, and maintainer decision block..github/CODEOWNERS_TEMPLATE.md: Starter ownership mapping to be renamed as.github/CODEOWNERS..github/dependabot.yml: Weekly dependency update automation and labeling.
- 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).