cristianradulescu / staccato-bundle
Symfony bundle that turns Markdown content into a static site.
Package info
github.com/cristianradulescu/staccato-bundle
Type:symfony-bundle
pkg:composer/cristianradulescu/staccato-bundle
Requires
- php: >=8.5
- ext-ctype: *
- ext-iconv: *
- symfony/config: ^8.1
- symfony/console: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/filesystem: ^8.1
- symfony/finder: ^8.1
- symfony/http-foundation: ^8.1.7
- symfony/http-kernel: ^8.1
- symfony/twig-bundle: ^8.1
- symfony/yaml: ^8.1.6
- tempest/highlight: ^2.29
- tempest/markdown: ^1.1
- twig/twig: ^3.14
Requires (Dev)
- fakerphp/faker: ^1.24
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.1
- symfony/framework-bundle: ^8.1
Suggests
- fakerphp/faker: Required for the site:build --use-fixtures option, which generates demo articles.
Provides
None
Conflicts
Replaces
None
README
Staccato is a Symfony bundle that turns Markdown content into a static site: pages, a paginated homepage, an RSS feed, and a sitemap, all rendered with Twig.
This repository is the engine — a reusable cristianradulescu/staccato-bundle package. It ships default templates and styling so you can focus on writing content instead of building a layout; it is meant to be required into a Symfony application (or a dedicated Staccato project skeleton, once published) that supplies content.
Requirements
- PHP 8.5+
- A Symfony 8.1+ application with
bin/consoleto install the bundle into (e.g. one created fromsymfony/skeleton)
symfony/twig-bundle is installed as a dependency of the bundle: it renders the default templates and makes them overridable (see "Customizing templates" below).
Quick start: a new blog
bin/create-blog.sh creates a Symfony application with Staccato installed, a sample article (plus a draft) and an About page, then builds it:
bin/create-blog.sh --title "My Blog" --base-url https://blog.example.com my-blog cd my-blog php -S localhost:8000 -t build
It checks for PHP 8.5+ and Composer first. Options: --bundle-path <path> installs the bundle from a local checkout instead of Packagist, and --no-build skips the initial build. Run it with --help to see all options.
Installation (into a consuming Symfony app)
composer require cristianradulescu/staccato-bundle
Register the bundles (if not done automatically by Symfony Flex):
// config/bundles.php return [ // ... Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Staccato\StaccatoBundle::class => ['all' => true], ];
Configure it under its own staccato key, e.g. config/packages/staccato.yaml:
staccato: title: My Site base_url: https://example.com description: A simple static site generated with Staccato. articles_per_page: 10 footer_text: Built with Staccato
The consuming application only needs to provide:
content/— Markdown source files
Everything else — layout, homepage pagination, 404 page, RSS/sitemap markup, and a default stylesheet — is shipped by the bundle and works out of the box. Optionally, the consuming app can also provide:
assets/— static files copied as-is into the build output (images, extra scripts, etc.)
Then build the site with the bundle's console command:
php bin/console site:build
Configuration reference
All keys are optional and live under the staccato root key:
| Key | Type | Default |
|---|---|---|
title |
string | Staccato |
base_url |
absolute URL | http://localhost |
description |
string | Static site generated with Staccato. |
language |
language tag (e.g. en, pt-BR) |
en |
articles_per_page |
positive integer | 10 |
footer_text |
string | Built with Staccato |
include_default_styles |
boolean | true |
stylesheets |
array | [] |
highlighter_css |
string | null |
content_dir |
relative path | content |
build_dir |
relative path | build |
assets_dir |
relative path | assets |
content_dir, build_dir and assets_dir are relative to your project root and may not contain . or .. segments. Because the build directory is removed on every clean build, build_dir may not equal, contain, or sit inside content_dir or assets_dir. Whatever assets_dir is called, its files are published under build/assets/, so links like /assets/logo.png keep working.
The rest of this README uses the default names (content/, build/, assets/).
Customizing templates
Staccato ships default templates under the @Staccato Twig namespace: base.html.twig, page.html.twig, home.html.twig, 404.html.twig, rss.xml.twig, sitemap.xml.twig.
To override any single one of them, place a file at the matching path in your own app — Symfony's standard bundle template override convention:
templates/bundles/StaccatoBundle/page.html.twig
This file takes priority over the bundle's own page.html.twig, while every other default template keeps working unchanged. Override base.html.twig to change the overall layout, or override individual page templates to change just their content structure.
Helpers available in every template:
staccato_path(path)— a site-relative link or asset path ({{ staccato_path(article.urlPath) }},{{ staccato_path('/assets/logo.png') }}). Use it for every internalhref/srcso links keep working when the site is served from a sub-path — see "Hosting under a sub-path" below.staccato_url(path)— the absolute URL underbase_url({{ staccato_url(page.urlPath) }}), for canonical links, feeds and social metadata.staccato— the site configuration ({{ staccato.title }},{{ staccato.description }},{{ staccato.language }},{{ staccato.baseUrl }}, ...).
The default base.html.twig renders <html lang> from language, a <meta name="description">, an RSS <link rel="alternate">, and exposes two blocks for page-specific head metadata: description (defaults to the site description; pages use their excerpt) and canonical (pages and homepage listings render an absolute <link rel="canonical">).
Hosting under a sub-path
If the site is served from a sub-path rather than a domain root — e.g. a GitHub Pages project site at https://user.github.io/my-repo/ — include that path in base_url:
staccato: base_url: https://user.github.io/my-repo
The default templates prefix every internal link and stylesheet with it (/my-repo/about/, /my-repo/assets/staccato/staccato.css), and RSS/sitemap URLs include it as part of base_url. The build/ layout itself does not change: deploy its contents to the sub-path.
Two things to keep in mind:
- Links written by hand inside Markdown content are not rewritten. Use relative links (
../other-article/) or include the sub-path yourself. - With a sub-path
base_url, previewingbuild/from a local web server's root breaks links. Serve it under the same sub-path, or set the sub-pathbase_urlonly for the environment you deploy (e.g. inconfig/packages/prod/staccato.yaml).
Styling and themes
The bundle ships a small, dependency-free default theme (no Node/Tailwind build step). On every build it is copied to build/assets/staccato/staccato.css and linked from the default base.html.twig, so a new site is styled out of the box.
Three options control the stylesheets, independently of each other:
include_default_styles(defaulttrue) — copy and link the bundledstaccato.css.stylesheets— your own CSS files (paths absolute or relative to your project root). Each is copied tobuild/assets/staccato/<file name>and linked afterstaccato.cssand the highlighter stylesheet, so it can override them. File names must be unique and must not bestaccato.cssorhighlighter.css.highlighter_css— a syntax-highlighting theme for code blocks (path absolute or relative to your project root, e.g. one of the themes shipped invendor/tempest/highlight/src/Themes/Css/). It is copied tobuild/assets/staccato/highlighter.cssand linked; when unset, nothing is copied or linked.
Tweak the default theme by adding your own stylesheet on top of it:
staccato: stylesheets: ['themes/tweaks.css']
Replace the default theme with your own, with no template override needed:
staccato: include_default_styles: false stylesheets: ['themes/my-theme.css'] highlighter_css: 'vendor/tempest/highlight/src/Themes/Css/github-light.css'
For full control over the markup, override base.html.twig as described in "Customizing templates" above.
Writing content
Supported Markdown front matter keys:
title(string)slug(string)date(string; parseable date)excerpt(string)draft(boolean)
Notes:
- Missing
title/slugare derived from the source path:content/about.md->/about/,content/docs/setup.md->/docs/setup/. Files incontent/articles/drop thearticles/prefix (content/articles/hello.md->/hello/). - To give a directory its own page, add a sibling file:
content/docs.md->/docs/, next tocontent/docs/setup.md->/docs/setup/. index.mdfiles are not supported and fail the build with a hint on how to rename them. The homepage/is always generated fromcontent/articles/, so aslugresolving to/fails the build too.- Homepage listing only includes files from
content/articles/.
Markdown is rendered with tempest/markdown. Fenced code blocks with a language (```php) are syntax-highlighted — pick a theme with highlighter_css — and headings get id attributes, so #anchor links work. When excerpt is missing, it is derived from the first ~220 characters of the page's text.
site:build command
Options:
--clean(default) cleansbuild/before generation;--no-cleankeeps existing output.--use-fixturesgenerates 35 Faker-based demo articles instead of readingcontent/. It needs the optionalfakerphp/fakerpackage:composer require --dev fakerphp/faker.
Environment behavior:
- In
dev, drafts are included. - In
prod, drafts are excluded.
php bin/console site:build --env=prod
Output layout
Directory-style URLs are used:
/->build/index.html/about/->build/about/index.html/docs/getting-started/->build/docs/getting-started/index.html
Assets are mirrored from assets/ to build/assets/. The default stylesheet (unless disabled) is copied to build/assets/staccato/staccato.css. Special generated files: build/404.html, build/rss.xml, build/sitemap.xml.
Homepage and pagination
- Homepage lists only files from
content/articles/, ordered bydate(newest first). - Pagination uses
articles_per_page(default10). - Generated listing routes:
/(page 1),/page/2/,/page/3/, ...
Developing this bundle
composer installcomposer cs:check/composer cs:fixcomposer stancomposer testcomposer qa(runs all of the above)
Tests boot a minimal Symfony test kernel (tests/Fixtures/TestKernel.php) registering FrameworkBundle + TwigBundle + StaccatoBundle to exercise the bundle's configuration and DI wiring and to render the default templates end-to-end, alongside plain unit tests for the content-loading and build services.