On this page
Content Routes
Analog also supports using markdown content as routes, and rendering markdown content in components.
Setup
In the src/app/app.config.ts, add the provideContent() function, along with the withMarkdownRenderer() feature to the providers array when bootstrapping the application.
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
provideContent(withMarkdownRenderer()),
],
};
Next, enable the content package in the vite.config.ts
/// <reference types="vitest" />
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
plugins: [
analog({
// enable content/highlighter
content: {
highlighter: 'prism',
},
}),
],
}));
Defining Content Routes
Content routes include support for frontmatter, metatags, and syntax highlighting with PrismJS.
The example route below in src/app/pages/about.md defines an /about route.
---
title: About
meta:
- name: description
content: About Page Description
- property: og:title
content: About
---
## About Analog
Analog is a meta-framework for Angular.
[Back Home](./)
PrismJS Syntax Highlighting
Analog supports syntax highlighting with PrismJS. To enable syntax highlighting with PrismJS, add withPrismHighlighter() to the provideContent() function in app.config.ts.
+ import { withPrismHighlighter } from '@analogjs/content/prism-highlighter';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
- provideContent(withMarkdownRenderer()),
+ provideContent(withMarkdownRenderer(), withPrismHighlighter()),
],
};
Import a Prism theme into to your global stylesheet:
@import 'prismjs/themes/prism.css';
Using the diff Highlight Plugin
Analog supports highlighting diff changes with PrismJS.
Add the prism-diff language to the additionalLangs in the analog plugin:
export default defineConfig({
// ...
plugins: [
analog({
content: {
highlighter: 'prism',
prismOptions: {
additionalLangs: ['prism-diff'],
},
},
}),
],
});
Add the diff-highlight plugin import to the app.config.ts:
+ import { withShikiHighlighter } from '@analogjs/content/shiki-highlighter';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
- provideContent(withMarkdownRenderer()),
+ provideContent(withMarkdownRenderer(), withShikiHighlighter()),
],
};
To enable build-time syntax highlighting with shiki, configure the analog plugin in the vite.config.ts.
export default defineConfig({
// ...
plugins: [
analog({
content: {
highlighter: 'shiki',
},
}),
],
});
Configure Shiki Highlighter
Please check out Shiki Documentation for more information on configuring Shiki.
To configure Shiki, you can pass options to the shikiOptions object.
export default defineConfig({
// ...
plugins: [
analog({
content: {
highlighter: 'shiki',
shikiOptions: {
highlight: {
// alternate theme
theme: 'ayu-dark',
},
highlighter: {
// add more languages for Shiki itself
additionalLangs: ['diff'],
},
},
},
}),
],
});
For Mermaid-heavy content, keep the existing loadMermaid runtime path and skip Mermaid grammar loading in Shiki to avoid unnecessary server-side highlighting work in constrained CI environments:
export default defineConfig({
plugins: [
analog({
content: {
highlighter: 'shiki',
shikiOptions: {
highlighter: {
additionalLangs: ['mermaid'],
skipLangs: ['mermaid'],
},
},
},
}),
],
});
With skipLangs: ['mermaid'], Analog keeps Mermaid blocks on the existing
path forloadMermaid, while Shiki skips loading and tokenizing the Mermaid grammar.By default,
shikiOptionshas the following options.{ "container": "%s", "highlight": { "theme": "github-dark" } "highlighter": { "langs": [ "json", "ts", "tsx", "js", "jsx", "html", "css", "angular-html", "angular-ts", ], "themes": ["github-dark", "github-light"] } }Defining Content Files
For more flexibility, markdown content files can be provided in the
src/contentfolder. Here you can list markdown files such as blog posts.--- title: My First Post slug: 2022-12-27-my-first-post description: My First Post Description coverImage: https://images.unsplash.com/photo-1493612276216-ee3925520721?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=464&q=80 --- Hello WorldUsing the Content Files List
To get a list using the list of content files in the
src/contentfolder, use theinjectContentFilesfunction from the(filterFn?: InjectContentFilesFilterFunction ) @analogjs/contentpackage in your component. To narrow the files, you can use thefilterFnpredicate function as an argument. You can use theInjectContentFilesFilterFunctiontype to set up your predicate.
injectContentFilesreturns metadata only:filename,slug, andattributes. Thecontentbody is not loaded; reading it from the returned items yieldsundefined. UseinjectContentto load and render an individual file's body.export interface PostAttributes { title: string; slug: string; description: string; coverImage: string; } @Component({ imports: [RouterOutlet, RouterLink], template: ` <ul> @for (post of posts; track post.slug) { <li> <a [routerLink]="['/blog', 'posts', post.slug]"> {{ post.attributes.title }} </a> </li> } @empty { <li>No posts yet.</li> } </ul> `, }) export default class BlogComponent { readonly posts = injectContentFiles<PostAttributes>((contentFile) => contentFile.filename.includes('/src/content/blog/'), ); }Using the Analog Markdown Component
Analog provides a
MarkdownComponentandinjectContent()function for rendering markdown content with frontmatter.The
injectContent()function uses theslugroute parameter by default to get the content file from thesrc/contentfolder.// /src/app/pages/blog/posts.[slug].page.ts export interface PostAttributes { title: string; slug: string; description: string; coverImage: string; } @Component({ imports: [MarkdownComponent, AsyncPipe], template: ` @if (post$ | async; as post) { <h1>{{ post.attributes.title }}</h1> <analog-markdown [content]="post.content"></analog-markdown> } `, }) export default class BlogPostComponent { readonly post$ = injectContent<PostAttributes>(); }Using A Resolver For Metatags
In your route configuration, you can use the
RouteMetaobject to resolve meta tags for a route. This is done by assigning thepostMetaResolverfunction to themetaproperty.Below is an example of using a
postMetaResolverfunction that fetches the meta tags for a post. This function returns an array of meta tags.export const postMetaResolver: ResolveFn<MetaTag[]> = (route) => { const postAttributes = injectActivePostAttributes(route); return [ { name: 'description', content: postAttributes.description, }, { name: 'author', content: 'Analog Team', }, { property: 'og:title', content: postAttributes.title, }, { property: 'og:description', content: postAttributes.description, }, { property: 'og:image', content: postAttributes.coverImage, }, ]; };The meta tags can be done asynchronously also. Assign the
postMetaResolverfunction to themetaproperty.export const routeMeta: RouteMeta = { title: postTitleResolver, meta: postMetaResolver, };The resolved meta tags can also be accessed in the component using the
ActivatedRouteservice.export default class BlogPostComponent { readonly route = inject(ActivatedRoute); readonly metaTags$ = this.route.data.pipe(map(data => data['meta'])); // In the template <my-component [metaTags]="metaTags$ | async"></my-component> }Enabling support for Mermaid
Analog's markdown component supports Mermaid. To enable support by the
MarkdownComponentdefine a dynamic import forloadMermaidinwithMarkdownRenderer().withMarkdownRenderer({ loadMermaid: () => import('mermaid'), });After it is enabled, Mermaid blocks are transformed by mermaid into SVGs.
Example of mermaid graph:
graph TD A[Before] -->|Playing with AnalogJS| B(Now Yes !)Support for Content Subdirectories
Analog also supports subdirectories within your content folder.
The
injectContent()function can also be used with an object that contains the route parameter and subdirectory name.This can be useful if, for instance, you have blog posts, as well as a portfolio of project markdown files to be used on the site.
src/ └── app/ │ └── pages/ │ └── project.[slug].page.ts └── content/ ├── posts/ │ ├── my-first-post.md │ └── my-second-post.md └── projects/ ├── my-first-project.md └── my-second-project.md// /src/app/pages/project.[slug].page.ts export interface ProjectAttributes { title: string; slug: string; description: string; coverImage: string; } @Component({ imports: [MarkdownComponent, AsyncPipe], template: ` @if (project$ | async; as project) { <h1>{{ project.attributes.title }}</h1> <analog-markdown [content]="project.content"></analog-markdown> } `, }) export default class ProjectComponent { readonly project$ = injectContent<ProjectAttributes>({ param: 'slug', subdirectory: 'projects', }); }Hierarchical (Nested) Content
When content is organized into category subdirectories under a
subdirectory, pairinjectContent({ param, subdirectory })with a catch-all route. The catch-all parameter captures the full path under the subdirectory and resolves to the matching nested file.src/ └── app/ │ └── pages/ │ └── docs/ │ └── [...slug].page.ts └── content/ └── docs/ ├── getting-started/ │ ├── welcome.md │ └── first-upload.md └── assets/ └── upload.md// /src/app/pages/docs/[...slug].page.ts export interface DocAttributes { title: string; } @Component({ imports: [MarkdownComponent, AsyncPipe], template: ` @if (doc$ | async; as doc) { <h1>{{ doc.attributes.title }}</h1> <analog-markdown [content]="doc.content"></analog-markdown> } `, }) export default class DocComponent { readonly doc$ = injectContent<DocAttributes>({ param: 'slug', subdirectory: 'docs', }); }A request for
/docs/getting-started/welcomeresolves tosrc/content/docs/getting-started/welcome.md. Frontmatterslugis optional in this layout; when omitted, the file is keyed by its on-disk path. If a file does specify aslugcontaining path separators, that slug is interpreted relative to its top-level subdirectory (e.g.slug: getting-started/welcomeon a file undersrc/content/docs/resolves tosrc/content/docs/getting-started/welcome).Loading Custom Content
By default, Analog uses the route params to build the filename for retrieving a content file from the
src/contentfolder. Analog also supports using a custom filename for retrieving content from thesrc/contentfolder. This can be useful if, for instance, you have a custom markdown file that you want to load on a page.The
injectContent()function can be used by passing an object that contains thecustomFilenameproperty.readonly post$ = injectContent<ProjectAttributes>({ customFilename: 'path/to/custom/file', });