Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to read and parse blog posts from .md files instead of api endpoint #118

Open
vmsantos opened this issue Feb 5, 2025 · 9 comments
Assignees
Labels
enhancement New feature or request good first issue Good for newcomers needs more info Further information is requested

Comments

@vmsantos
Copy link

vmsantos commented Feb 5, 2025

Is it possible? I've been trying to do it but something always breaks

@markteekman
Copy link
Member

Hey @vmsantos! Hmmm yes this should most definitely be possible. My plan was to eventually add another menu item "Portfolio" which would use exactly that and give users both options out of the box with this theme.

We'll have to look into it. We're currently in a big overhaul of the Accessible Astro ecosystem but we'll try to get back to you soon 🙂

@markteekman markteekman added the needs more info Further information is requested label Feb 5, 2025
@markteekman
Copy link
Member

@vmsantos Could you possibly share your repo so we can take a look at how it's setup? Or could you else provide some code examples here of what you tried.

@markteekman
Copy link
Member

This might be related since we updated to Astro 5.0: https://docs.astro.build/en/guides/upgrade-to/v5/#legacy-v20-content-collections-api

@vmsantos
Copy link
Author

vmsantos commented Feb 5, 2025

@vmsantos Could you possibly share your repo so we can take a look at how it's setup? Or could you else provide some code examples here of what you tried.

My code is a mess, but the workaround I found was to create an api in astro and deploy it to netlify instead of github pages, because of SSR (output: 'server'):

posts.json.ts

