From 08a7f44588d25fbdd5dcda94884d38d9bb27e6a9 Mon Sep 17 00:00:00 2001
From: Vladimir Eremin
Date: Sat, 31 Jan 2026 13:16:38 +0300
Subject: [PATCH] feat: Add articles in www
---
services/www/.storybook/main.ts | 20 ++--
.../content/articles/2026/test-article.mdx | 19 ++++
services/www/src/content.config.ts | 18 +++
.../ArticleLayout/ArticleLayout.stories.tsx | 99 ++++++++++++++++
.../layouts/ArticleLayout/ArticleLayout.tsx | 106 ++++++++++++++++++
.../www/src/pages/articles/[...slug].astro | 35 ++++++
services/www/src/pages/articles/index.astro | 53 +++++++++
services/www/src/pages/index.astro | 9 +-
8 files changed, 348 insertions(+), 11 deletions(-)
create mode 100644 services/www/content/articles/2026/test-article.mdx
create mode 100644 services/www/src/content.config.ts
create mode 100644 services/www/src/layouts/ArticleLayout/ArticleLayout.stories.tsx
create mode 100644 services/www/src/layouts/ArticleLayout/ArticleLayout.tsx
create mode 100644 services/www/src/pages/articles/[...slug].astro
create mode 100644 services/www/src/pages/articles/index.astro
diff --git a/services/www/.storybook/main.ts b/services/www/.storybook/main.ts
index 894331b..58a224c 100644
--- a/services/www/.storybook/main.ts
+++ b/services/www/.storybook/main.ts
@@ -1,5 +1,7 @@
import path from 'path';
import type { StorybookConfig } from '@storybook/react-vite';
+import { mergeConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
const config: StorybookConfig = {
stories: [
@@ -25,14 +27,16 @@ const config: StorybookConfig = {
},
},
async viteFinal(config) {
- config.resolve = config.resolve || {};
- config.resolve.alias = {
- ...config.resolve.alias,
- '#': path.resolve(__dirname, '../src'),
- '@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
- '@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
- };
- return config;
+ return mergeConfig(config, {
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ '#': path.resolve(__dirname, '../src'),
+ '@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
+ '@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
+ },
+ },
+ });
},
};
diff --git a/services/www/content/articles/2026/test-article.mdx b/services/www/content/articles/2026/test-article.mdx
new file mode 100644
index 0000000..d588649
--- /dev/null
+++ b/services/www/content/articles/2026/test-article.mdx
@@ -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);
+```
diff --git a/services/www/src/content.config.ts b/services/www/src/content.config.ts
new file mode 100644
index 0000000..980d5cc
--- /dev/null
+++ b/services/www/src/content.config.ts
@@ -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 };
diff --git a/services/www/src/layouts/ArticleLayout/ArticleLayout.stories.tsx b/services/www/src/layouts/ArticleLayout/ArticleLayout.stories.tsx
new file mode 100644
index 0000000..cdd2dba
--- /dev/null
+++ b/services/www/src/layouts/ArticleLayout/ArticleLayout.stories.tsx
@@ -0,0 +1,99 @@
+import { ArticleLayout } from './ArticleLayout';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+/**
+ * Metadata for the ArticleLayout component stories.
+ */
+const meta: Meta = {
+ 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;
+
+/**
+ * Default story showcasing various HTML elements to verify typography.
+ */
+export const Default: Story = {
+ render: (args) => (
+
+
Introduction
+
+ 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'll explore how to set
+ up a robust system for articles and projects.
+
+
+
+ "The best way to manage content is to not manage it at all." — Someone, probably.
+
+
+
+ Let's look at some code. Our new content.config.ts uses the{' '}
+ glob loader to pull in MDX files from outside the src directory:
+
+ Astro's zero-JS by default approach is perfect for content-heavy sites. By moving our
+ content outside of the src directory, we keep our project clean and organized.
+
+
+
+
+ Better performance: Only what you need is shipped.
+
+
+ Improved developer experience: Fast HMR and great tooling.
+
+
+ Type safety: Zod schemas validate your frontmatter.
+
+
+
+
Visual Media
+
+ We can also include images and videos. The layout ensures they are responsive and fit the
+ grid.
+
+
+ Placeholder for Image/Video
+
+
+
Conclusion: It's a great time to be a web developer.
+
+ ),
+};
diff --git a/services/www/src/layouts/ArticleLayout/ArticleLayout.tsx b/services/www/src/layouts/ArticleLayout/ArticleLayout.tsx
new file mode 100644
index 0000000..140c84b
--- /dev/null
+++ b/services/www/src/layouts/ArticleLayout/ArticleLayout.tsx
@@ -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
+ *
+ *
+
+
diff --git a/services/www/src/pages/index.astro b/services/www/src/pages/index.astro
index 0d3f47d..1101ef3 100644
--- a/services/www/src/pages/index.astro
+++ b/services/www/src/pages/index.astro
@@ -14,13 +14,16 @@ import '#global.css';
It will be further configured.
- For the source code and support please refer to github.comermnvldmr.com monorepo.
- Pictures of mine are available at instagram.com@deytenit.