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: update injectContent function to return content object instead of string #228

Closed brandonroberts closed 1 year ago

brandonroberts commented 1 year ago

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

content

Information

Currently if you want to display a single post, you can use the injectContent() function to get the rendered markdown as a string. If you want to display the metadata from the content within the component, you basically have to reimplement injectContent() to return the content with metadata.

import { AsyncPipe, DatePipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { MarkdownComponent } from '@analogjs/content';
import { ContentFile, injectContentFiles } from '@analogjs/content';
import { injectActivatedRoute } from '@analogjs/router';

export function injectPost(slug: string) {
  const route = injectActivatedRoute();
  return injectContentFiles<Post>().find(
    (posts) =>
      posts.filename === `/src/content/${route.snapshot.paramMap.get(slug)}.md`
  );
}

@Component({
  selector: 'post',
  standalone: true,
  imports: [MarkdownComponent, AsyncPipe, NgIf, DatePipe, ReadingTimePipe],
  template: `
    <div class="flex flex-grow justify-center min-h-screen" *ngIf="post">
      <article class="w-screen max-w-4xl p-8">
        <h2 class="text-gray-600 text-2xl">{{ post.attributes.title }}</h2>

        <span class="font-light text-sm">
          {{ post.attributes.publishedDate | date : 'MMMM dd, yyyy' }} -
          {{ post.content | readingtime }} min read
        </span>

        <analog-markdown [content]="post.content"></analog-markdown>
      </article>
    </div>
  `,
})
export default class BlogPostComponent {
  post = injectPost('slug');
}

The proposed change would return the content with metadata instead of the string itself

import { AsyncPipe, DatePipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { MarkdownComponent, injectContent } from '@analogjs/content';

export interface Post {
  title: string;
  slug: string;
  published: boolean;
  publishedDate: string;
}

@Component({
  selector: 'post',
  standalone: true,
  imports: [MarkdownComponent, AsyncPipe, NgIf, DatePipe, ReadingTimePipe],
  template: `
    <div class="flex flex-grow justify-center min-h-screen" *ngIf="post">
      <article class="w-screen max-w-4xl p-8">
        <h2 class="text-gray-600 text-2xl">{{ post.attributes.title }}</h2>

        <span class="font-light text-sm">
          {{ post.attributes.publishedDate | date : 'MMMM dd, yyyy' }} -
          {{ post.content | readingtime }} min read
        </span>

        <analog-markdown [content]="post.content"></analog-markdown>
      </article>
    </div>
  `,
})
export default class BlogPostComponent {
  post = injectContent<Post>();
}

Describe any alternatives/workarounds you're currently using

No response

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

goetzrobin commented 1 year ago

@brandonroberts just ran into the same issue working on my personal website! I can work on this if you want

brandonroberts commented 1 year ago

That would be great 👍

goetzrobin commented 1 year ago

Awesome! What do you think we should do when no file is found for the current route? Currently the customFallback param is returned. When returning an object we have other options:

  1. Returning an optional ContentFile inside the Observable & deprecating the fallback param
    export function injectContent<
    Attributes extends Record<string, any> = Record<string, any>
    >(
    param = 'slug',
    fallback = 'No Content Found'
    ): Observable<ContentFile<Attributes> | undefined> {
    const route = inject(ActivatedRoute);
    const contentFiles = injectContentFiles<Attributes>();
    return route.paramMap.pipe(
    map((params) => params.get(param)),
    map((slug) => {
      return contentFiles.find(
        (file) => file.filename === `/src/content/${slug}.md`
      );
    })
    );
    }
  2. Building a 'not found' ContentFile that returns the customFallback as its content
    export function injectContent<
    Attributes extends Record<string, any> = Record<string, any>
    >(
    param = 'slug',
    fallback = 'No Content Found'
    ): Observable<ContentFile<Attributes | Record<string, never>>> {
    const route = inject(ActivatedRoute);
    const contentFiles = injectContentFiles<Attributes | Record<string, never>>();
    return route.paramMap.pipe(
    map((params) => params.get(param)),
    map((slug) => {
      return (
        contentFiles.find(
          (file) => file.filename === `/src/content/${slug}.md`
        ) || {
          attributes: {},
          filename: '',
          content: fallback,
        }
      );
    })
    );
    }

Let me know what you think 👍

brandonroberts commented 1 year ago

Option 2 is good to me