Moving a TYPO3 project to Composer changes more than the way extensions are installed. It also changes how public extension assets such as CSS, JavaScript, images and fonts are exposed to the web.
If your project still contains hardcoded paths such as:
/typo3conf/ext/site_package/Resources/Public/...
you need to review them when migrating to a modern Composer-based TYPO3 installation.
In Composer mode, TYPO3 installs extensions outside the public web root. Their public resources are exposed through public/_assets/, while files such as PHP classes, configuration and templates remain protected from direct HTTP access.
TYPO3 v12 requires typo3/cms-composer-installers v5, making this structure part of the standard Composer setup.
The good news: you normally should not replace old URLs with hardcoded _assets URLs. Instead, use TYPO3's resource APIs, EXT: references, relative paths, or your frontend build system.
This guide shows exactly how to migrate.
What is _assets in TYPO3?
public/_assets/ contains symlinks to the Resources/Public/ directories of Composer-installed TYPO3 extensions.
For example:
packages/site_package/
└── Resources/
└── Public/
├── Css/
├── JavaScript/
├── Images/
└── Fonts/
Composer/TYPO3 exposes these public resources through:
public/_assets/<generated-hash>/
The hash helps avoid exposing the extension's name and Composer path. However, the exact hashing mechanism is an implementation detail and may change in future TYPO3 major versions.
Therefore: don't build your application around a hardcoded _assets/<hash>/... URL.
Why did TYPO3 change from typo3conf/ext to _assets?
The main reason is security and a cleaner Composer architecture.
In older installations, an extension could be directly accessible below:
public/typo3conf/ext/
That meant the web server could potentially expose files that were never intended to be public.
Modern Composer installations place extension packages outside the public web root, typically under vendor/ or a local packages/ directory. Only the extension's Resources/Public/ directory is exposed to the browser.
The basic architecture is now:
Composer extension
│
├── PHP
├── Configuration
├── Templates
└── Resources/Public
│
▼
public/_assets/
This creates a much better separation between application code and public assets.
typo3conf/ext vs Composer _assets
| Older approach | Composer-based TYPO3 |
| public/typo3conf/ext/ | vendor/ or packages/ |
| Extension code publicly located | Extension code outside web root |
| Assets referenced directly | Public assets exposed through _assets |
| Hardcoded extension paths common | TYPO3 resource APIs preferred |
| Higher risk of exposing internal files | Better public/private separation |
TYPO3's current directory structure documentation confirms that public/_assets/ contains symlinks to extension Resources/Public/ directories and that typo3conf/ext/ is no longer used in modern Composer projects.
Step 1: Find old typo3conf/ext references
Before changing code, search your complete project for:
typo3conf/ext/
Check:
- Fluid templates
- CSS
- SCSS
- JavaScript
- TypeScript
- PHP
- TypoScript
- TSconfig
- RTE configuration
- YAML
- JSON
- frontend build configuration
- custom scripts
TYPO3 itself recommends searching extension code for old typo3conf/ext/ references during the migration.
A simple project-wide search can reveal most migration candidates:
grep -R "typo3conf/ext/" .
Your goal is not to blindly replace every occurrence. First identify what kind of path it is.
Step 2: Keep public assets in Resources/Public
Public extension assets should live inside:
Resources/Public/
Typical structure:
Resources/Public/
├── Css/
├── JavaScript/
├── Images/
├── Fonts/
└── Icons/
TYPO3 defines Resources/Public specifically for files that should be delivered by the web server, such as CSS, JavaScript, images and fonts.
Private resources such as templates, configuration and localization files should remain private.
Migrating CSS asset paths
One of the most common migration problems is a hardcoded CSS URL.
Old approach
.CssClass {
background-image: url("/typo3conf/ext/site_package/Resources/Public/Images/TheImage.jpeg");
}
This path will not work correctly once the extension is installed through the modern Composer structure.
Best option: use relative paths
If the CSS and image belong to the same extension, use a relative URL:
.CssClass {
background-image: url("../Images/TheImage.jpeg");
}
This is the preferred approach for resources within the same extension. TYPO3's migration documentation specifically recommends relative links for this scenario.
Avoid this
background-image: url("/_assets/<hash>/Images/TheImage.jpeg");
Even though a hashed _assets URL may work, it couples your CSS to TYPO3's internal asset mapping.
What if CSS references an asset from another extension?
This is a more complicated case.
For example:
Extension A
└── Resources/Public/Css/style.css
Extension B
└── Resources/Public/Images/logo.svg
CSS cannot conveniently use TYPO3's EXT: syntax directly in the browser.
Better options include:
- Move shared assets into a central sitepackage.
- Use a frontend asset bundler such as Vite, webpack, Gulp or Encore.
- Generate the asset URL through Fluid, PHP or TypoScript and pass it into the frontend.
- Use a dedicated route or PSR-15 middleware for special dynamic asset requirements.
TYPO3 recommends centralizing shared assets or using a bundler where appropriate.
Migrating JavaScript asset paths
JavaScript often contains hidden asset dependencies.
For example, a map library may contain:
const icon = L.icon({
iconUrl: '/typo3conf/ext/site_package/Resources/Public/Icons/Map/marker.svg',
shadowUrl: '/typo3conf/ext/site_package/Resources/Public/Icons/Map/shadow.svg'
});
Instead of hardcoding the old path, let TYPO3 resolve the resource.
Recommended approach: pass URLs through HTML
In Fluid:
<div
id="map"
data-icon="{f:uri.resource(path: 'Icons/Map/marker.svg')}"
data-shadow="{f:uri.resource(path: 'Icons/Map/shadow.svg')}"
></div>
Then JavaScript can consume the generated URLs:
const mapElement = document.getElementById('map');
const icon = L.icon({
iconUrl: mapElement.dataset.icon,
shadowUrl: mapElement.dataset.shadow
});
This approach keeps TYPO3-specific path resolution in TYPO3 and keeps your JavaScript independent of _assets.
TYPO3's current Resources API recommends this kind of resource resolution rather than referencing _assets directly.
Migrating Fluid templates
Fluid templates should use TYPO3's resource handling instead of hardcoded typo3conf/ext URLs.
Old
<img src="/typo3conf/ext/site_package/Resources/Public/Images/logo.svg" alt="Logo">
Recommended
<img
src="{f:uri.resource(path: 'Images/logo.svg')}"
alt="Logo"
>
For an SVG sprite, for example:
<svg>
<use href="{f:uri.resource(path: 'Images/icons.svg')}#symbol"></use>
</svg>
You can also explicitly reference an extension using EXT: where appropriate:
{f:uri.resource(
path: 'EXT:site_package/Resources/Public/Images/logo.svg'
)}
TYPO3's Resources API identifies f:uri.resource as the standard Fluid mechanism for resolving public extension resources.
Best practice
Let Fluid/TYPO3 generate the public URL.
Do not try to calculate the _assets hash yourself.
Migrating PHP asset references
PHP can require either a filesystem path or a public web URL. These are two different things.
For a server-side file path, TYPO3 supports the EXT: syntax with its resource/path APIs:
$absoluteFilePath = GeneralUtility::getFileAbsFileName(
'EXT:site_package/Resources/Public/Images/logo.png'
);
For a public URL, use an API intended to resolve a web path rather than manually concatenating:
TYPO3_SITE_URL + /_assets/<hash>/...
TYPO3's current Resources API explicitly distinguishes server-side resource paths from public resource URLs.
Remember
Filesystem path ≠ Public URL
This distinction prevents many Composer migration bugs.
Migrating TypoScript asset references
TypoScript frequently contains references to CSS, JavaScript, fonts and images.
Use EXT: references instead of the old filesystem path.
For example:
page.meta {
og:image.cObject = TEXT
og:image.cObject {
typolink {
parameter.cObject = IMG_RESOURCE
parameter.cObject.file =
EXT:site_package/Resources/Public/Images/opengraph.png
returnLast = url
forceAbsoluteUrl = 1
}
}
}
The same principle applies when defining resources such as fonts or other public assets.
TYPO3's migration documentation recommends EXT:my_extension/Resources/Public/... notation where supported.
Migrating TSconfig
Old TSconfig references may look like:
<INCLUDE_TYPOSCRIPT: source="FILE:typo3conf/ext/site/Configuration/TSconfig/User/name.tsconfig">
Use the extension-aware syntax instead:
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:site_package/Configuration/TSconfig/User/name.tsconfig">
This removes the dependency on the old typo3conf/ext location.
Migrating RTE configuration
RTE configuration is another easy-to-miss location.
Old
editor:
config:
contentCss: '/typo3conf/ext/site_package/Resources/Public/Css/rte.css'
Recommended
editor:
config:
contentCss: 'EXT:site_package/Resources/Public/Css/rte.css'
When migrating a TYPO3 project, include RTE configuration in your project-wide search for typo3conf/ext.
What about static files?
Not every file belongs in Resources/Public.
If a file needs a predictable URL directly below the public web root, such as a project-level static resource, you may need a dedicated public directory or another TYPO3 mechanism.
For dynamic content, consider:
- PSR-15 middleware
- dynamic routes
- a dedicated public endpoint
- a project-level public directory
TYPO3's migration documentation specifically recommends dynamic routes, middleware or custom public directories for static links that cannot use the normal extension resource mechanism.
Also remember:
Do not put runtime-generated files into Resources/Public.
That directory is intended for static extension assets.
Don't manually manage public/_assets
This is one of the most important rules.
The _assets directory is generated and managed as part of the Composer/TYPO3 setup.
Do not:
- manually rename its directories
- manually replace its symlinks
- commit production
_assetscontents as normal files - build application logic around the generated hash
TYPO3 recommends that _assets remains reproducible and that its symlinks can be recreated through Composer. A composer dumpautoload can recreate missing public-asset links after resources are added.
How do I find the _assets URL?
If you genuinely need to inspect the generated asset mapping, TYPO3 Console provides:
vendor/bin/typo3 frontend:asseturl
This can help identify the public resource directory hash for installed extensions.
But use this primarily for debugging or special cases, not as your normal asset-reference strategy. TYPO3 recommends avoiding direct _assets references because the hashing mechanism is an implementation detail.
TYPO3 _assets migration checklist
Before finishing your migration, check:
- Search the complete project for
typo3conf/ext/. - Move public extension assets into
Resources/Public/. - Replace hardcoded Fluid URLs with
f:uri.resource. - Use
EXT:references in TypoScript and supported configuration. - Replace old CSS paths with relative URLs where assets belong to the same extension.
- Pass TYPO3-generated URLs to JavaScript through data attributes when necessary.
- Review PHP filesystem paths separately from public URLs.
- Check TSconfig and RTE configuration.
- Review frontend build pipelines.
- Avoid hardcoding
_assets/<hash>/. - Do not manually manage
_assetssymlinks. - Run
composer dumpautoloadwhen required. - Clear caches and test the frontend.
- Test CSS, JavaScript, images, fonts, SVGs and downloads.
- Test production deployment with a clean Composer install.
Common TYPO3 _assets migration mistakes
Replacing every old path with _assets
Don't simply turn:
/typo3conf/ext/site/Resources/Public/logo.svg
into:
/_assets/<hash>/logo.svg
Calculating the hash yourself
The hash is an implementation detail.
Using filesystem paths as browser URLs
A server-side path and a public URL are not interchangeable.
Forgetting frontend build tools
Webpack, Vite, Encore, Gulp and similar pipelines may still contain old output paths.
Editing _assets manually
Let Composer/TYPO3 create the symlinks.
Final takeaway
The move from typo3conf/ext to Composer-based extension handling is more than a path change. It is a shift toward a cleaner separation between private extension code and public web assets.
The most important rule is simple:
Don't migrate old paths by hardcoding _assets URLs. Migrate the way your application references assets.
Use:
Resources/Public/for static public extension assetsf:uri.resourcein FluidEXT:notation where supported- relative paths for assets within the same extension
- TYPO3 resource APIs for PHP
- data attributes for JavaScript when URLs need to be passed into frontend code
- a central asset strategy or bundler for shared assets
Once these patterns are in place, your TYPO3 project becomes more secure, Composer-friendly and easier to maintain across future TYPO3 upgrades.
FAQs
public/_assets/ contains symlinks to the Resources/Public directories of Composer-installed extensions. It allows public assets to be served without exposing the complete extension code.
Modern Composer-based TYPO3 installations place extensions outside the public web root. This improves security by exposing only resources intended for public delivery.
Generally, no. Use TYPO3's resource APIs, EXT: notation, relative URLs or your frontend build system instead.
For extensions, place static public resources in:
Resources/Public/
Typical subdirectories include Css, JavaScript, Images and Fonts.
Search your project for typo3conf/ext/, then migrate each reference according to its context: Fluid, CSS, JavaScript, PHP, TypoScript, TSconfig, RTE or frontend build configuration.
Wolfgang Weber
Brand & Communication LeadWolfgang Weber shapes TYPO3 with passion and expertise. As a TYPO3 enthusiast at T3Planet Shop, he has contributed to TYPO3 projects that make websites faster and more secure. Outside of TYPO3, you’ll probably find him exploring local cafés and enjoying football.
More From Author