Skip to content
Open
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
2 changes: 2 additions & 0 deletions .rumdl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ disable = [
"MD046",
# for some reason required for tables
"MD058",
# indentations in lists
"MD077",
]
69 changes: 37 additions & 32 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,48 @@ import mdx from "@astrojs/mdx";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
import { defineConfig } from "astro/config";
import expressiveCode from "astro-expressive-code";
import icon from "astro-icon";
import { defineConfig } from "astro/config";
import rehypeExternalLinks from "rehype-external-links";
import remarkEmoji from "remark-emoji";
import remarkHeadingId from "remark-heading-id";

const port = 3000;
const site = process.env.ENV === "production"
? "https://zero-to-nix.com"
: process.env.DEPLOY_PRIME_URL ?? `http://localhost:${port}`;

export default defineConfig({
integrations: [
alpinejs({
entrypoint: "./src/entrypoint",
}),
expressiveCode(),
icon(),
mdx({
//gfm: true,
remarkPlugins: [remarkEmoji, remarkHeadingId],
}),
sitemap(),
tailwind(),
react(),
],
markdown: {
rehypePlugins: [
[
rehypeExternalLinks,
{ rel: ["nofollow noopener noreferrer"], target: "_blank" },
],
],
},
prefetch: {
prefetchAll: true,
defaultStrategy: "hover",
},
server: {
open: true,
port: 3000,
},
site: "https://zero-to-nix.com",
integrations: [
alpinejs({
entrypoint: "./src/entrypoint",
}),
expressiveCode(),
icon(),
mdx({
//gfm: true,
remarkPlugins: [remarkEmoji, remarkHeadingId],
}),
sitemap(),
tailwind(),
react(),
],
markdown: {
rehypePlugins: [
[
rehypeExternalLinks,
{ rel: ["nofollow noopener noreferrer"], target: "_blank" },
],
],
},
prefetch: {
prefetchAll: true,
defaultStrategy: "hover",
},
server: {
open: true,
port,
},
site,
});
10 changes: 5 additions & 5 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = "dist"
# only want to run `npm ci`, which actually uses the `package-lock.json` lockfile.
# Source: https://answers.netlify.com/t/how-can-i-use-npm-ci-instead-of-npm-install/12570/14
NPM_FLAGS = "--version"
NODE_VERSION = "24.13.0" # Keep this in step with `nix develop --command node --version`
NODE_VERSION = "24.14.0" # Keep this in step with `nix develop --command node --version`

[context.production]
environment = { ENV = "production" }
Expand Down
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rehype-external-links": "^3.0.0",
"remark": "^15.0.1",
"remark-emoji": "^5.0.2",
"remark-heading-id": "^1.0.1",
"strip-markdown": "^6.0.0",
"typescript": "^5.9.3"
},
"devDependencies": {
Expand Down
22 changes: 19 additions & 3 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { SEO } from "astro-seo";
import { plainText } from "../lib/utils";
import { site } from "../site";
import Posthog from "./Posthog.astro";

Expand All @@ -8,14 +9,19 @@ type Props = {
description?: string;
tags?: string[];
author?: string;
markdownNegotiation?: boolean;
};

const root = Astro.site!.toString();
const canonical = new URL(Astro.url.pathname, root);

const { title: siteTitle, description: siteDescription } = site;

const { title, description, tags } = Astro.props;
const { title, description, tags, markdownNegotiation = false } = Astro.props;

const plainDescription = description
? (await plainText(description)).trim()
: siteDescription;
Comment on lines +22 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fallback if stripped markdown description is empty.

After markdown removal and trim, the result can be empty; this would emit a blank meta description. Please fallback to siteDescription in that case.

Proposed fix
-const plainDescription = description
-  ? (await plainText(description)).trim()
-  : siteDescription;
+const plainDescription = description
+  ? (await plainText(description)).trim() || siteDescription
+  : siteDescription;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Head.astro` around lines 22 - 24, plainDescription can become
an empty string after await plainText(description) and trim(), which will
produce a blank meta description; change the logic around plainDescription (the
const declaration that uses description, plainText, and siteDescription) to
check the trimmed result and if it's falsy/empty fallback to siteDescription
(i.e., compute const stripped = description ? (await
plainText(description)).trim() : ""; then set const plainDescription = stripped
|| siteDescription) so empty markdown-stripped descriptions use siteDescription
instead of emitting a blank.


const image = `${root}favicon.png`;
---
Expand Down Expand Up @@ -51,7 +57,7 @@ const image = `${root}favicon.png`;
<SEO
charset="utf-8"
title={title ?? siteTitle}
description={description ?? siteDescription}
description={plainDescription}
{canonical}
openGraph={{
basic: {
Expand All @@ -64,10 +70,20 @@ const image = `${root}favicon.png`;
twitter={{
site: root,
title: title ?? siteTitle,
description,
description: plainDescription,
image,
card: "summary",
}}
extend={{
link: markdownNegotiation
? [
{
rel: "alternate",
href: `${canonical.origin}${canonical.pathname.replace(/\/$/, "")}.md`,
},
Comment on lines +80 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add MIME type to the markdown alternate link.

For alternate-representation discovery, include type: "text/markdown" on the link entry.

Proposed fix
           {
             rel: "alternate",
+            type: "text/markdown",
             href: `${canonical.origin}${canonical.pathname.replace(/\/$/, "")}.md`,
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
rel: "alternate",
href: `${canonical.origin}${canonical.pathname.replace(/\/$/, "")}.md`,
},
{
rel: "alternate",
type: "text/markdown",
href: `${canonical.origin}${canonical.pathname.replace(/\/$/, "")}.md`,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Head.astro` around lines 80 - 83, The alternate link object in
Head.astro (the entry with rel: "alternate" and href:
`${canonical.origin}${canonical.pathname.replace(/\/$/, "")}.md`) needs a MIME
type for markdown; add a property `type: "text/markdown"` to that link object so
the alternate-representation discovery explicitly declares the content type.

]
: [],
}}
/>

