diff --git a/apps/blog/.gitignore b/apps/blog/.gitignore
new file mode 100644
index 00000000..5ef6a520
--- /dev/null
+++ b/apps/blog/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/blog/README.md b/apps/blog/README.md
new file mode 100644
index 00000000..e215bc4c
--- /dev/null
+++ b/apps/blog/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/apps/blog/components.json b/apps/blog/components.json
new file mode 100644
index 00000000..d710b496
--- /dev/null
+++ b/apps/blog/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/apps/blog/eslint.config.mjs b/apps/blog/eslint.config.mjs
new file mode 100644
index 00000000..c85fb67c
--- /dev/null
+++ b/apps/blog/eslint.config.mjs
@@ -0,0 +1,16 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+];
+
+export default eslintConfig;
diff --git a/apps/blog/next.config.mjs b/apps/blog/next.config.mjs
new file mode 100644
index 00000000..b3fb6fae
--- /dev/null
+++ b/apps/blog/next.config.mjs
@@ -0,0 +1,9 @@
+import { rewrites as getAnalyticsRewrites } from '@ds-project/services/analytics/rewrites.mjs';
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ transpilePackages: ['next-mdx-remote'],
+ rewrites: getAnalyticsRewrites,
+};
+
+export default nextConfig;
diff --git a/apps/blog/package.json b/apps/blog/package.json
new file mode 100644
index 00000000..1b9f3316
--- /dev/null
+++ b/apps/blog/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "blog",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint"
+ },
+ "dependencies": {
+ "@ds-project/services": "workspace:*",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@t3-oss/env-core": "catalog:",
+ "@t3-oss/env-nextjs": "catalog:",
+ "@tailwindcss/typography": "^0.5.15",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.408.0",
+ "next": "catalog:",
+ "next-mdx-remote": "^5.0.0",
+ "next-themes": "^0.4.4",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "sonner": "^1.7.2",
+ "tailwind-merge": "^2.4.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "catalog:"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "eslint": "catalog:",
+ "eslint-config-next": "15.1.5",
+ "postcss": "catalog:",
+ "tailwindcss": "catalog:",
+ "typescript": "catalog:"
+ }
+}
diff --git a/apps/blog/postcss.config.mjs b/apps/blog/postcss.config.mjs
new file mode 100644
index 00000000..1a69fd2a
--- /dev/null
+++ b/apps/blog/postcss.config.mjs
@@ -0,0 +1,8 @@
+/** @type {import('postcss-load-config').Config} */
+const config = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
+
+export default config;
diff --git a/apps/blog/public/file.svg b/apps/blog/public/file.svg
new file mode 100644
index 00000000..004145cd
--- /dev/null
+++ b/apps/blog/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/blog/public/globe.svg b/apps/blog/public/globe.svg
new file mode 100644
index 00000000..567f17b0
--- /dev/null
+++ b/apps/blog/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/blog/public/next.svg b/apps/blog/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/apps/blog/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/blog/public/vercel.svg b/apps/blog/public/vercel.svg
new file mode 100644
index 00000000..77053960
--- /dev/null
+++ b/apps/blog/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/blog/public/window.svg b/apps/blog/public/window.svg
new file mode 100644
index 00000000..b2b2a44f
--- /dev/null
+++ b/apps/blog/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/blog/src/app/blog/[slug]/page.tsx b/apps/blog/src/app/blog/[slug]/page.tsx
new file mode 100644
index 00000000..c30db3b0
--- /dev/null
+++ b/apps/blog/src/app/blog/[slug]/page.tsx
@@ -0,0 +1,34 @@
+import { getPost, getAllPosts } from '@/utils/mdx';
+
+export async function generateStaticParams() {
+ const posts = await getAllPosts();
+ return posts.map((post) => ({
+ slug: post.slug,
+ }));
+}
+
+export default async function BlogPost({
+ params,
+}: {
+ params: { slug: string };
+}) {
+ const post = await getPost(params.slug);
+
+ return (
+ <>
+
+
+ {post.title}
+
+ {new Date(post.date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+ {post.content}
+
+
+ >
+ );
+}
diff --git a/apps/blog/src/app/favicon.ico b/apps/blog/src/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/apps/blog/src/app/favicon.ico differ
diff --git a/apps/blog/src/app/globals.css b/apps/blog/src/app/globals.css
new file mode 100644
index 00000000..a23ac26b
--- /dev/null
+++ b/apps/blog/src/app/globals.css
@@ -0,0 +1,72 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/apps/blog/src/app/layout.tsx b/apps/blog/src/app/layout.tsx
new file mode 100644
index 00000000..236fe530
--- /dev/null
+++ b/apps/blog/src/app/layout.tsx
@@ -0,0 +1,44 @@
+import type { Metadata } from 'next';
+import { Fira_Sans, Fira_Mono } from 'next/font/google';
+import './globals.css';
+import { Footer } from '@/components/footer';
+import { Toaster } from '@/components/ui/sonner';
+import { AnalyticsProvider, PageTracker } from '@ds-project/services/analytics';
+
+const fontSans = Fira_Sans({
+ variable: '--font-sans',
+ subsets: ['latin'],
+ weight: ['400', '700'],
+});
+
+const fontMono = Fira_Mono({
+ variable: '--font-mono',
+ subsets: ['latin'],
+ weight: ['400', '700'],
+});
+
+export const metadata: Metadata = {
+ title: 'DS Pro Blog',
+ description: 'Insights about Design Systems and Frontend Development',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/blog/src/app/page.tsx b/apps/blog/src/app/page.tsx
new file mode 100644
index 00000000..089e12db
--- /dev/null
+++ b/apps/blog/src/app/page.tsx
@@ -0,0 +1,16 @@
+import { Bio } from '@/components/bio';
+import { BlogPosts } from '@/components/blog-posts';
+import { Newsletter } from '@/components/newsletter';
+import { SocialLinks } from '@/components/social-links';
+
+export default function Home() {
+ return (
+ <>
+ DS Pro
+
+
+
+
+ >
+ );
+}
diff --git a/apps/blog/src/app/subscribe.action.ts b/apps/blog/src/app/subscribe.action.ts
new file mode 100644
index 00000000..d3e17474
--- /dev/null
+++ b/apps/blog/src/app/subscribe.action.ts
@@ -0,0 +1,35 @@
+'use server';
+
+import { serverEnv } from '@/env/server-env';
+
+export async function subscribeToNewsletter(formData: FormData) {
+ const email = formData.get('email');
+
+ const { ok } = await fetch('https://api.useplunk.com/v1/track', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${serverEnv.PLUNK_API_KEY}`,
+ },
+ body: JSON.stringify({
+ event: 'subscribed',
+ email,
+ subscribed: false,
+ }),
+ });
+
+ if (!ok) {
+ return {
+ success: false,
+ message: 'Something went wrong. Please try again.',
+ };
+ }
+
+ // This is where you'd typically integrate with your email service
+ // For now, we'll just return a success message
+ return {
+ success: true,
+ message:
+ 'Thanks for subscribing! Check your email to confirm your subscription.',
+ };
+}
diff --git a/apps/blog/src/components/bio.tsx b/apps/blog/src/components/bio.tsx
new file mode 100644
index 00000000..43095c49
--- /dev/null
+++ b/apps/blog/src/components/bio.tsx
@@ -0,0 +1,14 @@
+export function Bio() {
+ return (
+
+
+ DS Pro is created by{' '}
+
+ Tomás Francisco
+
+ . Follow for thoughts about Design Systems, Digital Accessibility and
+ Frontend Development.
+
+
+ );
+}
diff --git a/apps/blog/src/components/blog-posts.tsx b/apps/blog/src/components/blog-posts.tsx
new file mode 100644
index 00000000..a4ae78b2
--- /dev/null
+++ b/apps/blog/src/components/blog-posts.tsx
@@ -0,0 +1,23 @@
+import { getAllPosts } from '@/utils/mdx';
+
+export async function BlogPosts() {
+ const posts = await getAllPosts();
+
+ return (
+
+ );
+}
diff --git a/apps/blog/src/components/footer.tsx b/apps/blog/src/components/footer.tsx
new file mode 100644
index 00000000..37bdf59c
--- /dev/null
+++ b/apps/blog/src/components/footer.tsx
@@ -0,0 +1,7 @@
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/apps/blog/src/components/newsletter.tsx b/apps/blog/src/components/newsletter.tsx
new file mode 100644
index 00000000..63b4c6a6
--- /dev/null
+++ b/apps/blog/src/components/newsletter.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import { useRef } from 'react';
+import { useFormStatus } from 'react-dom';
+import { subscribeToNewsletter } from '@/app/subscribe.action';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { toast } from 'sonner';
+
+function SubmitButton() {
+ const { pending } = useFormStatus();
+
+ return (
+
+ );
+}
+
+export function Newsletter() {
+ const formRef = useRef(null);
+
+ async function handleSubmit(formData: FormData) {
+ const result = await subscribeToNewsletter(formData);
+ if (result.success) {
+ toast.success(result.message);
+ formRef.current?.reset();
+ } else {
+ toast.error('Something went wrong. Please try again.');
+ }
+ }
+
+ return (
+
+ Newsletter
+
+
+ Subscribe to get notified when I publish new posts about design
+ systems and frontend development.
+
+
+
+
+ );
+}
diff --git a/apps/blog/src/components/social-links.tsx b/apps/blog/src/components/social-links.tsx
new file mode 100644
index 00000000..83d394fd
--- /dev/null
+++ b/apps/blog/src/components/social-links.tsx
@@ -0,0 +1,35 @@
+import { Github, Linkedin, Twitter } from 'lucide-react';
+
+export function SocialLinks() {
+ return (
+
+ );
+}
diff --git a/apps/blog/src/components/ui/button.tsx b/apps/blog/src/components/ui/button.tsx
new file mode 100644
index 00000000..36496a28
--- /dev/null
+++ b/apps/blog/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/apps/blog/src/components/ui/input.tsx b/apps/blog/src/components/ui/input.tsx
new file mode 100644
index 00000000..68551b92
--- /dev/null
+++ b/apps/blog/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/apps/blog/src/components/ui/sonner.tsx b/apps/blog/src/components/ui/sonner.tsx
new file mode 100644
index 00000000..452f4d9f
--- /dev/null
+++ b/apps/blog/src/components/ui/sonner.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/apps/blog/src/content/posts/accessible-design-systems.mdx b/apps/blog/src/content/posts/accessible-design-systems.mdx
new file mode 100644
index 00000000..b00cc27a
--- /dev/null
+++ b/apps/blog/src/content/posts/accessible-design-systems.mdx
@@ -0,0 +1,26 @@
+---
+title: "Building Accessible Design Systems"
+date: "2024-01-15"
+description: "A comprehensive guide to creating inclusive design systems that work for everyone"
+---
+
+# Building Accessible Design Systems
+
+Creating truly inclusive design systems requires more than just following WCAG guidelines. Let's explore how to bake accessibility into your design system from the ground up.
+
+## Why Accessibility Matters
+
+Design systems serve as the foundation for digital products. When we prioritize accessibility at this level, we ensure that all downstream applications inherit these inclusive practices.
+
+## Key Principles
+
+1. **Semantic HTML**: Always start with proper HTML structure
+2. **ARIA attributes**: Use them sparingly and only when necessary
+3. **Keyboard navigation**: Ensure all components are fully operable without a mouse
+4. **Color contrast**: Build a color system that meets WCAG 2.1 standards
+
+## Implementation Tips
+
+- Test components with screen readers during development
+- Include accessibility requirements in component documentation
+- Provide clear guidelines for component implementation
diff --git a/apps/blog/src/content/posts/component-driven-development.mdx b/apps/blog/src/content/posts/component-driven-development.mdx
new file mode 100644
index 00000000..d01af2fc
--- /dev/null
+++ b/apps/blog/src/content/posts/component-driven-development.mdx
@@ -0,0 +1,29 @@
+---
+title: "Component-Driven Development"
+date: "2024-01-29"
+description: "Best practices for developing and maintaining components in a design system"
+---
+
+# Component-Driven Development
+
+Component-driven development is a methodology that emphasizes building UIs from the bottom up, starting with components and ending with pages.
+
+## Benefits
+
+- Improved maintainability
+- Better reusability
+- Consistent user experience
+- Faster development cycles
+
+## Best Practices
+
+1. **Atomic Design Principles**
+ - Start with atoms
+ - Compose molecules
+ - Create organisms
+ - Assemble templates
+
+2. **Testing Strategy**
+ - Unit tests for logic
+ - Visual regression tests
+ - Accessibility testing
diff --git a/apps/blog/src/content/posts/design-system-documentation.mdx b/apps/blog/src/content/posts/design-system-documentation.mdx
new file mode 100644
index 00000000..cc97ae93
--- /dev/null
+++ b/apps/blog/src/content/posts/design-system-documentation.mdx
@@ -0,0 +1,29 @@
+---
+title: "Design System Documentation Best Practices"
+date: "2024-02-05"
+description: "How to create and maintain effective design system documentation"
+---
+
+# Design System Documentation Best Practices
+
+Good documentation is what transforms a component library into a true design system. Let's explore how to create documentation that serves both designers and developers.
+
+## Documentation Structure
+
+1. **Getting Started**
+ - Installation guides
+ - Basic principles
+ - Contributing guidelines
+
+2. **Component Documentation**
+ - Usage examples
+ - Props/API reference
+ - Accessibility considerations
+ - Do's and Don'ts
+
+## Maintenance Strategy
+
+- Keep docs close to code
+- Automate what you can
+- Regular review cycles
+- Version control for documentation
diff --git a/apps/blog/src/content/posts/design-tokens-frontend.mdx b/apps/blog/src/content/posts/design-tokens-frontend.mdx
new file mode 100644
index 00000000..1340fc3f
--- /dev/null
+++ b/apps/blog/src/content/posts/design-tokens-frontend.mdx
@@ -0,0 +1,25 @@
+---
+title: "Design Tokens in Modern Frontend"
+date: "2024-01-22"
+description: "How to implement and manage design tokens effectively in modern frontend applications"
+---
+
+# Design Tokens in Modern Frontend
+
+Design tokens are the foundation of any scalable design system. They help maintain consistency across platforms and make updates more manageable.
+
+## What Are Design Tokens?
+
+Design tokens are the smallest units of design decisions. They represent colors, spacing, typography, and other visual properties in a platform-agnostic way.
+
+## Implementation Strategies
+
+1. **Token Organization**
+ - Category-based structure
+ - Semantic naming conventions
+ - Platform-specific transformations
+
+2. **Build Process Integration**
+ - Style Dictionary
+ - CSS Custom Properties
+ - Theme switching support
diff --git a/apps/blog/src/env/server-env.ts b/apps/blog/src/env/server-env.ts
new file mode 100644
index 00000000..34e2d078
--- /dev/null
+++ b/apps/blog/src/env/server-env.ts
@@ -0,0 +1,12 @@
+import { createEnv } from '@t3-oss/env-nextjs';
+import { z } from 'zod';
+import { vercel } from '@t3-oss/env-core/presets';
+
+export const serverEnv = createEnv({
+ extends: [vercel()],
+ isServer: typeof window === 'undefined',
+ server: {
+ PLUNK_API_KEY: z.string().min(1),
+ },
+ experimental__runtimeEnv: process.env,
+});
diff --git a/apps/blog/src/lib/utils.ts b/apps/blog/src/lib/utils.ts
new file mode 100644
index 00000000..bd0c391d
--- /dev/null
+++ b/apps/blog/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/apps/blog/src/types/post.ts b/apps/blog/src/types/post.ts
new file mode 100644
index 00000000..2b875470
--- /dev/null
+++ b/apps/blog/src/types/post.ts
@@ -0,0 +1,12 @@
+export interface PostFrontmatter {
+ title: string;
+ date: string;
+ description: string;
+}
+
+export interface Post extends PostFrontmatter {
+ slug: string;
+ content: React.ReactNode;
+}
+
+export type PostPreview = Omit;
diff --git a/apps/blog/src/utils/mdx.ts b/apps/blog/src/utils/mdx.ts
new file mode 100644
index 00000000..22d2d4c4
--- /dev/null
+++ b/apps/blog/src/utils/mdx.ts
@@ -0,0 +1,42 @@
+import fs from 'fs';
+import path from 'path';
+import { compileMDX } from 'next-mdx-remote/rsc';
+import { Post, PostFrontmatter, PostPreview } from '../types/post';
+
+const POSTS_PATH = path.join(process.cwd(), 'src/content/posts');
+
+export async function getPost(slug: string): Promise {
+ const filePath = path.join(POSTS_PATH, `${slug}.mdx`);
+ const source = fs.readFileSync(filePath, 'utf8');
+
+ const { frontmatter, content } = await compileMDX({
+ source,
+ options: { parseFrontmatter: true },
+ });
+
+ // Validate required frontmatter fields
+ if (!frontmatter.title || !frontmatter.date || !frontmatter.description) {
+ throw new Error(`Missing required frontmatter fields in ${slug}.mdx`);
+ }
+
+ return {
+ slug,
+ content,
+ ...frontmatter,
+ };
+}
+
+export async function getAllPosts(): Promise {
+ const files = fs.readdirSync(POSTS_PATH);
+
+ const posts = await Promise.all(
+ files.map(async (file) => {
+ const slug = file.replace(/\.mdx$/, '');
+ const post = await getPost(slug);
+
+ return post;
+ })
+ );
+
+ return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
+}
diff --git a/apps/blog/tailwind.config.ts b/apps/blog/tailwind.config.ts
new file mode 100644
index 00000000..65f3ec12
--- /dev/null
+++ b/apps/blog/tailwind.config.ts
@@ -0,0 +1,62 @@
+import type { Config } from 'tailwindcss';
+
+export default {
+ darkMode: ['class'],
+ content: [
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ colors: {
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ '1': 'hsl(var(--chart-1))',
+ '2': 'hsl(var(--chart-2))',
+ '3': 'hsl(var(--chart-3))',
+ '4': 'hsl(var(--chart-4))',
+ '5': 'hsl(var(--chart-5))',
+ },
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
+} satisfies Config;
diff --git a/apps/blog/tsconfig.json b/apps/blog/tsconfig.json
new file mode 100644
index 00000000..c1334095
--- /dev/null
+++ b/apps/blog/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/engine/package.json b/apps/engine/package.json
index b02bb066..c2792d6e 100644
--- a/apps/engine/package.json
+++ b/apps/engine/package.json
@@ -33,8 +33,8 @@
"@sentry/nextjs": "^8.30.0",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.45.0",
- "@t3-oss/env-core": "^0.11.1",
- "@t3-oss/env-nextjs": "^0.11.1",
+ "@t3-oss/env-core": "catalog:",
+ "@t3-oss/env-nextjs": "catalog:",
"@tanstack/react-query": "^5.51.24",
"@terrazzo/token-tools": "catalog:",
"@trpc/react-query": "catalog:",
@@ -52,7 +52,6 @@
"next": "catalog:",
"next-safe-action": "^7.8.1",
"postgres": "^3.4.4",
- "posthog-js": "^1.190.2",
"rambda": "^9.2.1",
"react": "catalog:",
"react-diff-viewer": "^3.1.1",
@@ -73,7 +72,7 @@
"@ds-project/prettier": "workspace:*",
"@ds-project/services": "workspace:*",
"@ds-project/typescript": "workspace:*",
- "@next/env": "^14.2.13",
+ "@next/env": "catalog:",
"@octokit/types": "^13.5.0",
"@tailwindcss/typography": "^0.5.15",
"@types/fs-extra": "^11.0.4",
@@ -84,7 +83,7 @@
"@types/react-dom": "catalog:",
"drizzle-kit": "^0.24.2",
"eslint": "catalog:",
- "eslint-config-next": "14.2.5",
+ "eslint-config-next": "15.1.5",
"fs-extra": "^11.2.0",
"jiti": "^1.21.6",
"postcss": "catalog:",
diff --git a/apps/engine/src/app/layout.tsx b/apps/engine/src/app/layout.tsx
index c79f4b24..a2184df2 100644
--- a/apps/engine/src/app/layout.tsx
+++ b/apps/engine/src/app/layout.tsx
@@ -2,25 +2,14 @@ import './globals.css';
import { Inter } from 'next/font/google';
import { cn } from '@/lib/css';
-import { AnalyticsProvider } from '@/lib/analytics/provider';
import { Toaster } from '@ds-project/components';
import { Favicon } from '@/components';
-import dynamic from 'next/dynamic';
import { getMetadata } from '@/lib/metadata';
import { TooltipProvider } from '@ds-project/components';
+import { AnalyticsProvider, PageTracker } from '@ds-project/services/analytics';
const inter = Inter({ subsets: ['latin'] });
-const AnalyticsPageView = dynamic(
- () =>
- import('./_components/analytics-page-view').then(
- (module) => module.AnalyticsPageView
- ),
- {
- ssr: false,
- }
-);
-
export const metadata = getMetadata();
export default function RootLayout({
@@ -41,7 +30,7 @@ export default function RootLayout({
inter.className
)}
>
-
+
{children}