feat: Add articles in www

This commit is contained in:
deytenit committed 2026-01-31 13:16:38 +03:00
1 parent 283b1c87c2
commit 08a7f44588
8 files changed
+348 -11

No files matched your search

+12 -8
View File
@@ -1,5 +1,7 @@
import path from 'path'; import path from 'path';
import type { StorybookConfig } from '@storybook/react-vite'; import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
const config: StorybookConfig = { const config: StorybookConfig = {
stories: [ stories: [
@@ -25,14 +27,16 @@ const config: StorybookConfig = {
}, },
}, },
async viteFinal(config) { async viteFinal(config) {
config.resolve = config.resolve || {}; return mergeConfig(config, {
config.resolve.alias = { plugins: [tailwindcss()],
...config.resolve.alias, resolve: {
'#': path.resolve(__dirname, '../src'), alias: {
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'), '#': path.resolve(__dirname, '../src'),
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'), '@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
}; '@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
return config; },
},
});
}, },
}; };
@@ -0,0 +1,19 @@
---
title: "Hello World"
description: "My first article using the new content system."
createdDate: 2026-01-31
tags: ["test", "general"]
---
# Hello World
This is a test article.
## Section 1
Some content here.
```typescript
const hello = "world";
console.log(hello);
```
+18
View File
@@ -0,0 +1,18 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const articles = defineCollection({
// Load all markdown and MDX files from the content/articles directory
loader: glob({ pattern: "**/*.{md,mdx}", base: "./content/articles" }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
// Astro handles date strings in frontmatter, but we use coerce to be safe
createdDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
}),
});
export const collections = { articles };
@@ -0,0 +1,99 @@
import { ArticleLayout } from './ArticleLayout';
import type { Meta, StoryObj } from '@storybook/react';
/**
* Metadata for the ArticleLayout component stories.
*/
const meta: Meta<typeof ArticleLayout> = {
title: 'WWW/Layouts/ArticleLayout',
component: ArticleLayout,
tags: ['autodocs'],
args: {
title: 'Building a Modern Content System with Astro 5',
description:
"A deep dive into building a scalable, file-based content architecture using Astro's new Content Layer API and MDX.",
createdDate: new Date('2026-01-31'),
tags: ['engineering', 'astro', 'react'],
},
};
export default meta;
/**
*
*/
type Story = StoryObj<typeof ArticleLayout>;
/**
* Default story showcasing various HTML elements to verify typography.
*/
export const Default: Story = {
render: (args) => (
<ArticleLayout {...args}>
<h2>Introduction</h2>
<p>
Astro 5 introduces the Content Layer API, a powerful way to fetch content from any source,
including local files, remote APIs, and CMSs. In this article, we&apos;ll explore how to set
up a robust system for articles and projects.
</p>
<blockquote>
&quot;The best way to manage content is to not manage it at all.&quot; — Someone, probably.
</blockquote>
<p>
Let&apos;s look at some code. Our new <code>content.config.ts</code> uses the{' '}
<code>glob</code> loader to pull in MDX files from outside the <code>src</code> directory:
</p>
<pre>
<code>{`// services/www/src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const articles = defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "./content/articles" }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
createdDate: z.date(),
updatedDate: z.date().optional(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { articles };`}</code>
</pre>
<h3>Why Astro 5?</h3>
<p>
Astro&apos;s zero-JS by default approach is perfect for content-heavy sites. By moving our
content outside of the <code>src</code> directory, we keep our project clean and organized.
</p>
<ul>
<li>
<strong>Better performance:</strong> Only what you need is shipped.
</li>
<li>
<strong>Improved developer experience:</strong> Fast HMR and great tooling.
</li>
<li>
<strong>Type safety:</strong> Zod schemas validate your frontmatter.
</li>
</ul>
<h3>Visual Media</h3>
<p>
We can also include images and videos. The layout ensures they are responsive and fit the
grid.
</p>
<div className="bg-muted flex aspect-video items-center justify-center rounded-lg border-2 border-dashed">
<span className="text-muted-foreground">Placeholder for Image/Video</span>
</div>
<p>Conclusion: It&apos;s a great time to be a web developer.</p>
</ArticleLayout>
),
};
@@ -0,0 +1,106 @@
import { cn } from '@ermnvldmr/stl';
import { VStack, HStack } from '@ermnvldmr/ui';
import React from 'react';
/**
* Props for the ArticleLayout component.
*/
export interface ArticleLayoutProps {
/** The title of the article. */
title: string;
/** A short description or lead paragraph for the article. */
description?: string;
/** The date the article was created. */
createdDate: Date;
/** The date the article was last updated. */
updatedDate?: Date;
/** An array of tags associated with the article. */
tags?: string[];
/** The content of the article. */
children: React.ReactNode;
/** Optional additional class names for the article container. */
className?: string;
}
/**
* ArticleLayout provides a structured container for long-form content.
* It uses the project's base typography styles and provides a hero section
* for metadata like title, date, and tags.
*
* @param props - The component props.
* @param props.title
* @param props.description
* @param props.createdDate
* @param props.updatedDate
* @param props.tags
* @param props.children
* @param props.className
* @returns A React element representing the article layout.
* @example
* ```tsx
* <ArticleLayout
* title="Hello World"
* createdDate={new Date()}
* tags={['hello', 'world']}
* >
* <p>This is my first article.</p>
* </ArticleLayout>
* ```
*/
export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
title,
description,
createdDate,
updatedDate,
tags,
children,
className,
}) => {
const formattedDate = createdDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const displayDate = updatedDate
? `Updated ${updatedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}`
: formattedDate;
return (
<article className={cn('mx-auto max-w-3xl px-6 py-12 md:py-20', className)}>
<header className="pb-12">
<VStack gap={4}>
<HStack className="text-muted-foreground text-sm font-medium" gap={4}>
<time dateTime={createdDate.toISOString()}>{displayDate}</time>
{tags && tags.length > 0 && (
<HStack className="flex-wrap" gap={2}>
{tags.map((tag) => (
<span
key={tag}
className="before:text-muted-foreground/50 before:mr-0.5 before:content-['#']"
>
{tag}
</span>
))}
</HStack>
)}
</HStack>
<h1 className="leading-tight">{title}</h1>
{description && (
<p className="text-muted-foreground text-l leading-relaxed italic">
{description}
</p>
)}
</VStack>
</header>
<section className="article-content">{children}</section>
</article>
);
};
@@ -0,0 +1,35 @@
---
import { getCollection, render } from 'astro:content';
import { ArticleLayout } from '#layouts/ArticleLayout/ArticleLayout';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import '#global.css';
/**
* Generate paths for all articles in the collection.
* The slug will include the year directory (e.g., "2026/test-article").
*/
export async function getStaticPaths() {
const articles = await getCollection('articles');
return articles.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const { title, description, createdDate, updatedDate, tags } = entry.data;
---
<IndexLayout title={title} description={description ?? ''}>
<ArticleLayout
title={title}
description={description}
createdDate={createdDate}
updatedDate={updatedDate}
tags={tags}
>
<Content />
</ArticleLayout>
</IndexLayout>
@@ -0,0 +1,53 @@
---
import { getCollection } from 'astro:content';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import { SITE_TITLE } from '#shared/constants';
import '#global.css';
const articles = (await getCollection('articles')).sort(
(a, b) => b.data.createdDate.valueOf() - a.data.createdDate.valueOf()
);
---
<IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects.">
<main class="mx-auto max-w-3xl px-6 py-20">
<h1 class="mb-12">Articles</h1>
<ul class="space-y-12">
{
articles.map((article) => (
<li>
<a href={`/articles/${article.id}`} class="group block no-underline">
<article class="space-y-3">
<time class="text-muted-foreground text-sm font-medium">
{article.data.createdDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<h2 class="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors">
{article.data.title}
</h2>
{article.data.description && (
<p class="text-muted-foreground line-clamp-3 text-lg leading-relaxed">
{article.data.description}
</p>
)}
{article.data.tags && (
<div class="flex gap-2">
{article.data.tags.map((tag) => (
<span class="text-muted-foreground text-xs before:content-['#']">
{tag}
</span>
))}
</div>
)}
</article>
</a>
</li>
))
}
</ul>
</main>
</IndexLayout>
+6 -3
View File
@@ -14,13 +14,16 @@ import '#global.css';
It will be further configured. It will be further configured.
</p> </p>
<p> <p>
For the source code and support please refer to <a href="https://github.com/deytenit" For the source code and support please refer to <a href="https://github.com/deytenit/ermnvldmr.com/"
>github.com</a >ermnvldmr.com monorepo</a
>. >.
<br /> <br />
Pictures of mine are available at <a href="https://instagram.com/deytenit">instagram.com</a Pictures of mine are available at <a href="https://ermnvldmr.com/instagram">@deytenit</a
>. >.
</p> </p>
<p>
Check out my <a href="/articles">thoughts and articles on various matters</a>.
</p>
<p><em>Thank you for visiting!</em></p> <p><em>Thank you for visiting!</em></p>
</div> </div>
</main> </main>