import fs from 'fs/promises';
import path from 'path';
// Função para extrair o título do conteúdo Markdown
function extrairTitulo(markdown) {
  // Divide o conteúdo em linhas e filtra as linhas vazias
  const lines = markdown.split('\n').map(line => line.trim()).filter(Boolean);
  
  if (lines.length > 0) {
    // Se a primeira linha começar com "#" (cabeçalho Markdown), remova-o
    let titulo = lines[0];
    if (titulo.startsWith('#')) {
      titulo = titulo.replace(/^#+\s*/, '');
    }
    return titulo;
  }
  return 'Sem Título';
}

export async function GET() {
  try {
    const postsDir = path.resolve('src/posts');
    const files = await fs.readdir(postsDir);

    // Mapeia os arquivos para extrair dados
    const posts = await Promise.all(
      files.map(async (file) => {
        // Gera o slug baseado no nome do arquivo (removendo a extensão .md)
        const slug = file.replace(/\.md$/, '');
        const filePath = path.join(postsDir, file);
        const content = await fs.readFile(filePath, 'utf-8');

        // Extrai o título a partir da primeira linha do conteúdo
        const title = extrairTitulo(content);

        // Retorna o título, slug, userId fixo e o conteúdo completo
        return {
          slug,
          title,
          userId: 1,  // ou outro valor conforme sua necessidade
          body: content
        };
      })
    );

    return new Response(JSON.stringify(posts), {
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error) {
    console.error(error);
    return new Response(JSON.stringify({ error: 'Erro ao ler posts' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

`

[...page].astro

        ---
        import DefaultLayout from '../../layouts/DefaultLayout.astro'
        import { Card, Pagination } from 'accessible-astro-components'
    
    interface Page {
      data: Array<{
        title: string
        body: string
        userId: number
      }>
      start: number
      end: number
      total: number
      currentPage: number
      size: number
      url: {
        prev: string | null
        next: string | null
      }
    }
    
    export const prerender = false;
    
    // Initialize with default values
    let page: Page = {
      data: [],
      start: 0,
      end: 0,
      total: 0,
      currentPage: 1,
      size: 6,
      url: {
        prev: null,
        next: null
      }
    };
    
    if (Astro.request.method === 'GET') {
      const baseURL = 'https://xxxxxxxxx.netlify.app';
      let data;
      const params = new URL(Astro.request.url).pathname.split('/').filter(Boolean);
      const currentPage = parseInt(params[1] || '1');
      const pageSize = 6;
    
      try {
        const response = await fetch(`${baseURL}/api/posts.json`);
        data = await response.json();
      } catch (error) {
        console.error('Failed to fetch posts from API, using mock data:', error);
        data = [
          { title: 'First Post', body: 'This is the first post.', userId: 1 },
          { title: 'Second Post', body: 'This is the second post.', userId: 2 },
        ];
      }
    
      const totalPages = Math.ceil(data.length / pageSize);
      const start = (currentPage - 1) * pageSize;
      const end = Math.min(start + pageSize, data.length);
    
      page = {
        data: data.slice(start, end),
        start,
        end: end - 1,
        total: data.length,
        currentPage,
        size: pageSize,
        url: {
          prev: currentPage > 1 ? `/blog/${currentPage - 1}` : null,
          next: currentPage < totalPages ? `/blog/${currentPage + 1}` : null
        }
      };
    }
    ---
    
    <DefaultLayout
      title="Blog"
      description="An example of a blog with dynamic content fetched from JSONPlaceholder using the title, body and userId."
    >
      <section class="my-12">
        <div class="space-content container">
          <h1>Blog</h1>
          <p class="text-2xl">
            An example of a blog with dynamic content fetched from <a href="/api/posts.json">JSONPlaceholder</a> using the title,
            body and userId. The Accessible Astro Card Component is used here to display al the posts.
          </p>
        </div>
      </section>
      <section class="my-12">
        <div class="container">
          <p class="text-sm"><em>Post {page.start + 1} through {page.end + 1} of {page.total} total posts</em></p>
          <ul class="my-3">
            {
              page.data.map((post) => {
                // Jump to the first line of the body where there's no # or ##
                const bodyLines = post.body.split('\n').filter(line => !line.startsWith('#')).map(line => line.trim());
                const truncatedBody = bodyLines.join(' ').substring(0, 100) + '...'; // Truncate to 100 characters
    
                return (
                  <li>
                    <Card
                      url={'/blog/' + post.title.replaceAll(' ', '-').toLowerCase()}
                      title={post.title}
                      footer={''}
                    >
                      {truncatedBody}
                    </Card>
                  </li>
                );
              })
            }
          </ul>
          <div class="mt-12 grid place-content-center">
            <Pagination
              firstPage={page.url.prev ? '/blog' : null}
              previousPage={page.url.prev ? page.url.prev : null}
              nextPage={page.url.next ? page.url.next : null}
              lastPage={page.url.next ? `/blog/${Math.ceil(page.total / page.size)}` : null}
              currentPage={page.currentPage}
              totalPages={Math.ceil(page.total / page.size)}
            />
          </div>
        </div>
      </section>
    </DefaultLayout>
    
    <style lang="scss">
      ul {
        display: grid;
        grid-template-columns: 1fr;
        grid-gap: 4rem;
    
        @media (min-width: 550px) {
          grid-template-columns: repeat(2, 1fr);
          grid-gap: 2rem;
        }
    
        @media (min-width: 950px) {
          grid-template-columns: repeat(3, 1fr);
        }
      }
    </style>

[post].astro

  ---
  import DefaultLayout from '../../layouts/DefaultLayout.astro'
  import { Breadcrumbs, BreadcrumbsItem } from 'accessible-astro-components'
  import { marked } from 'marked'
  
  interface Post {
    title: string;
    body: string;
    userId: number;
  }
  
  export const prerender = false;
  
  // Initialize with default values
  let post: Post = {
    title: 'Post not found',
    body: 'The requested post could not be found.',
    userId: 0
  };
  
  if (Astro.request.method === 'GET') {
    const baseURL = 'https://xxxxx.netlify.app';
    const requestedPost = Astro.params.post;
    
    try {
      const response = await fetch(`${baseURL}/api/posts.json`);
      const data = await response.json();
      const foundPost = data.find((p) => p.title.replaceAll(' ', '-').toLowerCase() === requestedPost);
      
      if (foundPost) {
        post = foundPost;
      }
    } catch (error) {
      console.error('Failed to fetch post:', error);
    }
  }
  
  const renderedBody = marked(post.body);
  ---
  
  <DefaultLayout title={post.title} description={post.body} url={post.title}>
    <div class="container">
      <div class="mt-12">
        <Breadcrumbs>
          <BreadcrumbsItem href="/" label="Home" />
          <BreadcrumbsItem href="/blog" label="Blog" />
          <BreadcrumbsItem currentPage={true} label={post.title} />
        </Breadcrumbs>
      </div>
    </div>
  <!--   <section class="my-12">
      <div class="container">
        <h1>{post.title}</h1><br />
        <p>By userId: {post.userId}</p>
      </div>
    </section> -->
    <section class="my-12">
      <div class="container markdown-body" set:html={renderedBody}></div>
    </section>
  </DefaultLayout>
  
  <style lang="scss">
    ul {
      display: grid;
      grid-template-columns: 1fr;
      grid-gap: 4rem;
  
      @media (min-width: 550px) {
        grid-template-columns: repeat(2, 1fr);
        grid-gap: 2rem;
      }
  
      @media (min-width: 950px) {
        grid-template-columns: repeat(3, 1fr);
      }
    }
  
    .markdown-body {
      font-size: 1.25rem;
      line-height: 1.75rem;
      max-width: 100%;
      overflow: auto;
    }
  </style>

@vmsantos
Copy link
Author

vmsantos commented Feb 5, 2025

I'm also accepting suggestions on the api/backend part. I didn't want a real backend, but perhaps it is the way to go. I don't know which would be the best language or framework for that (or where to host).

@markteekman
Copy link
Member

@vmsantos thanks for the info! We only work on these projects in our spare time and are working on a big update currently so please hang tight 😄

@vmsantos
Copy link
Author

@vmsantos thanks for the info! We only work on these projects in our spare time and are working on a big update currently so please hang tight 😄

No problem! Thanks

@markteekman
Copy link
Member

@vmsantos we are done shipping a couple of big updates, we'll look into this soon. We are going to build that portfolio page example first and then see how your story fits in 😄

@peterpadberg
Copy link
Member

@vmsantos We just added a portfolio example using the Content collections. You can take a look at the pages and the markdown. Does this accomplish what you are trying to do?

@markteekman markteekman added enhancement New feature or request good first issue Good for newcomers labels Feb 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers needs more info Further information is requested
Projects
Development

No branches or pull requests

3 participants