Turn Markdown into a fully-typed, i18n-ready, static React site β zero runtime, no MDX, no server.
Works in any Vite + React project. Drop it in, point to your content folder, done.
| Problem | Flatwave Solution |
|---|---|
| MDX locks you into custom components | Pure Markdown β use any React component via virtual module |
| i18n is an afterthought | First-class i18n β locale routes (/en/about, /es/about), auto hreflang, language switcher |
| TypeScript support is partial | Fully typed β virtual module + React hooks with full IntelliSense |
| SSG requires complex config | Zero-config SSG β static HTML at build, deploy to Netlify, Vercel, S3, GitHub Pages, Nginx |
| Content validation is manual | Built-in validation β catches missing fields, duplicate IDs, broken links at build time |
# 1. Install
npm install @kamansoft/vite-plugin-flatwave-react
# 2. Add to vite.config.ts
import { flatwaveContent } from '@kamansoft/vite-plugin-flatwave-react';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [
react(),
flatwaveContent({
contentDir: path.resolve(__dirname, 'src/content'),
locales: ['en', 'es'],
defaultLocale: 'en',
}),
],
});
# 3. Create content
mkdir -p src/content/en
cat > src/content/en/index.md <<'EOF'
---
title: 'Welcome'
slug: ''
id: 'home'
public: true
description: 'My first Flatwave page'
---
# Hello Flatwave! π
This is **pure Markdown** β no MDX, no custom components needed.
EOF
# 4. Run dev server
npm run devOpen http://localhost:5173/en/ β your page is live with hot reload! π
Use the virtual module hooks + components in your existing React Router setup:
// src/pages/[id].tsx
import { getContent, getAlternatives } from 'virtual:flatwave/content';
import { FlatwaveMDPageComponent } from '@kamansoft/vite-plugin-flatwave-react/react';
import Layout from '../components/Layout';
export default function Page({ params }: { params: { id: string } }) {
const { id } = params;
const locale = import.meta.env.VITE_CURRENT_LOCALE || 'en';
const content = getContent(id, locale);
const alternatives = getAlternatives(id, locale);
if (!content) return <div>404 - Not Found</div>;
return (
<Layout locale={locale} alternatives={alternatives}>
<FlatwaveMDPageComponent
frontmatter={content.frontmatter}
markdownHtml={content.body}
locale={locale}
/>
</Layout>
);
}React hooks for use anywhere in your components:
import {
useFlatwaveContent, // Get single content entry
useFlatwaveRoutes, // Get all routes (optionally filtered by locale)
useFlatwaveAlternatives, // Get locale alternatives for language switcher
useFlatwaveLocales, // Get all configured locales
} from '@kamansoft/vite-plugin-flatwave-react/react';
function BlogPost({ id, locale }) {
const post = useFlatwaveContent(id, locale);
const alternatives = useFlatwaveAlternatives(id, locale);
const locales = useFlatwaveLocales();
if (!post) return <div>Loading...</div>;
return (
<article>
<h1>{post.frontmatter.title}</h1>
<nav>
{Object.entries(alternatives).map(([lang, path]) => (
<a key={lang} href={path}>
{lang.toUpperCase()}
</a>
))}
</nav>
<FlatwaveMDComponent
frontmatter={post.frontmatter}
markdownHtml={post.body}
locale={locale}
/>
</article>
);
}Let Flatwave handle routing, language detection, and rendering automatically:
// src/App.tsx
import {
FlatwaveLanguageRouter,
FlatwaveMDPageComponent,
useFlatwaveRoutes,
useFlatwaveContent,
} from '@kamansoft/vite-plugin-flatwave-react/react';
export function App() {
const routes = useFlatwaveRoutes();
return (
<FlatwaveLanguageRouter
supportedLanguages={['en', 'es']}
defaultLanguage="en"
routes={routes}
renderPage={(route, lang) => {
const content = useFlatwaveContent(route.contentId, lang);
return (
<FlatwaveMDPageComponent
frontmatter={content?.frontmatter}
markdownHtml={content?.body}
locale={lang}
/>
);
}}
/>
);
}That's it. You get:
- Auto locale detection from browser language
- Locale-prefixed routes (
/en/about,/es/about) - Language switcher component built-in
- SEO meta tags from frontmatter
src/
βββ content/
βββ en/
β βββ index.md # β /en/
β βββ about.md # β /en/about
β βββ blog/
β βββ hello.md # β /en/blog/hello
βββ es/
βββ index.md # β /es/
βββ about.md # β /es/about
βββ blog/
βββ hello.md # β /es/blog/hello
Frontmatter (required fields):
---
title: 'About Us' # Page title β <title>, <h1>, og:title
slug: 'about' # URL segment β /{locale}/about
id: 'about' # Groups translations (same across locales)
public: true # false = hidden from routes, sitemap, manifest
description: 'Short desc' # meta description, og:description
canonical: '/en/about' # Optional, defaults to /{locale}/{slug}
robots: 'index, follow' # Default: index, follow
# SEO Extras
og:
title: 'Custom OG Title'
image: '/images/og-about.png'
twitter:
card: 'summary_large_image'
jsonLd:
'@context': 'https://schema.org'
'@type': 'WebPage'
# Navigation
menu: 'main' # 'main' | 'footer' | custom
menu_position: 2 # Sort order in menus
# Any extra keys β preserved in `attributes`
custom_field: 'value'
---
Your markdown content here...flatwaveContent({
// Required
contentDir: './src/content', // Where .md files live
locales: ['en', 'es', 'pt'], // All supported locales
defaultLocale: 'en', // Must be in locales[]
// Validation
strictMissingLocales: false, // true β missing locale = build error
requiredFields: ['title', 'slug', 'id', 'public'],
// SSG (enabled by default)
ssg: {
enabled: true,
compileMarkdown: {
allowRawHtml: false, // Allow raw HTML in markdown
remarkPlugins: [], // Custom remark plugins
rehypePlugins: [], // Custom rehype plugins
},
hooks: {
beforeRender: async (ctx) => {
/* inject data */
},
transformMarkdown: async (md, ctx) => {
/* pre-process */
},
transformHtml: async (html, ctx) => {
/* post-process */
},
afterRender: async (html, ctx) => {
/* side effects */
},
onError: async (err, ctx) => {
/* fallback HTML */
},
},
strategy: new DefaultRenderStrategy(), // Or custom RenderStrategy
},
// SEO
sitemap: {
hostname: 'https://mysite.com', // Required for sitemap.xml
},
// Output
emitRouteManifest: true, // route-manifest.json
emitSitemap: true, // sitemap.xml
emitRobotsTxt: true, // robots.txt
});| Feature | MDX / Next.js / Astro | Flatwave |
|---|---|---|
| Markdown | Standard + JSX | β Standard only |
| Components | Import in .mdx |
β Via virtual module + your components |
| i18n | Plugin/config heavy | β Zero-config, locale-first |
| Types | Partial / manual | β Full TypeScript from virtual module |
| SSG | Framework-dependent | β
react-dom/server β deploy anywhere |
| Bundle size | Includes runtime | β Zero runtime dependencies |
| Validation | Manual / runtime | β Build-time, fail-fast |
| Guide | Description |
|---|---|
| π Architecture | System design, module breakdown, Mermaid diagrams, type system |
| π οΈ Development | Coding standards, Docker, Husky, Git workflow, local npm linking |
| π CI/CD & Release | Semantic-release, OIDC publishing, GitHub Actions pipeline |
| π§ API Reference | Complete TypeScript API for virtual module, hooks, SSG, config |
We welcome contributions! See CONTRIBUTING.md.
Quick checklist for PRs:
- Conventional Commit title (
feat:,fix:,chore:, etc.) -
npm run validatepasses (format + lint + type-check + build + test) - Tests added for new features
MIT Β© KamanaSoft
- π Issue Tracker β Bug reports & feature requests
- π‘ Discussions β Questions, ideas, showcases
- π¬ Discord β Real-time chat with the Vite community
Built with β€οΈ for React developers who love Markdown and static sites.
If this project helps you, please consider giving it a β on GitHub!