mirror of
https://github.com/prdlk/website.git
synced 2026-08-02 17:31:41 +00:00
feat(docs): add astro routes for experience, projects, speaking, and writing pages
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
import { render } from "astro:content";
|
||||||
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import PostPreview from "@/components/blog/PostPreview.astro";
|
||||||
|
import { getAllExperience, getContentForExperience } from "@/data/experience";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const experiences = await getAllExperience();
|
||||||
|
return Promise.all(
|
||||||
|
experiences.map(async (exp) => ({
|
||||||
|
params: { slug: exp.id },
|
||||||
|
props: { exp, content: await getContentForExperience(exp.id) },
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { exp, content } = Astro.props as Props;
|
||||||
|
const { Content } = await render(exp);
|
||||||
|
const { startDate, endDate, organization, role, location, url } = exp.data;
|
||||||
|
const range = `${startDate.getFullYear()} – ${endDate ? endDate.getFullYear() : "Present"}`;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: exp.data.description ?? `${role ?? ""} at ${organization}`.trim(),
|
||||||
|
title: exp.data.title,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<nav class="mb-8" aria-label="Breadcrumbs">
|
||||||
|
<ul class="flex items-center">
|
||||||
|
<li class="flex items-center">
|
||||||
|
<a class="text-accent" href="/experience/">Experience</a>
|
||||||
|
<Icon aria-hidden="true" name="mdi:chevron-right" class="mx-1.5" />
|
||||||
|
</li>
|
||||||
|
<li aria-current="page">{exp.data.title}</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h1 class="title">{role ? `${role}, ${organization}` : organization}</h1>
|
||||||
|
<p class="text-muted mt-1">
|
||||||
|
{range}{location && ` · ${location}`}
|
||||||
|
{
|
||||||
|
url && (
|
||||||
|
<>
|
||||||
|
{" · "}
|
||||||
|
<a class="cactus-link" href={url} rel="noopener noreferrer" target="_blank">
|
||||||
|
Website →
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="prose prose-sm prose-cactus mt-6 mb-12 max-w-none">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
content.projects.length > 0 && (
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="title mb-4 text-xl">Projects</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
{content.projects.map((p) => (
|
||||||
|
<li>
|
||||||
|
<a class="cactus-link" href={`/projects/${p.id}/`}>
|
||||||
|
{p.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
content.writing.length > 0 && (
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="title mb-4 text-xl">Writing</h2>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
{content.writing.map((p) => (
|
||||||
|
<li class="grid gap-1 sm:grid-cols-[auto_1fr]">
|
||||||
|
<PostPreview as="h3" post={p} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
content.speaking.length > 0 && (
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="title mb-4 text-xl">Speaking</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
{content.speaking.map((t) => (
|
||||||
|
<li>
|
||||||
|
<a class="cactus-link" href={`/speaking/${t.id}/`}>
|
||||||
|
{t.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { getAllExperience } from "@/data/experience";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
|
||||||
|
const experiences = await getAllExperience();
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: "Roles and organizations I've worked with",
|
||||||
|
title: "Experience",
|
||||||
|
};
|
||||||
|
|
||||||
|
const range = (start: Date, end?: Date) =>
|
||||||
|
`${start.getFullYear()} – ${end ? end.getFullYear() : "Present"}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<h1 class="title mb-12">Experience</h1>
|
||||||
|
<ul class="space-y-10">
|
||||||
|
{
|
||||||
|
experiences.map((exp) => (
|
||||||
|
<li>
|
||||||
|
<div class="flex flex-wrap items-baseline justify-between gap-x-4">
|
||||||
|
<h2 class="title text-lg">
|
||||||
|
<a class="cactus-link" href={`/experience/${exp.id}/`}>
|
||||||
|
{exp.data.role ? `${exp.data.role}, ${exp.data.organization}` : exp.data.organization}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<span class="text-muted text-sm">{range(exp.data.startDate, exp.data.endDate)}</span>
|
||||||
|
</div>
|
||||||
|
{exp.data.location && <p class="text-muted text-sm">{exp.data.location}</p>}
|
||||||
|
{exp.data.description && <p class="mt-2">{exp.data.description}</p>}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import type { GetStaticPaths, Page } from "astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import Pagination from "@/components/Paginator.astro";
|
||||||
|
import Project from "@/components/project/Project.astro";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { collectionDateSort } from "@/utils/date";
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
const MAX_PROJECTS_PER_PAGE = 10;
|
||||||
|
const allProjects = await getCollection("projects");
|
||||||
|
return paginate(allProjects.sort(collectionDateSort), { pageSize: MAX_PROJECTS_PER_PAGE });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: Page<CollectionEntry<"projects">>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: "Things I've built",
|
||||||
|
title: "Projects",
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationProps = {
|
||||||
|
...(page.url.prev && {
|
||||||
|
prevUrl: {
|
||||||
|
text: "← Previous Page",
|
||||||
|
url: page.url.prev,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(page.url.next && {
|
||||||
|
nextUrl: {
|
||||||
|
text: "Next Page →",
|
||||||
|
url: page.url.next,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<section>
|
||||||
|
<h1 class="title mb-12 flex items-center gap-3">
|
||||||
|
Projects <a class="text-accent" href="/projects/rss.xml" target="_blank">
|
||||||
|
<span class="sr-only">RSS feed</span>
|
||||||
|
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:rss" />
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<ul class="mt-6 space-y-8 text-start">
|
||||||
|
{
|
||||||
|
page.data.map((project) => (
|
||||||
|
<li class="">
|
||||||
|
<Project project={project} as="h2" isPreview />
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<Pagination {...paginationProps} />
|
||||||
|
</section>
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
|
import Project from "@/components/project/Project.astro";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
|
||||||
|
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const allProjects = await getCollection("projects");
|
||||||
|
return allProjects.map((project) => ({
|
||||||
|
params: { slug: project.id },
|
||||||
|
props: { project },
|
||||||
|
}));
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: project.data.description || `Project: ${project.data.title}`,
|
||||||
|
title: project.data.title,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<Project as="h1" project={project} />
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import rss from "@astrojs/rss";
|
||||||
|
import { siteConfig } from "@/site.config";
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const projects = await getCollection("projects");
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: siteConfig.title,
|
||||||
|
description: siteConfig.description,
|
||||||
|
site: import.meta.env.SITE,
|
||||||
|
items: projects.map((project) => ({
|
||||||
|
title: project.data.title,
|
||||||
|
pubDate: project.data.publishDate,
|
||||||
|
link: `projects/${project.id}/`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import type { GetStaticPaths, Page } from "astro";
|
||||||
|
import FormattedDate from "@/components/FormattedDate.astro";
|
||||||
|
import Pagination from "@/components/Paginator.astro";
|
||||||
|
import { getAllSpeaking } from "@/data/speaking";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { collectionDateSort } from "@/utils/date";
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
const talks = await getAllSpeaking();
|
||||||
|
return paginate(talks.sort(collectionDateSort), { pageSize: 10 });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: Page<CollectionEntry<"speaking">>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: "Talks and presentations I've given",
|
||||||
|
title: "Speaking",
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationProps = {
|
||||||
|
...(page.url.prev && { prevUrl: { text: "← Previous Page", url: page.url.prev } }),
|
||||||
|
...(page.url.next && { nextUrl: { text: "Next Page →", url: page.url.next } }),
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<h1 class="title mb-12">Speaking</h1>
|
||||||
|
<ul class="space-y-6">
|
||||||
|
{
|
||||||
|
page.data.map((talk) => (
|
||||||
|
<li class="grid gap-1 sm:grid-cols-[auto_1fr]">
|
||||||
|
<FormattedDate class="text-muted min-w-30 font-semibold" date={talk.data.publishDate} />
|
||||||
|
<div>
|
||||||
|
<h2 class="title text-base">
|
||||||
|
<a class="cactus-link" href={`/speaking/${talk.id}/`}>
|
||||||
|
{talk.data.title}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
{talk.data.event && <p class="text-muted text-sm">{talk.data.event}</p>}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<Pagination {...paginationProps} />
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from "astro:content";
|
||||||
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
|
import ExperienceTags from "@/components/ExperienceTags.astro";
|
||||||
|
import FormattedDate from "@/components/FormattedDate.astro";
|
||||||
|
import StreamPlayer from "@/components/StreamPlayer.astro";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const talks = await getCollection("speaking");
|
||||||
|
return talks.map((talk) => ({
|
||||||
|
params: { slug: talk.id },
|
||||||
|
props: { talk },
|
||||||
|
}));
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { talk } = Astro.props as Props;
|
||||||
|
const { Content } = await render(talk);
|
||||||
|
const { title, description, videoId, originalSource, event, publishDate, experiences } = talk.data;
|
||||||
|
|
||||||
|
const meta = { description, title };
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<article data-pagefind-body>
|
||||||
|
<h1 class="title mb-2">{title}</h1>
|
||||||
|
<p class="text-muted mb-6">
|
||||||
|
<FormattedDate date={publishDate} />{event && ` · ${event}`}
|
||||||
|
</p>
|
||||||
|
<StreamPlayer videoId={videoId} title={title} />
|
||||||
|
{
|
||||||
|
originalSource && (
|
||||||
|
<p class="text-muted mt-3 text-sm">
|
||||||
|
<a class="cactus-link" href={originalSource} rel="noopener noreferrer" target="_blank">
|
||||||
|
Watch original on YouTube →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="prose prose-sm prose-cactus mt-8 max-w-none">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
{experiences.length > 0 && <div class="mt-8"><ExperienceTags experiences={experiences} /></div>}
|
||||||
|
</article>
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import type { GetStaticPaths, Page } from "astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import PostPreview from "@/components/blog/PostPreview.astro";
|
||||||
|
import Pagination from "@/components/Paginator.astro";
|
||||||
|
import { getAllExperience } from "@/data/experience";
|
||||||
|
import { getAllPosts, groupPostsByYear } from "@/data/post";
|
||||||
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { collectionDateSort } from "@/utils/date";
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
const MAX_POSTS_PER_PAGE = 10;
|
||||||
|
const MAX_PINNED_POSTS = 3;
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
|
const allPostsByDate = allPosts.sort(collectionDateSort);
|
||||||
|
const experiences = await getAllExperience();
|
||||||
|
const pinnedPosts = allPostsByDate
|
||||||
|
.values()
|
||||||
|
.filter((p) => p.data.pinned)
|
||||||
|
.take(MAX_PINNED_POSTS)
|
||||||
|
.toArray();
|
||||||
|
return paginate(allPostsByDate, {
|
||||||
|
pageSize: MAX_POSTS_PER_PAGE,
|
||||||
|
props: { experiences, pinnedPosts },
|
||||||
|
});
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: Page<CollectionEntry<"writing">>;
|
||||||
|
experiences: CollectionEntry<"experience">[];
|
||||||
|
pinnedPosts: CollectionEntry<"writing">[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page, experiences, pinnedPosts } = Astro.props;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
description: "Essays, notes and articles I've written",
|
||||||
|
title: "Writing",
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationProps = {
|
||||||
|
...(page.url.prev && {
|
||||||
|
prevUrl: {
|
||||||
|
text: "← Previous Page",
|
||||||
|
url: page.url.prev,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(page.url.next && {
|
||||||
|
nextUrl: {
|
||||||
|
text: "Next Page →",
|
||||||
|
url: page.url.next,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedByYear = groupPostsByYear(page.data);
|
||||||
|
const descYearKeys = Object.keys(groupedByYear).sort((a, b) => +b - +a);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout meta={meta}>
|
||||||
|
<div class="mb-12 flex items-center gap-3">
|
||||||
|
<h1 class="title">Writing</h1>
|
||||||
|
<a class="text-accent" href="/rss.xml" target="_blank">
|
||||||
|
<span class="sr-only">RSS feed</span>
|
||||||
|
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:rss" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid sm:grid-cols-[3fr_1fr] sm:gap-x-8 sm:gap-y-16">
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
pinnedPosts.length > 0 && (
|
||||||
|
<section class="mb-16">
|
||||||
|
<h2 class="title mb-6 text-xl">Pinned</h2>
|
||||||
|
<ul class="space-y-4" role="list">
|
||||||
|
{pinnedPosts.map((p) => (
|
||||||
|
<li class="grid gap-1 sm:grid-cols-[auto_1fr]">
|
||||||
|
<PostPreview post={p} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
descYearKeys.map((yearKey) => (
|
||||||
|
<section class="mb-16">
|
||||||
|
<h2 id={`year-${yearKey}`} class="title text-lg">
|
||||||
|
<span class="sr-only">Posts in</span>
|
||||||
|
{yearKey}
|
||||||
|
</h2>
|
||||||
|
<ul class="mt-5 space-y-4 text-start">
|
||||||
|
{groupedByYear[yearKey]?.map((p) => (
|
||||||
|
<li class="grid gap-1 sm:grid-cols-[auto_1fr] sm:[&_q]:col-start-2">
|
||||||
|
<PostPreview post={p} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Pagination {...paginationProps} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
!!experiences.length && (
|
||||||
|
<aside>
|
||||||
|
<h2 class="title mb-4 text-lg">Experience</h2>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
{experiences.map((exp) => (
|
||||||
|
<li>
|
||||||
|
<a class="cactus-link" href={`/experience/${exp.id}/`}>
|
||||||
|
{exp.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<span class="mt-4 block sm:text-end">
|
||||||
|
<a class="hover:text-link" href="/experience/">
|
||||||
|
View all <span aria-hidden="true">→</span>
|
||||||
|
<span class="sr-only">experience</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { render } from "astro:content";
|
||||||
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
|
import { getAllPosts } from "@/data/post";
|
||||||
|
import PostLayout from "@/layouts/BlogPost.astro";
|
||||||
|
|
||||||
|
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const blogEntries = await getAllPosts();
|
||||||
|
return blogEntries.map((post) => ({
|
||||||
|
params: { slug: post.id },
|
||||||
|
props: { post },
|
||||||
|
}));
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const { Content } = await render(post);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PostLayout post={post}>
|
||||||
|
<Content />
|
||||||
|
</PostLayout>
|
||||||
Reference in New Issue
Block a user