<link
Expand Down
5 changes: 3 additions & 2 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ import { site } from "../site";
type Props = {
title?: string;
description?: string;
markdownNegotiation?: boolean;
};

const { defaultLanguage } = site;

const { title, description } = Astro.props;
const { title, description, markdownNegotiation = false } = Astro.props;
---

<html
x-data={`{ quickStart: $persist(true), drawer: false, banner: $persist(true), language: $persist("${defaultLanguage}") }`}
class="h-screen"
>
<head>
<Head {title} {description} />
<Head {title} {description} {markdownNegotiation} />
</head>
<body
class="flex min-h-full flex-col bg-white font-sans text-dark antialiased dark:bg-dark dark:text-white"
Expand Down
7 changes: 7 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { marked } from "marked";
import { remark } from "remark";
import strip from "strip-markdown";

export const conceptPagePath = (slug: string): string => {
return pagePath("concepts", slug);
Expand All @@ -12,6 +14,11 @@ export const startPagePath = (slug: string): string => {
return pagePath("start", slug.substring(1));
};

export const plainText = async (md: string): Promise<string> => {
const file = await remark().use(strip).process(md);
return String(file).trim();
};

const pagePath = (collection: string, slug: string): string => {
return `/${collection}/${slug}`;
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/concepts/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const relatedConceptPages: { title: string; href: string }[] = (
});
---

<Layout {title} description={snippet}>
<Layout {title} description={snippet} markdownNegotiation>
<HorizontalContainer>
<Hero
{title}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/start/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const { Content } = await page.render();
const numQuickStartPages = (await getStartPagesByOrderParam()).length;
---

<Layout {title}>
<Layout {title} markdownNegotiation>
<HorizontalContainer>
<Hero
{title}
Expand Down
6 changes: 2 additions & 4 deletions src/pages/start/[slug].md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ export const prerender = true;

type Props = { entry: CollectionEntry<"start"> };

const stripExt = (id: string) => id.replace(/\.(md|mdx)$/i, "");

export const getStaticPaths = (async () => {
const entries = await getCollection("start");
return entries.map((entry) => ({
params: { slug: stripExt(entry.id) },
params: { slug: entry.slug.substring(1) },
props: { entry },
}));
}) satisfies GetStaticPaths;
Expand All @@ -19,7 +17,7 @@ export const GET: APIRoute<Props> = async ({ props, params }) => {
let entry: CollectionEntry<"start"> | undefined = props?.entry;
if (!entry && typeof params.slug === "string") {
const all = await getCollection("start");
entry = all.find((e) => stripExt(e.id) === params.slug);
entry = all.find((e) => e.slug.substring(1) === params.slug);
}
if (!entry) {
return new Response("Not Found", { status: 404 });
Expand Down
Loading