Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ updates:
minor-and-patch:
patterns: ["*"]
update-types: [minor, patch]
ignore:
# eslint-config-next depends on eslint-plugin-react, which caps its eslint
# peer at ^9. Under eslint 10 the plugin throws at rule-load time and
# linting cannot run. Drop this once the plugin supports eslint 10.
- dependency-name: eslint
update-types: [version-update:semver-major]
open-pull-requests-limit: 5
labels:
- dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Dependency Review
uses: actions/dependency-review-action@v5
Expand Down
14 changes: 8 additions & 6 deletions .github/workflows/nextjs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'

- name: Restore Next.js cache
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx', '**.[jt]s') }}
Expand All @@ -58,7 +58,7 @@ jobs:
run: npx tsc --noEmit

- name: Setup uv
uses: astral-sh/setup-uv@v8.2.0
uses: astral-sh/setup-uv@v9.0.0
with:
enable-cache: true

Expand Down Expand Up @@ -90,15 +90,17 @@ jobs:
echo "- **Output:** \`out/\`" >> $GITHUB_STEP_SUMMARY

- name: Upload Pages artifact
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v5
with:
path: out/

deploy:
name: Deploy to GitHub Pages
needs: build
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
# Deploy only from main. workflow_dispatch on main still matches this ref;
# dispatching from any other branch builds but does not publish.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10

Expand Down
6 changes: 4 additions & 2 deletions app/docs/[...mdxPath]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { importPage } from 'nextra/pages'
import { getPageMap } from 'nextra/page-map'
import { useMDXComponents } from 'nextra-theme-docs'
// Aliased on import: this is a nextra factory, not a React hook. Under the
// use* name react-hooks/rules-of-hooks rejects it inside an async component.
import { useMDXComponents as getMDXComponents } from 'nextra-theme-docs'

