An Astro-inspired static site implementation built as a Vite plugin with SolidJS and TSX.
Read the introduction: Another static site generator.
Install from npm:
nub add solid-staticBuild this project and add it to your app as a local or workspace dependency. Then configure it in vite.config.ts:
import{defineConfig}from"vite";import{staticSite}from"solid-static";import{createHtmlMarkdownProcessor,solidMarkdown,}from"solid-static/markdown";import{responsiveImages}from"solid-static/responsive-images";exportdefaultdefineConfig({plugins: [staticSite({collections: {},i18n: {defaultLocale: "en",locales: ["en"],routing: {prefixDefaultLocale: false},},integrations: [solidMarkdown(),responsiveImages()],markdown: {processor: createHtmlMarkdownProcessor()},markdownExport: {exclude: [],force404Markdown: true},trailingSlash: "always",}),],});Set markdownExport to generate a Markdown sibling for every emitted HTML
page at build time. The generated files are output-relative route documents
(index.md, about/index.md, and so on), with no extra slashless .md
aliases. For Cloudflare Pages, pair this with a Free Transform Rule matching
GET requests whose Accept header contains text/markdown and rewrite the
path dynamically with concat(http.request.uri.path, "index.md"). Thus /
maps to /index.md and /about/ maps to /about/index.md without a Worker.
The option is static-only and does not add a runtime server or Worker. Keep
404.html included and set force404Markdown: true when the same Free
Cloudflare setup should serve the generated Markdown body for missing paths.
The generated 404 asset remains a real 404 and can be labeled
Content-Type: text/markdown by a response-header Transform Rule when the
request accepts Markdown.
staticSite({// ...other optionsmarkdownExport: {exclude: ["404.html"],selectors: ["main","article","body"],transform: (markdown,fileName)=>`<!-- generated from ${fileName} -->\n\n${markdown}`,},});Add .tsx, .md, or .mdx pages under src/pages. The directory structure determines each page's route. Markdown pages must declare a SolidJS layout in their frontmatter.
Import a self-mounting browser entry with the ?island query, then reference the returned URL from a module script. The page remains static HTML; only the named entry and its imports are compiled for the browser.
importcounterIslandfrom"../app/counter-island.tsx?island";exportdefault()=>(<html><body><divid="counter">0</div><scripttype="module"src={counterIsland}/></body></html>);import{createSignal}from"solid-js";import{render}from"solid-js/web";constCounter=()=>{const[count,setCount]=createSignal(0);return<buttononClick={()=>setCount(value=>value+1)}>{count()}</button>;};constroot=document.querySelector("#counter");if(!(rootinstanceofHTMLElement)){thrownewTypeError("Missing #counter island root");}render(()=><Counter/>,root);Vite serves the source entry during development. Production builds emit hashed JavaScript and CSS assets and rewrite only pages that reference the island.
Use client to configure the clean nested browser build explicitly. It accepts
Vite configuration such as aliases, defines, mode, CSS options, browser-only
plugins, and build target or minification settings. Server integrations are not
forwarded automatically.
staticSite({client: {build: {minify: false,target: "es2020"},define: {__BROWSER__: "true"},resolve: {alias: {"@client": "/src/client"}},},// Other static-site options.});Page components and Markdown or MDX layouts receive a route prop. route.path is the page's absolute public URL pathname. It never contains a query or hash and never exposes an internal route ID or output file name. Dynamic parameters are expanded before the pathname is normalized.
| Page | trailingSlash: "always" | trailingSlash: "never" |
|---|---|---|
| Root | / | / |
Static TSX guides.tsx | /guides/ | /guides |
Markdown or MDX guides.md | /guides/ | /guides |
Dynamic guides/[slug].tsx, slug example | /guides/example/ | /guides/example |
Custom 404.tsx | /404 | /404 |
route.fileName remains output-relative: for example, guides/index.html in "always" mode and guides.html in "never" mode. A custom 404 is always emitted as 404.html, while its route identity remains /404. When the development server uses that page to answer a missing URL, route.path remains /404; it does not represent the original request pathname.
Import an image through Vite, then render it with ResponsiveImage in a SolidJS page or component:
importherofrom"../assets/hero.jpg";import{ResponsiveImage}from"solid-static/image";exportdefaultfunctionHome(){return(<ResponsiveImagesrc={hero}alt="Mountain landscape"width={1600}height={900}layout="constrained"widths={[480,768,1200,1600]}sizes="(max-width: 768px) 100vw, 1200px"format="webp"loading="lazy"/>);}The responsive images integration generates the requested variants and adds the resulting srcset during development and production builds.
Use getImage() during server rendering to generate one transformed image. It follows Astro's getImage() pattern for images used outside a standard image component. Import it from solid-static/image, then await it at module scope or inside an async server-rendered component:
importsourcefrom"../assets/social-preview.png";import{getImage}from"solid-static/image";constpreview=awaitgetImage({src: source,width: 1200,height: 630,format: "jpg",quality: "high",fit: "cover",position: "center",});exportdefaultfunctionPage(){return(<html><head><metaproperty="og:image"content={preview.src}/></head><body><imgsrc={preview.src}alt=""{...preview.attributes}/></body></html>);}getImage(options) accepts:
| Option | Type | Default | Description |
|---|---|---|---|
src | string | ImageMetadata | required | Imported image URL or { src, width, height, format } metadata. |
width | positive integer | source width | Output width. |
height | positive integer | source height | Output height. |
format | "avif" | "jpeg" | "jpg" | "png" | "webp" | "webp" | Output format. |
quality | 0–100 or "low" | "mid" | "high" | "max" | encoder default | Output quality. |
fit | "contain" | "cover" | "fill" | "inside" | "outside" | "cover" | How the source fits the requested dimensions. |
position | string | "center" | Crop or embed position used by the image transformer. |
When src contains image metadata, specifying only width or height infers the other dimension while preserving the aspect ratio. The returned promise resolves to a GetImageResult containing the generated src, inferred attributes, normalized options, original rawOptions, and an Astro-compatible srcSet object. Generated URLs work in both the Vite development server and production builds. getImage() throws if called in the browser.
Dedicated documentation is not available yet. For the concepts and intended behavior, see the corresponding Astro guides: