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
16 changes: 16 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: 1 addition & 1 deletion src/components/Dropdown/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "/styles/variables.scss";
@use "/styles/variables" as *;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Was this to fix anything in particular? Ideally this change should not need to affect how we import other files, or update the package lock.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Those changes were to address local Sass deprecation warnings about @import being phased out in favor of @use. I updated them while testing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

good to know, thanks!


.container {
position: relative;
Expand Down
62 changes: 62 additions & 0 deletions src/components/HeadingPermalink/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ComponentChildren, HTMLAttributes, VNode } from "preact";

type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";

type HeadingProps = HTMLAttributes<HTMLHeadingElement> & {
as: HeadingTag;
children?: ComponentChildren;
id?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For my understanding, what passes this in? Since it seems to be implicit, we may want to add a comment to explain it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

MDX passes these props automatically when rendering headings. Astro already injects things like id, children, etc. into the heading components, so this just forwards them into our custom component.

};

type HeadingOverrideProps = Omit<HeadingProps, "as">;

const joinClasses = (...classes: unknown[]) =>
classes
.filter((value): value is string => typeof value === "string")
.join(" ");

export const HeadingPermalink = ({
as: Tag,
children,
class: classProp,
className,
id,
...props
}: HeadingProps) => {
const hasPermalink = typeof id === "string" && id.length > 0;

const mergedClassName = joinClasses(
"heading-permalink",
hasPermalink && "heading-permalink--enabled",
classProp,
className,
);

return (
<Tag {...props} id={id} class={mergedClassName}>
{hasPermalink ? (
<a href={`#${id}`} class="heading-permalink__link">
{children}
</a>
) : (
children
)}
</Tag>
);
};

const createHeadingOverride = (as: HeadingTag) => {
const Component = (props: HeadingOverrideProps): VNode => (
<HeadingPermalink {...props} as={as} />
);

Component.displayName = `HeadingPermalink${as.toUpperCase()}`;
return Component;
};

export const HeadingPermalinkH1 = createHeadingOverride("h1");
export const HeadingPermalinkH2 = createHeadingOverride("h2");
export const HeadingPermalinkH3 = createHeadingOverride("h3");
export const HeadingPermalinkH4 = createHeadingOverride("h4");
export const HeadingPermalinkH5 = createHeadingOverride("h5");
export const HeadingPermalinkH6 = createHeadingOverride("h6");
3 changes: 1 addition & 2 deletions src/components/Nav/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@import "/styles/variables.scss";
@import "/styles/global.scss";
@use "/styles/variables" as *;

