-
Notifications
You must be signed in to change notification settings - Fork 274
add MDX heading permalinks for tutorial pages #1403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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; | ||
|
|
||
| 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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
| 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; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -82,6 +90,12 @@ const { showBanner, englishUrl } = checkTranslationBanner( | |
| img: FreeRatioImage, | ||
| pre: PreProxy, | ||
| a: LinkWrapper, | ||
| h1: HeadingPermalinkH1, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. contribution_pages.mp4I’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> | ||
|
|
||
| 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"; |
| 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"; | ||
|
|
||
| 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 { | ||
| & > *, | ||
|
|
@@ -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: "#"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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; | ||
| } | ||
| } | ||

There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good to know, thanks!