analogjs / analog

The fullstack meta-framework for Angular. Powered by Vite and Nitro
https://analogjs.org
MIT License
2.5k stars 240 forks source link

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

Closed brandonroberts closed 1 year ago

brandonroberts commented 1 year ago

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

markostanimirovic commented 1 year ago

@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:

brandonroberts commented 1 year ago

Definitely prefer the more flexible option with better types.

goetzrobin commented 1 year ago

@markostanimirovic @brandonroberts I can implement as laid out here: https://github.com/analogjs/analog/issues/222#issuecomment-1384677257 If you guys are not already working on this and want to assign this issue to me 👍

brandonroberts commented 1 year ago

@goetzrobin 👍