Feature: add injectable token/service that provides list of content w/frontmatter

This issue has been tracked since 2023-01-16.

Which scope/s are relevant/related to the feature request?

content

Information

Define an injectable token or service that provides a list of the files markdown files scanned from the src/content folder in an array that includes frontmatter.

Example structure below

import { inject, InjectionToken } from '@angular/core';

export interface Frontmatter {
  title: string;
  description: string;
  publishedDate: string;
  slug: string;
  published: boolean;
  meta: string[];
  [name: string]: any; // extra metadata
}

export interface AnalogContentMetadata {
  filename: string;
  content: string;
  frontmatter: Frontmatter;
}

export const CONTENT_FILES_TOKEN = new InjectionToken<AnalogContentMetadata[]>('@analogjs/content Content Files', {
  providedIn: 'root',
  factory() {
    const rawContentFiles = import.meta.glob('/src/content/**/*.md', {
      eager: true,  // maybe support lazy loading here
      as: 'raw',
    });

    const contentFiles = Object.keys(rawContentFiles)
      .map((contentFile) => {
        const metadata = frontmatter<Frontmatter>(rawContentFiles[contentFile]);

        return {
          filename: contentFile,
          content: metadata.body, 
          frontmatter: {
            ...metadata.attributes,
            title: metadata.attributes.title,
            description: metadata.attributes.description,
            slug: metadata.attributes.slug,
            publishedDate: metadata.attributes.publishedDate,
            published: metadata.attributes.published,
          }
        };
      });

    return contentFiles;
  },
});

export function injectContentMetadata() {
  return inject(CONTENT_FILES_TOKEN);
}

Usage

import { Component } from '@angular/core';

import { injectContentMetadata } from '@analogjs/content';

@Component({
  selector: 'blog-posts',
  standalone: true,
  imports: [NgFor],
  template: `
    <div *ngFor="let post of posts">
      {{ post.frontmatter.title }} <br/>
    </div>
  `,
})
export default class BlogComponent {
  posts = injectContentMetadata(); // AnalogContentMetadata[]
}

This could be used to list blog posts for example and could be filtered further based on needs, such as only listing published posts as an example.

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No
markostanimirovic wrote this answer on 2023-01-18

@brandonroberts

What do you think about having a more flexible result, so attributes type can be provided based on the project needs? This will also provide the ability to set proper typing for plain markdown content without frontmatter.

export interface ContentFile<
  Attributes extends Record<string, any> = Record<string, any>
> {
  filename: string;
  content: string;
  attributes: Attributes;
}

const CONTENT_FILES_TOKEN = new InjectionToken<ContentFile[]>(
  '@analogjs/content Content Files',
  {
    providedIn: 'root',
    factory() {
      const rawContentFiles = import.meta.glob('/src/content/**/*.md', {
        eager: true,
        as: 'raw',
      });

      return Object.keys(rawContentFiles).map((filename) => {
        const { body, attributes } = fm<Record<string, any>>(
          rawContentFiles[filename]
        );

        return {
          filename,
          content: body,
          attributes,
        };
      });
    },
  }
);

export function injectContentFiles<
  Attributes extends Record<string, any>
>(): ContentFile<T>[] {
  return inject(CONTENT_FILES_TOKEN) as ContentFile<Attributes>[];
}

// usage:

interface PostAttributes {
  title: string;
  coverSrc: string;
  published: boolean;
}

@Component({
  selector: 'app-blog',
  standalone: true,
  imports: [NgFor],
  template: `
    <article *ngFor="let post of publishedPosts">
      <h2>{{ post.attributes.title }}</h2>
      <img [src]="post.attributes.coverSrc" [alt]="post.attributes.title" />
    </article>
  `,
})
export default class BlogComponent {
  private readonly posts = injectContentFiles<PostAttributes>();
  readonly publishedPosts = this.posts.filter(
    (post) => post.attributes.published
  );
}

Changes:

  • Function injectContentMetadata is renamed to injectContentFiles.
  • Property frontmatter is renamed to attributes.
  • Interface AnalogContentMetadata is renamed to ContentFile and Frontmatter is removed, so attributes can be typed via generic.
brandonroberts wrote this answer on 2023-01-18

Definitely prefer the more flexible option with better types.

goetzrobin wrote this answer on 2023-01-18

@markostanimirovic @brandonroberts I can implement as laid out here: #222 (comment)
If you guys are not already working on this and want to assign this issue to me 👍

brandonroberts wrote this answer on 2023-01-18
More Details About Repo
Owner Name analogjs
Repo Name analog
Full Name analogjs/analog
Language TypeScript
Created Date 2022-07-06
Updated Date 2023-03-28
Star Count 885
Watcher Count 18
Fork Count 67
Issue Count 33

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date