Skip to content

Get Started Astro Tutorial — 9. Displaying a post

00:00 / 00:00

9. Displaying a post

To build the page that displays a single post, we’ll reuse much of what we’ve already done for the post list page. You might wonder: why reuse code if we’re only loading one post here? The reason lies in how Astro works.

At build time, Astro reads all posts and generates a separate static page for each post—so the process is quite similar to building the post list page.

To generate one page per post, we’ll use a dynamic route "[slug]". That slug will be the post title, so if we have a post titled "my-first-post", the generated page will be "my-first-post.html".

Let’s get started. Under the "posts" folder, create a file named "[slug].astro".

First, we configure Markdown rendering and syntax highlighting:

./src/pages/posts/[slug].astro

---
import { marked } from 'marked';
import Layout from '../../layouts/Layout.astro';
import { getPosts } from './post.api';
import hljs from 'highlight.js';

const renderer = new marked.Renderer();
renderer.code = ({ text, lang }) => {
  const validLang = hljs.getLanguage(lang || 'plaintext') ? lang || 'plaintext' : 'plaintext';
  const highlighted = hljs.highlight(text, { language: validLang }).value;
  return `<pre><code class="hljs ${validLang}">${highlighted}</code></pre>`;
};

marked.setOptions({ renderer });
---

Then we use "getStaticPaths" to generate a slug for each post. We pass the correct post via props and generate each page:

Place this just before the frontmatter:

export async function getStaticPaths() {
  const posts = await getPosts();
  return posts.map((post) => ({
    params: {
      slug: post.title
        .toLowerCase()
        .trim()
        .replace(/[^a-z0-9\s-]/g, "")
        .replace(/\s+/g, "-"),
    },
    props: { post },
  }));
}

const { post } = Astro.props;

const postContent = marked(post.content || "");
const postDate = new Date(post.date).toLocaleDateString("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
});

const postImage = `url(${post.image.url})`;

Let’s render a simple layout:

<Layout>
  <article>
    <h1>{post.title}</h1>
    <p><strong>Date:</strong> {post.date}</p>
    <p><strong>Author:</strong> {post.author}</p>
    <img src={post.image.url} alt={post.title} width="800" />
    <p><strong>Summary:</strong> {post.summary}</p>
    <div>
      <strong>Content:</strong>
      <p>{post.content}</p>
    </div>
  </article>
</Layout>

Now let’s add some nice styles:

<style define:vars={{ postImage }}>
  .background::before {
    content: '';
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    width: 100%;
    height: 360px;
    background-image: var(--postImage);
    background-size: cover;
    background-repeat: no-repeat;
    background-position: top center;
  }

  .article {
    max-width: 800px;
    margin-right: auto;
    margin-left: auto;
    margin-top: var(--space-2xl);
    padding: var(--space-lg);
    background-color: var(--pure-white);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
  }

  .article__header {
    text-align: center;
  }

  .article__title {
    font-size: var(--fs-3xl);
    font-family: var(--ff-title);
  }

  .article__content {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);

    h2 {
      margin-top: var(--space-sm);
    }

    ul {
      padding-left: var(--space-md);
    }

    img {
      width: 100%;
      object-fit: cover;
      aspect-ratio: 21/9;
      border-radius: var(--border-radius);
    }

    code {
      font-size: var(--fs-xs);
      border-radius: var(--border-radius);
      padding: var(--space-xs);
    }
  }
</style>

And update the markup to use the styles:

<Layout>
  <div class="container background">
    <article class="article">
      <header class="article__header">
        <p>
          <time datetime="2025-03-05">{postDate}</time>
        </p>
        <h1 class="article__title">{post.title}</h1>
      </header>
      <div set:html={postContent} class="article__content" />
      <footer>
        <p>Written by <strong>{post.author}</strong></p>
      </footer>
    </article>
  </div>
</Layout>

And that’s it! ✅