no-js is an opinionated Go framework for server-rendered web applications.
It is built around a strict web/* app tree, a generated App Bundle, and a
convention-first runtime.
For app developers using no-js:
- Getting Started
The shortest path from empty app tree to
httpserver.NewApp(...). - App Conventions
The strict
web/*contract, generated outputs, and reserved route files. - Feature Guides Overview The module-by-module guide for public framework features.
For contributors developing no-js itself:
- Developing
no-jsRepository boundaries, main implementation areas, and contributor rules. - AI Agents A framework-repo reading order and editing guide for agents.
- Routing and Generation
Route tree conventions, generated handlers, resolver contracts, and the
App Bundle. - HTTP Server and Runtime
httpserver.NewApp(...), default runtime wiring, andCustom Config. - Metadata and Head
MetaContext,<head>composition, alternates, and HTMX metadata patches. - i18n Localized routing, generated message keys, and request-scoped translation context.
- Discovery
robots.go,feed.go,sitemap.go, and generated sitemap chunks. - Static Assets Fingerprinted asset output, manifests, runtime prefixes, and public-file boundaries.
- Site Resolution
Site Resolvercontracts and request-aware canonical roots. - Request Cache and Partials Request-scoped cache sharing and HTMX partial rendering behavior.
The route generator is strict. The happy path assumes:
web/routes route tree
web/generated generated route modules and App Bundle boundary
web/resolvers handwritten resolver methods
web/view runtime contracts used by generated code
web/assets source bundled static assets
web/assets-build generated hashed static assets
web/public fixed-path public files served by convention
See App Conventions for the full layout and route rules.
The preferred runtime integration is:
handler, err := httpserver.NewApp(httpserver.Config[*runtime.Context]{
App: generated.Bundle(appContext),
Custom: customConfig,
})Terminology:
Framework Config: generic runtime behavior only
App Bundle: generated route/runtime contract
Custom Config: isolated app-specific hooks
Site Resolver: shared domain and canonical URL policy
Advanced composition: any app-owned package, only when needed
Convention defaults:
Assets manifest: web/assets-build/manifest.json
Static prefix: /_assets/
Public files: web/public
Localization: auto-wired when built-in i18n is enabled and `web/i18n/messages` exists
Reserved files under web/routes return structured discovery data. sitemap.go
and feed.go may live at the route root or in nested route directories. The
framework owns the transport, endpoint paths, and XML/text rendering:
robots.goreturnsdiscovery.Robotssitemap.goreturns[]discovery.SitemapEntry, plus optionalGenerateSitemapsandSitemapByIDfeed.goreturnsdiscovery.FeedDocument
Use the exported structs in framework/discovery/discovery.go as the
field-level source of truth.
Specification references:
- Robots Exclusion Protocol: RFC 9309
- RSS 2.0: RSS Specification
- XML Sitemaps: Sitemaps XML format
- Alternate-language sitemap links: Google Search Central hreflang guidance
- Image sitemap extensions: Google Search Central image sitemaps
no-js supports an optional root config file named no-js.bundle.yaml.
This file is for build-time configuration only. It controls deterministic inputs such as project layout paths, feature flags used during layout resolution, and static asset build settings.
If no-js.bundle.yaml is missing, the CLI uses framework defaults. A missing config file is not an error.
If the file exists, version: 1 is required and unknown fields are rejected.
YAML values override defaults field by field. Unspecified fields keep their default values.
There are no globally required operational YAML fields. Requirements are resolved from the command and enabled
features. For example, go.mod, web/routes, and web/view must exist, the resolved i18n directory must exist if
i18n routing is enabled, and static asset paths must be valid when running static asset generation.
Invalid YAML is an error.
no-js.bundle.yaml must not contain runtime or environment-specific values. Keep process-time configuration such as
listen address, site-resolution policy, API tokens, analytics IDs, cache overrides, advanced asset overrides, and
service wiring in app-owned Go server code.
Example default-equivalent config:
version: 1
project:
routes_dir: web/routes
generated_dir: web/generated
resolvers_dir: web/resolvers
view_dir: web/view
i18n_dir: web/i18n
assets_dir: web/assets
assets_build_dir: web/assets-build
server:
features:
i18n_routing: auto
static_assets: auto
health_endpoint: auto
i18n:
mode: auto
static_assets:
manifest_path: web/assets-build/manifest.jsonmanifest_path points to generated static-bundle metadata. It stores the asset hash used by the runtime to construct
the final versioned asset prefix. The happy path uses runtime convention defaults instead of manual asset wiring.
- This framework is intentionally opinionated. The generator assumes the strict
web/*layout and specific template signatures. - The preferred runtime integration is
generated.Bundle(appContext)withhttpserver.NewApp(...). - Generated code imports
web/view, but current view contracts still use the package identifierruntime. - The generator is module-aware: framework imports point to
github.com/RevoTale/no-js, but generated app imports are resolved from the consuming app'sgo.mod. - Localization is convention-first: built-in i18n is generated when enabled and
web/i18n/messagesexists. - Advanced composition is supported, but it is not tied to a reserved package or directory name.
- Site and canonical-domain policy should be centralized through a
Site Resolver. - i18n locales are currently normalized to two-letter lowercase codes.
- HTMX support is request-driven. Partial requests are detected through
HX-Request, and metadata patches are emitted through response headers. - Static assets and public files are separate concerns:
/_assets/is the default runtime prefix for fingerprinted build output, while public files are served as fixed request paths.
no-js originated as an extraction from RevoTale/blog.