diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 6614670..18a80a3 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -55,6 +55,11 @@ jobs: with: node-version: 24 cache: npm + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true - name: Restore cache uses: actions/cache@v4 with: @@ -70,6 +75,7 @@ jobs: - name: Build with Next.js env: NEXT_BASE_PATH: ${{ github.event.repository.name }} + JEKYLL_BASE_PATH: /${{ github.event.repository.name }}/blogs run: npm run build - name: Download DocumentDB packages from latest release run: .github/scripts/download_packages.sh diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ee09c25..72ef7b5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -41,6 +41,12 @@ jobs: with: node-version: 24 cache: npm + - name: Setup Ruby + # The full site build now includes the Jekyll-powered blogs section. + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true - name: Install dependencies run: npm ci - name: Build Next.js site diff --git a/.gitignore b/.gitignore index ab74a61..d62659b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,12 @@ articles/**/*.yml /.next/ /out/ +# jekyll +/.bundle/ +/.jekyll-cache/ +/.sass-cache/ +/vendor/bundle/ + # production /build @@ -48,4 +54,4 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3009d5d --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +gem "ffi", "~> 1.15.5" +gem "jekyll", "~> 4.3.4" +gem "webrick", "~> 1.8" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..acc265d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,76 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + colorator (1.1.0) + concurrent-ruby (1.3.6) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.15.5) + forwardable-extended (2.6.0) + google-protobuf (3.23.4) + http_parser.rb (0.8.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + jekyll (4.3.4) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-watch (2.2.1) + listen (~> 3.0) + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (5.1.1) + rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.4) + rouge (3.30.0) + safe_yaml (1.0.5) + sass-embedded (1.58.3) + google-protobuf (~> 3.21) + rake (>= 10.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.2) + +PLATFORMS + ruby + +DEPENDENCIES + ffi (~> 1.15.5) + jekyll (~> 4.3.4) + webrick (~> 1.8) + +BUNDLED WITH + 2.4.22 diff --git a/app/blogs/page.tsx b/app/blogs/page.tsx deleted file mode 100644 index a42308b..0000000 --- a/app/blogs/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { getAllPosts } from '../services/blogService'; -import { Card } from '../components/Card'; -import { Post } from '../types/Post'; - -export default function Blogs() { - const posts: Post[] = getAllPosts(); - const featuredPosts = posts.filter(post => post.featured); - const regularPosts = posts.filter(post => !post.featured); - - return ( -
- {/* Artistic background elements */} -
-
-
-
-
-
-
- - {/* Floating particles */} -
-
-
-
-
- -
- {/* Header */} -
-

- Latest from our Blog -

-

- Insights, updates, and deep dives into the world of document databases and open-source innovation -