.container {
height: fit-content;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "/styles/variables.scss";
@use "/styles/variables" as *;

.container {
position: relative;
Expand Down
22 changes: 20 additions & 2 deletions src/layouts/ContributorDocLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import {
type JumpToState,
} from "../globals/state";
import { getCurrentLocale, getUiTranslator } from "../i18n/utils";
import {
HeadingPermalinkH1,
HeadingPermalinkH2,
HeadingPermalinkH3,
HeadingPermalinkH4,
HeadingPermalinkH5,
HeadingPermalinkH6,
} from "@components/HeadingPermalink";

interface Props {
entry: CollectionEntry<"contributor-docs">;
Expand All @@ -18,7 +26,7 @@ const currentLocale = getCurrentLocale(Astro.url.pathname);
const t = await getUiTranslator(currentLocale);

const { entry } = Astro.props;
const { Content, headings } = await entry.render();
const { Content, components, headings } = await entry.render();

const jumpToLinks = headings
.filter((heading: MarkdownHeading) => heading.depth <= 3)
Expand All @@ -45,6 +53,16 @@ setJumpToState(jumpToState);
className="contribute"
>
<section class="rendered-markdown">
<Content />
<Content
components={{
...components,
h1: HeadingPermalinkH1,
h2: HeadingPermalinkH2,
h3: HeadingPermalinkH3,
h4: HeadingPermalinkH4,
h5: HeadingPermalinkH5,
h6: HeadingPermalinkH6,
}}
/>
</section>
</BaseLayout>
2 changes: 1 addition & 1 deletion src/layouts/HomepageLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ setJumpToState(null);
</BaseLayout>

<style lang="scss">
@import "../../styles/variables.scss";
@use "../../styles/variables" as *;
h2 {
@apply mb-sm mt-0;
@media (min-width: $breakpoint-tablet) {
Expand Down
14 changes: 14 additions & 0 deletions src/layouts/TutorialLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import PreProxy from "@components/PreProxy/index.astro";
import CodeBlockWrapper from "@components/CodeBlockWrapper/index.astro";
import LinkWrapper from "@components/LinkWrapper/index.astro";
import RelatedItems from "@components/RelatedItems/index.astro";
import {
HeadingPermalinkH1,
HeadingPermalinkH2,
HeadingPermalinkH3,
HeadingPermalinkH4,
HeadingPermalinkH5,
HeadingPermalinkH6,
} from "@components/HeadingPermalink";
import {
generateJumpToState,
getRelatedEntriesinCollection,
Expand Down Expand Up @@ -82,6 +90,12 @@ const { showBanner, englishUrl } = checkTranslationBanner(
img: FreeRatioImage,
pre: PreProxy,
a: LinkWrapper,
h1: HeadingPermalinkH1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

While the tutorial layout is the main thing I had in mind when raising the issue, are there other pages that would benefit from this too? e.g. probably pages with the contributor docs should also have it. Put another way, should this be the default, maybe with other pages opting out if need be? @ksen0 let me know if you have thoughts there!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I scoped it to tutorials first since that’s what the issue mentioned, but this could definitely be useful for other MDX pages like contributor docs too.

I wasn’t sure if it should be global by default or opt-in per layout, so I kept it limited for now. happy to extend it or move it to a shared layout if that’s preferred.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Makes sense, we can keep it limited at first, but I think at the very least let's do this in the contributor docs pages too to start with.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

contribution_pages.mp4

I’ve extended the heading permalink behavior to contributor docs as well, while keeping it opt-in per layout rather than making it global for all markdown pages.

Tutorials and contributor docs now both use the shared heading permalink component, and the existing contributor docs jump-to links still use Astro’s generated heading slugs. I kept other MDX pages unchanged for now so we can expand deliberately if more pages need it later.

h2: HeadingPermalinkH2,
h3: HeadingPermalinkH3,
h4: HeadingPermalinkH4,
h5: HeadingPermalinkH5,
h6: HeadingPermalinkH6,
}}
/>
</div>
Expand Down
8 changes: 4 additions & 4 deletions styles/base.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "./variables.scss";
@import "./global.scss";
@import "./code-editor.scss";
@import "./markdown.scss";
@use "./variables";
@use "./global";
@use "./code-editor";
@use "./markdown";
2 changes: 1 addition & 1 deletion styles/global.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "variables.scss";
@use "/styles/variables" as *;

@font-face {
font-family: "National Park";
Expand Down
42 changes: 40 additions & 2 deletions styles/markdown.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Default styles to apply to markdown generated content

@import "variables.scss";
@use "variables" as *;

.rendered-markdown {
& > *,
Expand Down Expand Up @@ -108,10 +108,48 @@
}
}

.tutorials .rendered-markdown,
.contribute .rendered-markdown {
.heading-permalink--enabled {
scroll-margin-top: var(--heading-scroll-margin-top, 100px);
}

.heading-permalink__link {
display: inline;
color: inherit;
text-decoration: none;
}

.heading-permalink__link::after {
content: "#";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I suggest using alternative text syntax for the CSS-generated content to specify an empty string. This ensures screen readers do not read the character aloud:

.heading-permalink__link::after {
  content: "#" / "";
}

Browser support is strong across modern engines, excluding legacy IE (see Can I Use). You can verify this by inspecting the accessible name in the browser DevTools Accessibility tab to ensure the "#" symbol is excluded from the calculated link name.

Image

For more details, see Sara Soueidan's article: https://www.sarasoueidan.com/blog/alt-text-for-css-generated-content/

margin-left: 0.22em;
opacity: 0;
color: #d14;
font-weight: normal;
transition:
opacity 0.15s ease-in-out,
color 0.15s ease-in-out;

.dark-theme & {
color: #ff5c8a;
}
}

.heading-permalink__link:hover,
.heading-permalink__link:focus-visible {
text-decoration: underline;
}

.heading-permalink:hover .heading-permalink__link::after,
.heading-permalink__link:focus-visible::after {
opacity: 1;
}
}


html[lang="ko"] .rendered-markdown {
// Korean text needs more line spacing for readability
line-height: 1.7;
// Prevent awkward Korean line breaks
word-break: keep-all;
}
}
Loading