type PageMapItem = {
route?: string
Expand Down Expand Up @@ -40,7 +42,7 @@ export default async function Page(props: {
const params = await props.params
const result = await importPage(['docs', ...params.mdxPath])
const { default: MDXContent, toc, metadata, ...rest } = result
const { wrapper: Wrapper } = useMDXComponents()
const { wrapper: Wrapper } = getMDXComponents()
return (
<Wrapper toc={toc} metadata={metadata} {...rest}>
<MDXContent {...props} params={params} />
Expand Down
6 changes: 4 additions & 2 deletions app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { importPage } from 'nextra/pages'
import { useMDXComponents } from 'nextra-theme-docs'
// Aliased on import: this is a nextra factory, not a React hook. Under the
// use* name react-hooks/rules-of-hooks rejects it inside an async component.
import { useMDXComponents as getMDXComponents } from 'nextra-theme-docs'

// importPage resolves from the content root, so ['docs'] → content/docs/index.mdx
export async function generateMetadata() {
Expand All @@ -10,7 +12,7 @@ export async function generateMetadata() {
export default async function DocsPage() {
const result = await importPage(['docs'])
const { default: MDXContent, toc, metadata, ...rest } = result
const { wrapper: Wrapper } = useMDXComponents()
const { wrapper: Wrapper } = getMDXComponents()
return (
<Wrapper toc={toc} metadata={metadata} {...rest}>
<MDXContent />
Expand Down
4 changes: 3 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Link from 'next/link';

import { HeroLogo } from '@/components/hero-logo';
import { Terminal } from '@/components/terminal';
import { StandardsBand } from '@/components/standards-band';
Expand All @@ -17,7 +19,7 @@ export default function HomePage() {
covering DevOps, platform engineering, and security.
</p>
<div className="hero-actions">
<a href="/docs/cheatsheets" className="btn-primary">Browse Cheatsheets</a>
<Link href="/docs/cheatsheets" className="btn-primary">Browse Cheatsheets</Link>
<a
href="https://security.libredevops.org"
className="btn-secondary"
Expand Down
7 changes: 4 additions & 3 deletions components/cheatsheet-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import Image from 'next/image';
import { useState } from 'react';

interface Cheatsheet {
Expand Down Expand Up @@ -34,7 +35,7 @@ const AiIcon = (
);

const BicepIcon = (
<img
<Image
src="/assets/bicep-logo.svg"
alt="Bicep"
width={28}
Expand All @@ -44,7 +45,7 @@ const BicepIcon = (
);

const DefenderIcon = (
<img
<Image
src="/assets/defender-logo.svg"
alt="Microsoft Defender"
width={28}
Expand All @@ -65,7 +66,7 @@ const TerminalIcon = (

function DevIcon({ name, alt, width = 28 }: { name: string; alt: string; width?: number }) {
return (
<img
<Image
src={`https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/${name}`}
alt={alt}
width={width}
Expand Down
7 changes: 3 additions & 4 deletions components/docs-logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import Image from 'next/image';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

import { useMounted } from '@/lib/hooks';

export function DocsLogo() {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);

useEffect(() => { setMounted(true); }, []);
const mounted = useMounted();

const wrapperStyle: React.CSSProperties = {
display: 'flex',
Expand Down
7 changes: 3 additions & 4 deletions components/hero-logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import Image from 'next/image';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

import { useMounted } from '@/lib/hooks';

export function HeroLogo() {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);

useEffect(() => { setMounted(true); }, []);
const mounted = useMounted();

if (!mounted) {
// Placeholder matches rendered size to avoid layout shift
Expand Down
36 changes: 17 additions & 19 deletions components/navbar-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
'use client';

import { Eye, EyeOff } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useSyncExternalStore } from 'react';

const STORAGE_KEY = 'docs-banner-hidden';
import {
getBannerHidden,
getBannerHiddenOnServer,
setBannerHidden,
subscribeBannerHidden,
} from '@/lib/banner';

export function NavbarToggle() {
const [hidden, setHidden] = useState(false);
const hidden = useSyncExternalStore(
subscribeBannerHidden,
getBannerHidden,
getBannerHiddenOnServer,
);

// Mirror the preference onto the DOM, which React does not own.
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY) === '1';
setHidden(stored);
document.body.classList.toggle('banner-hidden', stored);
document.body.classList.toggle('banner-hidden', hidden);
// The preload class on <html> was set by an inline script before React
// hydrated; hand control back to the body class now.
document.documentElement.classList.remove('banner-hidden-preload');
Expand All @@ -22,30 +30,20 @@ export function NavbarToggle() {
return () => {
document.body.classList.remove('banner-hidden');
};
}, []);
}, [hidden]);

useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
e.preventDefault();
setHidden(prev => {
const next = !prev;
localStorage.setItem(STORAGE_KEY, next ? '1' : '0');
document.body.classList.toggle('banner-hidden', next);
return next;
});
setBannerHidden(!getBannerHidden());
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);

const toggle = () => {
const next = !hidden;
setHidden(next);
localStorage.setItem(STORAGE_KEY, next ? '1' : '0');
document.body.classList.toggle('banner-hidden', next);
};
const toggle = () => setBannerHidden(!hidden);

return (
<button
Expand Down
20 changes: 9 additions & 11 deletions components/rotating-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { useEffect, useRef, useState } from 'react';

import { useMounted, usePrefersReducedMotion } from '@/lib/hooks';

const words = ['DevOps', 'Platform', 'Security', 'Software', 'AI'];

type Phase = 'typing' | 'hold' | 'deleting';
Expand All @@ -10,20 +12,16 @@ export function RotatingTitle() {
const ref = useRef<HTMLHeadingElement>(null);
// SSR + first paint show the first word fully typed; the typewriter only
// takes over after mount (and only when motion is allowed).
const mounted = useMounted();
const reducedMotion = usePrefersReducedMotion();
const animate = mounted && !reducedMotion;

const [wordIdx, setWordIdx] = useState(0);
const [typed, setTyped] = useState(words[0]);
const [phase, setPhase] = useState<Phase>('hold');
const [animate, setAnimate] = useState(false);
const [typed, setTyped] = useState('');
const [phase, setPhase] = useState<Phase>('typing');
const [visible, setVisible] = useState(true);
const [caretOn, setCaretOn] = useState(true);

useEffect(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
setTyped('');
setPhase('typing');
setAnimate(true);
}, []);

// Pause while the title is scrolled out of view.
useEffect(() => {
const el = ref.current;
Expand Down Expand Up @@ -77,7 +75,7 @@ export function RotatingTitle() {
return (
<h1 className="hero-title" ref={ref}>
<span className="hero-title-rotator">
{typed}
{animate ? typed : words[0]}
{animate && (
<span
className="hero-title-caret"
Expand Down
14 changes: 8 additions & 6 deletions components/standards-band.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';

import Link from 'next/link';

import { ArrowRight } from 'lucide-react';

interface Standard {
Expand Down Expand Up @@ -68,27 +70,27 @@ export function StandardsBand() {

<div className="standards-band-pills">
{standards.map((s) => (
<a key={s.href} href={s.href} className="standards-band-pill">
<Link key={s.href} href={s.href} className="standards-band-pill">
{s.label}
</a>
</Link>
))}
</div>

<a href="/docs/documents" className="standards-band-cta">
<Link href="/docs/documents" className="standards-band-cta">
Explore documents
<ArrowRight size={16} />
</a>
</Link>
</div>
</div>
<p className="standards-band-seealso">
See also the{' '}
<a
<Link
href="/docs/documents/azure-naming-convention"
className="standards-band-seealso-link"
>
Azure Naming Convention
<ArrowRight size={14} />
</a>
</Link>
</p>
</>
);
Expand Down
Loading
Loading