-
-
- - - {/* Blog Grid */} -
- {/* Featured Posts */} - {featuredPosts.map((post, index) => ( - - ))} - - {/* Regular Posts */} - {regularPosts.map((post, index) => ( - - ))} -
-
-
- ); -} \ No newline at end of file diff --git a/app/components/Card.tsx b/app/components/Card.tsx deleted file mode 100644 index 2fe678d..0000000 --- a/app/components/Card.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { JSX } from 'react'; -import { Post } from '../types/Post'; - -export function Card({ post, featured = false }: { post: Post; featured?: boolean }) { - const icons: Record = { - 'microsoft-open-source-blog': ( - - ), - 'aws-blog': ( - - ), - 'azure-cosmos-db-blog': ( - - ), - 'yugabytedb-blog': ( - - ), - }; - - const icon = icons[post.category as keyof typeof icons]; - - const styles = { - 'microsoft-open-source-blog': { - label: 'Microsoft Open Source Blog', - timestamp: 'August 25, 2025', - gradientFrom: 'from-blue-500', - gradientTo: 'to-blue-600', - textColor: 'blue-400', - hoverColor: 'blue-300', - bgGradient: 'from-blue-500/10 to-purple-500/10', - borderHover: 'border-blue-500/50', - tagColors: [ - 'bg-blue-500/20 text-blue-400', - 'bg-purple-500/20 text-purple-400', - 'bg-green-500/20 text-green-400', - 'bg-orange-500/20 text-orange-400', - ], - }, - 'aws-blog': { - label: 'AWS Blogs', - timestamp: 'Recent', - gradientFrom: 'from-orange-500', - gradientTo: 'to-orange-600', - textColor: 'orange-400', - hoverColor: 'orange-300', - bgGradient: 'from-orange-500/10 to-amber-500/10', - borderHover: 'border-orange-500/50', - tagColors: [ - 'bg-orange-500/20 text-orange-400', - 'bg-amber-500/20 text-amber-400', - 'bg-yellow-500/20 text-yellow-400', - 'bg-blue-500/20 text-blue-400', - ], - }, - 'azure-cosmos-db-blog': { - label: 'Azure Cosmos DB Blog', - timestamp: 'Recent', - gradientFrom: 'from-purple-500', - gradientTo: 'to-purple-600', - textColor: 'purple-400', - hoverColor: 'purple-300', - bgGradient: 'from-purple-500/10 to-pink-500/10', - borderHover: 'border-purple-500/50', - tagColors: [ - 'bg-purple-500/20 text-purple-400', - 'bg-blue-500/20 text-blue-400', - 'bg-green-500/20 text-green-400', - 'bg-orange-500/20 text-orange-400', - ], - }, - 'yugabytedb-blog': { - label: 'YugabyteDB Blog', - timestamp: 'Partner Content', - gradientFrom: 'from-green-500', - gradientTo: 'to-green-600', - textColor: 'green-400', - hoverColor: 'green-300', - bgGradient: 'from-green-500/10 to-emerald-500/10', - borderHover: 'border-green-500/50', - tagColors: [ - 'bg-green-500/20 text-green-400', - 'bg-blue-500/20 text-blue-400', - 'bg-orange-500/20 text-orange-400', - 'bg-yellow-500/20 text-yellow-400', - ], - }, - }; - - const style = styles[post.category as keyof typeof styles]; - - return ( - - ); -} diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index d1b04f8..e91f3d7 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -81,12 +81,12 @@ export default function Navbar() { > K8s Operator - Blogs - + diff --git a/app/services/blogService.ts b/app/services/blogService.ts deleted file mode 100644 index 82c58a8..0000000 --- a/app/services/blogService.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import yaml from 'js-yaml'; -import { Post } from '../types/Post'; - -export function getAllPosts(): Post[] { - const filePath = path.join(process.cwd(), 'blogs', 'content.yml'); - const fileContents = fs.readFileSync(filePath, 'utf8'); - const posts = yaml.load(fileContents) as Post[]; - return posts; -} \ No newline at end of file diff --git a/app/types/Post.ts b/app/types/Post.ts deleted file mode 100644 index 23419f0..0000000 --- a/app/types/Post.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Post { - title: string; - category: string; - description: string; - tags: string[]; - featured?: boolean; - uri: string; -} \ No newline at end of file diff --git a/blogs/_config.yml b/blogs/_config.yml new file mode 100644 index 0000000..f9503d1 --- /dev/null +++ b/blogs/_config.yml @@ -0,0 +1,22 @@ +title: DocumentDB Blog +description: Insights, updates, and deep dives into the world of document databases and open-source innovation. +baseurl: /blogs +url: "" +permalink: pretty +markdown: kramdown +kramdown: + input: GFM +defaults: + - + scope: + path: "" + values: + layout: default + - + scope: + path: "" + type: posts + values: + layout: post + category: documentdb-blog + permalink: /posts/:title/ diff --git a/blogs/_data/categories.yml b/blogs/_data/categories.yml new file mode 100644 index 0000000..65e72ca --- /dev/null +++ b/blogs/_data/categories.yml @@ -0,0 +1,15 @@ +documentdb-blog: + label: DocumentDB Blog + meta: Published here +microsoft-open-source-blog: + label: Microsoft Open Source Blog + meta: August 25, 2025 +aws-blog: + label: AWS Blogs + meta: Recent +azure-cosmos-db-blog: + label: Azure Cosmos DB Blog + meta: Recent +yugabytedb-blog: + label: YugabyteDB Blog + meta: Partner Content diff --git a/blogs/content.yml b/blogs/_data/posts.yml similarity index 100% rename from blogs/content.yml rename to blogs/_data/posts.yml diff --git a/blogs/_includes/post-card.html b/blogs/_includes/post-card.html new file mode 100644 index 0000000..79ffd0f --- /dev/null +++ b/blogs/_includes/post-card.html @@ -0,0 +1,74 @@ +{% assign post = include.post %} +{% assign category = site.data.categories[post.category] %} +{% assign description = post.description | default: post.excerpt | strip_html | strip_newlines | strip %} + +{% if post.date %} + {% assign meta_text = post.date | date: "%B %-d, %Y" %} +{% elsif post.meta %} + {% assign meta_text = post.meta %} +{% else %} + {% assign meta_text = category.meta | default: "Community article" %} +{% endif %} + +{% if post.uri %} + {% assign href = post.uri %} + {% assign is_external = true %} +{% else %} + {% assign href = post.url | relative_url %} + {% assign is_external = false %} +{% endif %} + +
+ + {% if post.cover_image %} + {% if post.cover_image contains '://' %} + {% assign cover_image_src = post.cover_image %} + {% else %} + {% assign cover_image_src = post.cover_image | relative_url %} + {% endif %} +
+ {{ post.cover_image_alt | default: post.title | escape }} +
+ {% endif %} + +
+
+ +
+

{{ category.label | default: post.category }}

+

{{ meta_text }}

+
+
+ +

{{ post.title }}

+

{{ description }}

+ + {% if post.tags and post.tags.size > 0 %} +
+ {% for tag in post.tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} + + + {% if is_external %} + {% if include.featured %}Read full article{% else %}Read more{% endif %} + {% else %} + {% if include.featured %}Read featured post{% else %}Read post{% endif %} + {% endif %} + + +
+
+
diff --git a/blogs/_layouts/default.html b/blogs/_layouts/default.html new file mode 100644 index 0000000..91df859 --- /dev/null +++ b/blogs/_layouts/default.html @@ -0,0 +1,48 @@ + + + + + + {% if page.title %}{{ page.title }} · {% endif %}DocumentDB + + + + + + {% assign site_root = site.baseurl | replace: '/blogs', '' %} + {% if site_root == '' %} + {% assign site_root = '/' %} + {% endif %} + + {% assign home_href = site_root | append: '/' | replace: '//', '/' %} + {% assign docs_href = site_root | append: '/docs/' | replace: '//', '/' %} + {% assign packages_href = site_root | append: '/packages/' | replace: '//', '/' %} + {% assign blogs_href = site.baseurl | append: '/' | replace: '//', '/' %} + +
+ + +
+ {{ content }} +
+ +
+ +
+
+ + diff --git a/blogs/_layouts/post.html b/blogs/_layouts/post.html new file mode 100644 index 0000000..13f702b --- /dev/null +++ b/blogs/_layouts/post.html @@ -0,0 +1,53 @@ +--- +layout: default +--- +{% assign category = site.data.categories[page.category] %} + +
+

+ ← Back to all blog posts +

+ +
+

{{ category.label | default: "DocumentDB Blog" }}

+

{{ page.title }}

+ + + + {% if page.description %} +

{{ page.description }}

+ {% endif %} + + {% if page.tags and page.tags.size > 0 %} + + {% endif %} +
+ + {% if page.cover_image %} + {% if page.cover_image contains '://' %} + {% assign cover_image_src = page.cover_image %} + {% else %} + {% assign cover_image_src = page.cover_image | relative_url %} + {% endif %} +
+ {{ page.cover_image_alt | default: page.title | escape }} +
+ {% endif %} + +
+
+ {{ content }} +
+
+
diff --git a/blogs/_posts/2026-03-19-meet-documentdb-kubernetes-operator.md b/blogs/_posts/2026-03-19-meet-documentdb-kubernetes-operator.md new file mode 100644 index 0000000..46aaf2a --- /dev/null +++ b/blogs/_posts/2026-03-19-meet-documentdb-kubernetes-operator.md @@ -0,0 +1,91 @@ +--- +title: Meet the DocumentDB Kubernetes Operator +description: Preview an open-source operator that brings declarative deployment, secure connectivity, high availability, backup and restore, and multi-environment deployment patterns to DocumentDB on Kubernetes. +date: 2026-03-19 +featured: true +author: DocumentDB team +category: documentdb-blog +tags: + - Kubernetes + - Operator + - DocumentDB + - Open Source +--- +Running a document database on Kubernetes should not require a maze of hand-built scripts for failover, certificate handling, backup jobs, and day-two troubleshooting. The new DocumentDB Kubernetes Operator is designed to turn that operational work into a Kubernetes-native workflow. + +DocumentDB is the engine powering vCore-based Azure Cosmos DB for MongoDB. Built on PostgreSQL, it provides a native document-oriented NoSQL database with support for CRUD operations on BSON data types. The Kubernetes Operator runs and manages DocumentDB on Kubernetes: when you deploy a cluster, it creates and manages PostgreSQL instances, the DocumentDB Gateway, and the supporting Kubernetes resources around them. Because the gateway enables MongoDB-compatible drivers, APIs, and tools, teams can keep working with familiar clients such as `mongosh`. + +## Why this matters + +Kubernetes teams have strong patterns for stateless applications, but data services often still depend on custom runbooks and fragile operational glue. The DocumentDB Kubernetes Operator closes that gap for DocumentDB by giving platform teams a single declarative control surface for deployment, availability, security, and recovery. + +With the preview release, you can: + +- install the operator with Helm +- have the Helm chart install CloudNativePG as a dependency +- deploy DocumentDB through a Kubernetes custom resource +- connect with `mongosh` and other MongoDB-compatible tooling +- manage backup, restore, TLS, and promotion workflows through Kubernetes-native APIs + +## From quickstart to useful data fast + +The quickstart is intentionally direct. Install `cert-manager`, install the operator, create a Secret for gateway credentials, and apply a `DocumentDB` resource. The public preview docs target Kubernetes 1.35+ and call out local development with `kind` (v0.31+) and `minikube`, while also showing cloud-friendly access patterns through `LoadBalancer` services on AKS, EKS, and GKE. + +At the heart of that flow is a simple custom resource: + +```yaml +apiVersion: documentdb.io/preview +kind: DocumentDB +metadata: + name: documentdb-preview + namespace: documentdb-preview-ns +spec: + nodeCount: 1 + instancesPerNode: 1 + documentDbCredentialSecret: documentdb-credentials + resource: + storage: + pvcSize: 10Gi + exposeViaService: + serviceType: ClusterIP +``` + +Once the cluster reports a healthy state, you can connect locally with port forwarding or expose it through a load balancer in supported environments. The result is a much shorter path from cluster creation to a live MongoDB-compatible endpoint. + +## Built for day-two operations + +Bringing up a cluster is only the beginning, so the operator is opinionated about day-two workflows as well. + +Set `instancesPerNode: 3` and the operator creates one primary instance and two replicas for local high availability and automatic failover. Use `Backup` and `ScheduledBackup` resources for on-demand and scheduled backups, retention policies, and restore workflows into a new cluster. And when operators need visibility into what is happening, the `kubectl documentdb` plugin adds purpose-built commands for status inspection, event triage, and primary promotion. + +That makes the operator interesting not only for first deployment, but for the operational rhythm that follows: health checks, recovery planning, planned changes, and repeatable workflows that fit naturally into Kubernetes. + +## Security and connectivity without reinventing the basics + +Secure connectivity is built into the model from the start. The DocumentDB gateway always encrypts client connections; the TLS mode controls how certificates are managed. In practice, that means teams can choose the workflow that matches their environment: + +- `SelfSigned` for development and test environments +- `CertManager` for clusters that already standardize on cert-manager +- `Provided` for organizations that manage certificates through their own PKI processes + +The same pattern carries into networking. The docs cover local development through `ClusterIP` plus port forwarding, and cloud exposure through `LoadBalancer` services where that model makes sense. The operator keeps those choices declarative instead of forcing each team to reinvent them. + +## A practical path to multi-environment deployment + +The public docs position the operator across multiple cloud environments and Kubernetes distributions, and the multi-cluster guidance goes further with a documented KubeFleet-based deployment pattern spanning AKS and an on-premises Kubernetes cluster. That is a meaningful direction for teams that want Kubernetes-native control over data placement and replication without committing their operational model to a single environment. + +Just as importantly, the operator keeps sensitive workflows explicit. Promotion is an intentional operational action, surfaced through Kubernetes resources and the `kubectl documentdb promote` workflow, which is often exactly the right design when changes cross infrastructure or compliance boundaries. + +## Why try it now + +The project is still in preview, and the public docs are clear that it is not yet recommended for production workloads. That is exactly why now is the right time to evaluate it. + +If you want to see how DocumentDB could fit into your Kubernetes platform, this preview gives you a concrete way to do it: start locally, inspect the control model, test backup and restore, evaluate TLS options, and explore the operator experience before general availability. + +## Start exploring + +- [Quickstart guide](https://github.com/documentdb/documentdb-kubernetes-operator/blob/main/docs/operator-public-documentation/preview/index.md) +- [Backup and restore guide](https://github.com/documentdb/documentdb-kubernetes-operator/blob/main/docs/operator-public-documentation/preview/backup-and-restore.md) +- [kubectl-documentdb plugin](https://github.com/documentdb/documentdb-kubernetes-operator/blob/main/docs/operator-public-documentation/preview/kubectl-plugin.md) +- [TLS configuration guide](https://github.com/documentdb/documentdb-kubernetes-operator/blob/main/docs/operator-public-documentation/preview/configuration/tls.md) +- [Multi-cluster deployment guide](https://github.com/documentdb/documentdb-kubernetes-operator/blob/main/docs/developer-guides/manual-multi-cloud-deployment-guide.md) diff --git a/blogs/assets/blog.css b/blogs/assets/blog.css new file mode 100644 index 0000000..5c3fd5c --- /dev/null +++ b/blogs/assets/blog.css @@ -0,0 +1,609 @@ +:root { + --page-bg: #09090b; + --surface: rgba(23, 23, 27, 0.84); + --surface-strong: #111827; + --surface-soft: rgba(38, 38, 45, 0.82); + --text: #f5f7fb; + --muted: #a7afbe; + --subtle: #7c8699; + --border: rgba(148, 163, 184, 0.16); + --shadow: rgba(2, 6, 23, 0.5); +} + +* { + box-sizing: border-box; +} + +html { + min-height: 100%; +} + +body { + margin: 0; + min-height: 100%; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 30%), + radial-gradient(circle at top right, rgba(168, 85, 247, 0.14), transparent 28%), + radial-gradient(circle at bottom center, rgba(34, 197, 94, 0.1), transparent 26%), + var(--page-bg); +} + +a { + color: inherit; + text-decoration: none; +} + +.site-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.site-header { + position: sticky; + top: 0; + z-index: 40; + border-bottom: 1px solid var(--border); + background: rgba(9, 9, 11, 0.88); + backdrop-filter: blur(14px); +} + +.site-header__inner, +.site-footer__inner { + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; +} + +.site-header__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1rem 0; +} + +.site-brand { + font-size: 1.35rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.site-nav { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 1rem; + color: var(--muted); +} + +.site-nav a { + transition: color 160ms ease; +} + +.site-nav a:hover, +.site-nav a[aria-current="page"] { + color: var(--text); +} + +.blog-main { + flex: 1; + padding: 4.5rem 1rem 5rem; +} + +.blog-hero, +.posts-group, +.posts-list, +.blog-post { + width: min(1100px, 100%); + margin: 0 auto; +} + +.blog-hero { + text-align: center; + margin-bottom: 2.75rem; +} + +.blog-hero__kicker { + margin: 0 0 0.85rem; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #93c5fd; +} + +.blog-hero__title { + margin: 0; + font-size: clamp(2.75rem, 6vw, 4.5rem); + line-height: 1.05; + letter-spacing: -0.05em; +} + +.blog-hero__description { + max-width: 760px; + margin: 1.3rem auto 0; + font-size: clamp(1.05rem, 2vw, 1.25rem); + line-height: 1.7; + color: var(--muted); +} + +.posts-group { + margin-bottom: 3rem; +} + +.posts-group__header { + margin-bottom: 1.1rem; +} + +.posts-group__title { + margin: 0; + font-size: clamp(1.45rem, 2.2vw, 1.9rem); + letter-spacing: -0.03em; +} + +.posts-group__description { + margin: 0.45rem 0 0; + max-width: 48rem; + color: var(--muted); + line-height: 1.7; +} + +.posts-list { + display: grid; + gap: 1.35rem; +} + +.post-card { + --accent-rgb: 96, 165, 250; + --accent-alt-rgb: 37, 99, 235; + display: block; + position: relative; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 24px; + background: linear-gradient(180deg, rgba(17, 24, 39, 0.88), rgba(10, 10, 11, 0.94)); + box-shadow: 0 24px 60px -28px var(--shadow); + cursor: pointer; + transition: + transform 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease, + color 180ms ease; +} + +.post-card::before { + content: ""; + position: absolute; + inset: -30%; + background: + radial-gradient(circle at top right, rgba(var(--accent-rgb), 0.22), transparent 24%), + radial-gradient(circle at bottom left, rgba(var(--accent-alt-rgb), 0.18), transparent 28%); + pointer-events: none; +} + +.post-card:hover, +.post-card:focus-visible { + transform: translateY(-3px); + border-color: rgba(var(--accent-rgb), 0.45); + box-shadow: 0 30px 65px -28px rgba(var(--accent-rgb), 0.22); +} + +.post-card:focus-visible { + outline: 3px solid rgba(var(--accent-rgb), 0.35); + outline-offset: 3px; +} + +.post-card__body { + position: relative; + padding: 1.75rem; +} + +.post-card__media { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; + border-bottom: 1px solid rgba(148, 163, 184, 0.14); + background: rgba(15, 23, 42, 0.8); +} + +.post-card__image { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.post-card--featured .post-card__media { + aspect-ratio: 16 / 8; +} + +.post-card--featured .post-card__body { + padding: 2.3rem; +} + +.post-card__header { + display: flex; + align-items: center; + gap: 0.95rem; + margin-bottom: 1.25rem; +} + +.post-card__icon { + flex: 0 0 auto; + width: 2.75rem; + height: 2.75rem; + border-radius: 0.95rem; + background: linear-gradient(135deg, rgba(var(--accent-rgb), 1), rgba(var(--accent-alt-rgb), 1)); + box-shadow: 0 18px 32px -20px rgba(var(--accent-rgb), 0.85); +} + +.post-card__source, +.post-card__meta { + margin: 0; +} + +.post-card__source { + font-size: 0.95rem; + font-weight: 600; + color: rgb(var(--accent-rgb)); +} + +.post-card__meta { + margin-top: 0.15rem; + color: var(--subtle); + font-size: 0.8rem; +} + +.post-card__title { + margin: 0; + font-size: 1.45rem; + line-height: 1.18; + letter-spacing: -0.03em; + transition: color 180ms ease; +} + +.post-card--featured .post-card__title { + font-size: clamp(1.9rem, 3vw, 2.5rem); +} + +.post-card__description { + margin: 1rem 0 0; + color: var(--muted); + line-height: 1.75; +} + +.post-card__tags { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + margin-top: 1.4rem; +} + +.post-card__tag { + display: inline-flex; + align-items: center; + padding: 0.45rem 0.8rem; + border-radius: 999px; + background: rgba(var(--accent-rgb), 0.14); + color: rgb(var(--accent-rgb)); + font-size: 0.8rem; + font-weight: 600; +} + +.post-card__link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin-top: 1.45rem; + font-weight: 600; + color: rgb(var(--accent-rgb)); +} + +.post-card:hover .post-card__title, +.post-card:focus-visible .post-card__title, +.post-card:hover .post-card__link, +.post-card:focus-visible .post-card__link { + color: rgb(var(--accent-alt-rgb)); +} + +.post-card__link span[aria-hidden="true"] { + transition: transform 180ms ease; +} + +.post-card:hover .post-card__link span[aria-hidden="true"], +.post-card:focus-visible .post-card__link span[aria-hidden="true"] { + transform: translateX(3px); +} + +.post-card--microsoft-open-source-blog { + --accent-rgb: 96, 165, 250; + --accent-alt-rgb: 147, 51, 234; +} + +.post-card--aws-blog { + --accent-rgb: 249, 115, 22; + --accent-alt-rgb: 245, 158, 11; +} + +.post-card--azure-cosmos-db-blog { + --accent-rgb: 168, 85, 247; + --accent-alt-rgb: 59, 130, 246; +} + +.post-card--yugabytedb-blog { + --accent-rgb: 34, 197, 94; + --accent-alt-rgb: 16, 185, 129; +} + +.post-card--documentdb-blog { + --accent-rgb: 14, 165, 233; + --accent-alt-rgb: 59, 130, 246; +} + +.blog-post { + max-width: 840px; +} + +.blog-post__back { + margin: 0 0 1.2rem; +} + +.blog-post__back a { + color: var(--muted); + font-weight: 600; + transition: color 160ms ease; +} + +.blog-post__back a:hover { + color: var(--text); +} + +.blog-post__header, +.blog-post__content-shell { + border: 1px solid var(--border); + border-radius: 28px; + background: linear-gradient(180deg, rgba(17, 24, 39, 0.82), rgba(10, 10, 11, 0.9)); + box-shadow: 0 24px 60px -28px var(--shadow); +} + +.blog-post__header { + padding: 2rem; +} + +.blog-post__eyebrow { + margin: 0; + font-size: 0.92rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #7dd3fc; +} + +.blog-post__title { + margin: 0.75rem 0 0; + font-size: clamp(2.4rem, 5vw, 3.8rem); + line-height: 1.04; + letter-spacing: -0.05em; +} + +.blog-post__meta { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + margin-top: 1rem; + color: var(--subtle); + font-size: 0.95rem; +} + +.blog-post__lead { + margin: 1rem 0 0; + color: var(--muted); + font-size: 1.08rem; + line-height: 1.75; +} + +.blog-post__tags { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + margin-top: 1.35rem; +} + +.blog-post__tag { + display: inline-flex; + align-items: center; + padding: 0.45rem 0.8rem; + border-radius: 999px; + background: rgba(14, 165, 233, 0.14); + color: #7dd3fc; + font-size: 0.8rem; + font-weight: 600; +} + +.blog-post__content-shell { + margin-top: 1.5rem; + padding: 2rem; +} + +.blog-post__hero { + margin-top: 1.5rem; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 28px; + background: rgba(15, 23, 42, 0.85); + box-shadow: 0 24px 60px -28px var(--shadow); +} + +.blog-post__hero-image { + display: block; + width: 100%; + height: auto; +} + +.blog-post__content { + color: #e5e7eb; + line-height: 1.8; + font-size: 1.05rem; +} + +.blog-post__content > :first-child { + margin-top: 0; +} + +.blog-post__content > :last-child { + margin-bottom: 0; +} + +.blog-post__content h2, +.blog-post__content h3, +.blog-post__content h4 { + margin-top: 2.25rem; + margin-bottom: 0.8rem; + line-height: 1.2; + letter-spacing: -0.03em; +} + +.blog-post__content h2 { + font-size: 1.75rem; +} + +.blog-post__content h3 { + font-size: 1.35rem; +} + +.blog-post__content p, +.blog-post__content ul, +.blog-post__content ol, +.blog-post__content blockquote, +.blog-post__content pre, +.blog-post__content table { + margin: 1.2rem 0; +} + +.blog-post__content ul, +.blog-post__content ol { + padding-left: 1.5rem; +} + +.blog-post__content li + li { + margin-top: 0.45rem; +} + +.blog-post__content a { + color: #93c5fd; + text-decoration: underline; + text-decoration-color: rgba(147, 197, 253, 0.35); + text-underline-offset: 0.18em; +} + +.blog-post__content a:hover { + color: #bfdbfe; +} + +.blog-post__content blockquote { + padding: 1rem 1.25rem; + border-left: 4px solid rgba(59, 130, 246, 0.7); + border-radius: 16px; + background: rgba(30, 41, 59, 0.55); + color: #dbeafe; +} + +.blog-post__content pre { + overflow-x: auto; + padding: 1rem 1.15rem; + border-radius: 18px; + border: 1px solid rgba(148, 163, 184, 0.16); + background: #0f172a; +} + +.blog-post__content code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.92em; +} + +.blog-post__content :not(pre) > code { + padding: 0.18rem 0.42rem; + border-radius: 10px; + background: rgba(30, 41, 59, 0.75); +} + +.blog-post__content img { + display: block; + width: 100%; + max-width: 100%; + margin: 1.5rem 0; + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 24px; + background: rgba(15, 23, 42, 0.82); + box-shadow: 0 24px 60px -28px var(--shadow); +} + +.blog-post__content table { + width: 100%; + border-collapse: collapse; +} + +.blog-post__content th, +.blog-post__content td { + padding: 0.75rem; + border-bottom: 1px solid rgba(148, 163, 184, 0.16); + text-align: left; +} + +.blog-post__content hr { + margin: 2rem 0; + border: 0; + border-top: 1px solid rgba(148, 163, 184, 0.16); +} + +.site-footer { + border-top: 1px solid var(--border); + background: rgba(9, 9, 11, 0.9); +} + +.site-footer__inner { + padding: 1.2rem 0 1.4rem; + color: var(--subtle); + font-size: 0.85rem; + line-height: 1.6; + text-align: center; +} + +@media (max-width: 820px) { + .site-header__inner { + flex-direction: column; + align-items: flex-start; + } + + .site-nav { + justify-content: flex-start; + } + + .blog-main { + padding-top: 3rem; + } + + .blog-post__header, + .blog-post__content-shell { + padding: 1.5rem; + } +} + +@media (max-width: 640px) { + .site-header__inner, + .site-footer__inner { + width: min(1100px, calc(100% - 1.5rem)); + } + + .post-card__body, + .post-card--featured .post-card__body { + padding: 1.35rem; + } + + .post-card__header { + align-items: flex-start; + } +} diff --git a/blogs/index.html b/blogs/index.html new file mode 100644 index 0000000..01d3efb --- /dev/null +++ b/blogs/index.html @@ -0,0 +1,65 @@ +--- +title: Latest from our Blog +description: Insights, updates, and deep dives into the world of document databases and open-source innovation. +--- +{% assign featured_local_posts = site.posts | where: "featured", true %} +{% assign featured_external_posts = site.data.posts | where: "featured", true %} +{% assign local_posts = site.posts | where_exp: "post", "post.featured != true" %} +{% assign external_posts = site.data.posts | where_exp: "post", "post.featured != true" %} + +
+

Community insights

+

Latest from our Blog

+

+ Insights, updates, and deep dives into the world of document databases and open-source innovation. +

+
+ +{% if featured_local_posts.size > 0 or featured_external_posts.size > 0 %} +
+
+ +

Highlighted posts and important community announcements.

+
+ +
+ {% for post in featured_local_posts %} + {% include post-card.html post=post featured=true %} + {% endfor %} + + {% for post in featured_external_posts %} + {% include post-card.html post=post featured=true %} + {% endfor %} +
+
+{% endif %} + +{% if local_posts.size > 0 %} +
+
+

Published here

+

Markdown posts maintained directly in this repository and rendered with the same card style as the rest of the blog.

+
+ +
+ {% for post in local_posts %} + {% include post-card.html post=post featured=false %} + {% endfor %} +
+
+{% endif %} + +{% if external_posts.size > 0 %} +
+
+

From around the web

+

Curated partner and community articles hosted outside this repository.

+
+ +
+ {% for post in external_posts %} + {% include post-card.html post=post featured=false %} + {% endfor %} +
+
+{% endif %} diff --git a/package.json b/package.json index 2c332ae..0dfcd18 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "private": true, "scripts": { "dev": "npm run compile && next dev --turbopack", - "build": "npm run compile && next build --turbopack", + "build": "npm run build:next && npm run build:blogs", + "build:next": "npm run compile && next build --turbopack", + "build:blogs": "BUNDLE_FORCE_RUBY_PLATFORM=true bundle exec jekyll build --config blogs/_config.yml --source blogs --destination out/blogs --baseurl \"${JEKYLL_BASE_PATH:-/blogs}\"", "compile": "npm run compile:clean && npm run compile:content", "compile:content": "tsx scripts/compile-content.tsx", "compile:clean": "tsx scripts/compile-clean.tsx", diff --git a/readme.md b/readme.md index 422c9e3..46e9c79 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,13 @@ # DocumentDB Website -A modern website for DocumentDB built with [Next.js](https://nextjs.org/) and [Tailwind CSS](https://tailwindcss.com/). The site features community blog posts and technical documentation, with content automatically pulled from the [documentdb/docs](https://github.com/documentdb/docs) repository during the build process. +A modern website for DocumentDB built with [Next.js](https://nextjs.org/) for the main site and [Jekyll](https://jekyllrb.com/) for the `/blogs/` section. The site features community blog posts and technical documentation, with content automatically pulled from the [documentdb/docs](https://github.com/documentdb/docs) repository during the build process. ## Prerequisites for Development - **[Node.js](https://nodejs.org/)** (*20 or higher*) +- **[Ruby](https://www.ruby-lang.org/)** with **Bundler** (*for the Jekyll-powered blog section*) + - **[Git](https://git-scm.com/)** (*for cloning documentation content*) You can develop locally on any machine with these prerequisites installed, or use [GitHub Codespaces](#develop-in-github-codespaces) for a pre-configured environment. @@ -22,13 +24,26 @@ Get started by cloning and running this repository locally. npm install ``` -1. Start the development server: +1. Install Ruby dependencies: + + ```bash + BUNDLE_FORCE_RUBY_PLATFORM=true bundle install + ``` + +1. Start the Next.js development server: ```bash npm run dev ``` -1. Observe that the site will be available at http://localhost:3000 +1. For a full static preview, including the Jekyll-powered blog section: + + ```bash + npm run build + npm run start + ``` + +1. Observe that the preview site will be available at http://localhost:3000 The first time you run `npm run dev` or `npm run build`, documentation content will be automatically compiled from the [documentdb/docs](https://github.com/documentdb/docs) repository. @@ -40,9 +55,49 @@ We welcome contributions to improve the DocumentDB website, whether it's blog po Blog posts are managed locally in this repository. Contribute directly through a pull request to this repository. -1. Open [blogs/content.yml](blogs/content.yml) +You can publish blog content in two ways: + +1. **Markdown posts hosted in this repo** + + Add a new file under `blogs/_posts/` using the Jekyll naming format: + + ```text + blogs/_posts/YYYY-MM-DD-my-post-title.md + ``` + + Start it with front matter like: + + ```yaml + --- + title: My Blog Post + description: One-line summary used in the blog card. + date: 2026-03-19 + featured: false + author: Your Name + category: documentdb-blog + cover_image: /assets/images/posts/my-post-title/hero.png + cover_image_alt: Short description of the image + tags: + - Example + - Markdown + --- + ``` + + Store post images under: + + ```text + blogs/assets/images/posts/my-post-title/ + ``` + + Then write the post body in Markdown. Jekyll renders the post page and uses the same card layout on the blog index. Images can be referenced directly from Markdown, for example: + + ```md + ![Architecture diagram]({{ '/assets/images/posts/my-post-title/diagram.png' | relative_url }}) + ``` + +1. **Curated external articles** -1. Add your blog post entry following the format of existing posts + Open [blogs/_data/posts.yml](blogs/_data/posts.yml) and add a new entry following the existing YAML format when you want the card to link to an article hosted elsewhere. 1. Submit a pull request for review @@ -100,13 +155,13 @@ The `content.config.json` file controls how documentation is compiled from exter While content is automatically compiled during builds, you can manually trigger these operations during development: ```bash -# Clean all target directories -npm run clean:content +# Build the full static site artifact +npm run build ``` ```bash -# Compile content from sources -npm run compile:content +# Start the static preview server +npm run start ``` ## Develop in GitHub Codespaces