-
-
Notifications
You must be signed in to change notification settings - Fork 644
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
feat: add support for light / dark hero images #280
Conversation
🦋 Changeset detectedLatest commit: 383c376 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for astro-starlight ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
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.
Since there's been quite a few changes to the codebase this PR has a few conflicts. I'm not sure of the best way to communicate those so I've created a diff that I'll include below. With that diff applied and then the suggestion on updating the docs phrasing this one looks good to me 🥳
diff --git a/packages/starlight/components/Hero.astro b/packages/starlight/components/Hero.astro
index 1fccc61..fbf56b8 100644
--- a/packages/starlight/components/Hero.astro
+++ b/packages/starlight/components/Hero.astro
@@ -4,160 +4,146 @@ import { Image } from 'astro:assets';
import CallToAction from './CallToAction.astro';
interface Props {
- fallbackTitle: string;
- hero: NonNullable<CollectionEntry<'docs'>['data']['hero']>;
+ fallbackTitle: string;
+ hero: NonNullable<CollectionEntry<'docs'>['data']['hero']>;
}
-const {
- title = Astro.props.fallbackTitle,
- tagline,
- image,
- actions,
-} = Astro.props.hero;
+const { title = Astro.props.fallbackTitle, tagline, image, actions } = Astro.props.hero;
const imageAttrs = {
- loading: 'eager' as const,
- decoding: 'async' as const,
- width: 400,
- height: 400,
- alt: image?.alt,
+ loading: 'eager' as const,
+ decoding: 'async' as const,
+ width: 400,
+ height: 400,
+ alt: image?.alt,
};
// darkImage is the default and uses either file, dark or raw html
const darkImage = image?.file ? image.file : image?.dark ? image.dark : null;
// lightImage is only used if darkImage is also used
-const lightImage = image?.file ? null : (image?.dark && image?.light) ? image.light : null;
+const lightImage = image?.file ? null : image?.dark && image?.light ? image.light : null;
// rawHtml is only used if darkImage is not used
const rawHtml = !darkImage && image?.html ? image.html : null;
---
<div class="hero">
-
- {
- darkImage && (
- darkImage.format === 'svg' ? (
- <img src={darkImage.src} {...imageAttrs} class:list={{ 'dark-only': Boolean(lightImage) }}/>
- ) : (
- <Image src={darkImage} {...imageAttrs} class:list={{ 'dark-only': Boolean(lightImage) }}/>
- )
- )
- }
- {
- lightImage && (
- lightImage.format === 'svg' ? (
- <img src={lightImage.src} {...imageAttrs} class="light-only"/>
- ) : (
- <Image src={lightImage} {...imageAttrs} class="light-only"/>
- )
- )
- }
-{
- rawHtml && <div class="hero-html flex" set:html={rawHtml} />
-}
- <div class="flex stack">
- <div class="flex copy">
- <h1 id="_top" data-page-title set:html={title} />
- {tagline && <div class="tagline" set:html={tagline} />}
- </div>
- {
- actions.length > 0 && (
- <div class="flex actions">
- {actions.map(({ text, ...attrs }) => (
- <CallToAction {...attrs} set:html={text} />
- ))}
- </div>
- )
- }
- </div>
+ {
+ darkImage &&
+ (darkImage.format === 'svg' ? (
+ <img
+ src={darkImage.src}
+ {...imageAttrs}
+ class:list={{ 'dark-only': Boolean(lightImage) }}
+ />
+ ) : (
+ <Image src={darkImage} {...imageAttrs} class:list={{ 'dark-only': Boolean(lightImage) }} />
+ ))
+ }
+ {
+ lightImage &&
+ (lightImage.format === 'svg' ? (
+ <img src={lightImage.src} {...imageAttrs} class="light-only" />
+ ) : (
+ <Image src={lightImage} {...imageAttrs} class="light-only" />
+ ))
+ }
+ {rawHtml && <div class="hero-html sl-flex" set:html={rawHtml} />}
+ <div class="sl-flex stack">
+ <div class="sl-flex copy">
+ <h1 id="_top" data-page-title set:html={title} />
+ {tagline && <div class="tagline" set:html={tagline} />}
+ </div>
+ {
+ actions.length > 0 && (
+ <div class="sl-flex actions">
+ {actions.map(({ text, ...attrs }) => (
+ <CallToAction {...attrs} set:html={text} />
+ ))}
+ </div>
+ )
+ }
+ </div>
</div>
<style>
- .hero {
- display: grid;
- align-items: center;
- gap: 1rem;
- padding-bottom: 1rem;
- }
-
- .hero > img,
- .hero > .hero-html {
- object-fit: contain;
- width: min(70%, 20rem);
- height: auto;
- margin-inline: auto;
- }
-
- :global([data-theme='light']) .dark-only {
- display: none;
- }
- :global([data-theme='dark']) .light-only {
- display: none;
- }
-
- .stack {
- flex-direction: column;
- gap: clamp(1.5rem, calc(1.5rem + 1vw), 2rem);
- text-align: center;
- }
-
- .copy {
- flex-direction: column;
- gap: 1rem;
- align-items: center;
- }
-
- .copy > * {
- max-width: 50ch;
- }
-
- h1 {
- font-size: clamp(
- var(--sl-text-3xl),
- calc(0.25rem + 5vw),
- var(--sl-text-6xl)
- );
- line-height: var(--sl-line-height-headings);
- font-weight: 600;
- color: var(--sl-color-white);
- }
-
- .tagline {
- font-size: clamp(
- var(--sl-text-base),
- calc(0.0625rem + 2vw),
- var(--sl-text-xl)
- );
- color: var(--sl-color-gray-2);
- }
-
- .actions {
- gap: 1rem 2rem;
- flex-wrap: wrap;
- justify-content: center;
- }
-
- @media (min-width: 50rem) {
- .hero {
- grid-template-columns: 7fr 4fr;
- gap: 3%;
- padding-block: clamp(2.5rem, calc(1rem + 10vmin), 10rem);
- }
-
- .hero > img,
- .hero > .hero-html {
- order: 2;
- width: min(100%, 25rem);
- }
-
- .stack {
- text-align: start;
- }
-
- .copy {
- align-items: flex-start;
- }
-
- .actions {
- justify-content: flex-start;
- }
- }
+ .hero {
+ display: grid;
+ align-items: center;
+ gap: 1rem;
+ padding-bottom: 1rem;
+ }
+
+ .hero > img,
+ .hero > .hero-html {
+ object-fit: contain;
+ width: min(70%, 20rem);
+ height: auto;
+ margin-inline: auto;
+ }
+
+ :global([data-theme='light']) .dark-only {
+ display: none;
+ }
+ :global([data-theme='dark']) .light-only {
+ display: none;
+ }
+
+ .stack {
+ flex-direction: column;
+ gap: clamp(1.5rem, calc(1.5rem + 1vw), 2rem);
+ text-align: center;
+ }
+
+ .copy {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ }
+
+ .copy > * {
+ max-width: 50ch;
+ }
+
+ h1 {
+ font-size: clamp(var(--sl-text-3xl), calc(0.25rem + 5vw), var(--sl-text-6xl));
+ line-height: var(--sl-line-height-headings);
+ font-weight: 600;
+ color: var(--sl-color-white);
+ }
+
+ .tagline {
+ font-size: clamp(var(--sl-text-base), calc(0.0625rem + 2vw), var(--sl-text-xl));
+ color: var(--sl-color-gray-2);
+ }
+
+ .actions {
+ gap: 1rem 2rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ @media (min-width: 50rem) {
+ .hero {
+ grid-template-columns: 7fr 4fr;
+ gap: 3%;
+ padding-block: clamp(2.5rem, calc(1rem + 10vmin), 10rem);
+ }
+
+ .hero > img,
+ .hero > .hero-html {
+ order: 2;
+ width: min(100%, 25rem);
+ }
+
+ .stack {
+ text-align: start;
+ }
+
+ .copy {
+ align-items: flex-start;
+ }
+
+ .actions {
+ justify-content: flex-start;
+ }
+ }
</style>
diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts
index 320b30b..5345c46 100644
--- a/packages/starlight/schema.ts
+++ b/packages/starlight/schema.ts
@@ -1,121 +1,177 @@
-import { z } from "astro/zod";
-import { HeadConfigSchema } from "./schemas/head";
-import { TableOfContentsSchema } from "./schemas/tableOfContents";
-import { Icons } from "./components/Icons";
-export { i18nSchema } from "./schemas/i18n";
+import { z } from 'astro/zod';
+import { HeadConfigSchema } from './schemas/head';
+import { PrevNextLinkConfigSchema } from './schemas/prevNextLink';
+import { TableOfContentsSchema } from './schemas/tableOfContents';
+import { Icons } from './components/Icons';
+import { BadgeConfigSchema } from './schemas/badge';
+export { i18nSchema } from './schemas/i18n';
type IconName = keyof typeof Icons;
const iconNames = Object.keys(Icons) as [IconName, ...IconName[]];
type ImageFunction = () => z.ZodObject<{
- src: z.ZodString;
- width: z.ZodNumber;
- height: z.ZodNumber;
- format: z.ZodUnion<
- [
- z.ZodLiteral<"png">,
- z.ZodLiteral<"jpg">,
- z.ZodLiteral<"jpeg">,
- z.ZodLiteral<"tiff">,
- z.ZodLiteral<"webp">,
- z.ZodLiteral<"gif">,
- z.ZodLiteral<"svg">
- ]
- >;
+ src: z.ZodString;
+ width: z.ZodNumber;
+ height: z.ZodNumber;
+ format: z.ZodUnion<
+ [
+ z.ZodLiteral<'png'>,
+ z.ZodLiteral<'jpg'>,
+ z.ZodLiteral<'jpeg'>,
+ z.ZodLiteral<'tiff'>,
+ z.ZodLiteral<'webp'>,
+ z.ZodLiteral<'gif'>,
+ z.ZodLiteral<'svg'>,
+ ]
+ >;
}>;
export function docsSchema() {
- return ({ image }: { image: ImageFunction }) =>
- z.object({
- /** The title of the current page. Required. */
- title: z.string(),
-
- /**
- * A short description of the current page’s content. Optional, but recommended.
- * A good description is 150–160 characters long and outlines the key content
- * of the page in a clear and engaging way.
- */
- description: z.string().optional(),
-
- /**
- * Custom URL where a reader can edit this page.
- * Overrides the `editLink.baseUrl` global config if set.
- *
- * Can also be set to `false` to disable showing an edit link on this page.
- */
- editUrl: z
- .union([z.string().url(), z.boolean()])
- .optional()
- .default(true),
-
- /** Set custom `<head>` tags just for this page. */
- head: HeadConfigSchema(),
-
- /** Override global table of contents configuration for this page. */
- tableOfContents: TableOfContentsSchema().optional(),
-
- /**
- * Set the layout style for this page.
- * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
- */
- template: z.enum(["doc", "splash"]).default("doc"),
-
- /** Display a hero section on this page. */
- hero: z
- .object({
- /**
- * The large title text to show. If not provided, will default to the top-level `title`.
- * Can include HTML.
- */
- title: z.string().optional(),
- /**
- * A short bit of text about your project.
- * Will be displayed in a smaller size below the title.
- */
- tagline: z.string().optional(),
- /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */
- image: z
- .object({
- /** Alt text for screenreaders and other assistive technologies describing your hero image. */
- alt: z.string().default(""),
- /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */
- file: image().optional(),
- /** Relative path to an image file in your repo to use in dark mode, e.g. `../../assets/hero-dark.png`. */
- dark: image().optional(),
- /** Relative path to an image file in your repo to use in light mode, e.g. `../../assets/hero-light.png`. */
- light: image().optional(),
- /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */
- html: z.string().optional(),
- })
- .optional(),
- /** An array of call-to-action links displayed at the bottom of the hero. */
- actions: z
- .object({
- /** Text label displayed in the link. */
- text: z.string(),
- /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */
- link: z.string(),
- /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */
- variant: z
- .enum(["primary", "secondary", "minimal"])
- .default("minimal"),
- /**
- * An optional icon to display alongside the link text.
- * Can be an inline `<svg>` or the name of one of Starlight’s built-in icons.
- */
- icon: z
- .union([z.enum(iconNames), z.string().startsWith("<svg")])
- .transform((icon) => {
- const parsedIcon = z.enum(iconNames).safeParse(icon);
- return parsedIcon.success
- ? ({ type: "icon", name: parsedIcon.data } as const)
- : ({ type: "raw", html: icon } as const);
- })
- .optional(),
- })
- .array()
- .default([]),
- })
- .optional(),
- });
+ return ({ image }: { image: ImageFunction }) =>
+ z.object({
+ /** The title of the current page. Required. */
+ title: z.string(),
+
+ /**
+ * A short description of the current page’s content. Optional, but recommended.
+ * A good description is 150–160 characters long and outlines the key content
+ * of the page in a clear and engaging way.
+ */
+ description: z.string().optional(),
+
+ /**
+ * Custom URL where a reader can edit this page.
+ * Overrides the `editLink.baseUrl` global config if set.
+ *
+ * Can also be set to `false` to disable showing an edit link on this page.
+ */
+ editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),
+
+ /** Set custom `<head>` tags just for this page. */
+ head: HeadConfigSchema(),
+
+ /** Override global table of contents configuration for this page. */
+ tableOfContents: TableOfContentsSchema().optional(),
+
+ /**
+ * Set the layout style for this page.
+ * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
+ */
+ template: z.enum(['doc', 'splash']).default('doc'),
+
+ /** Display a hero section on this page. */
+ hero: z
+ .object({
+ /**
+ * The large title text to show. If not provided, will default to the top-level `title`.
+ * Can include HTML.
+ */
+ title: z.string().optional(),
+ /**
+ * A short bit of text about your project.
+ * Will be displayed in a smaller size below the title.
+ */
+ tagline: z.string().optional(),
+ /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */
+ /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */
+ image: z
+ .object({
+ /** Alt text for screenreaders and other assistive technologies describing your hero image. */
+ alt: z.string().default(''),
+ /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */
+ file: image().optional(),
+ /** Relative path to an image file in your repo to use in dark mode, e.g. `../../assets/hero-dark.png`. */
+ dark: image().optional(),
+ /** Relative path to an image file in your repo to use in light mode, e.g. `../../assets/hero-light.png`. */
+ light: image().optional(),
+ /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */
+ html: z.string().optional(),
+ })
+ .optional(),
+ /** An array of call-to-action links displayed at the bottom of the hero. */
+ actions: z
+ .object({
+ /** Text label displayed in the link. */
+ text: z.string(),
+ /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */
+ link: z.string(),
+ /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */
+ variant: z.enum(['primary', 'secondary', 'minimal']).default('minimal'),
+ /**
+ * An optional icon to display alongside the link text.
+ * Can be an inline `<svg>` or the name of one of Starlight’s built-in icons.
+ */
+ icon: z
+ .union([z.enum(iconNames), z.string().startsWith('<svg')])
+ .transform((icon) => {
+ const parsedIcon = z.enum(iconNames).safeParse(icon);
+ return parsedIcon.success
+ ? ({ type: 'icon', name: parsedIcon.data } as const)
+ : ({ type: 'raw', html: icon } as const);
+ })
+ .optional(),
+ })
+ .array()
+ .default([]),
+ })
+ .optional(),
+
+ /**
+ * The last update date of the current page.
+ * Overrides the `lastUpdated` global config or the date generated from the Git history.
+ */
+ lastUpdated: z.union([z.date(), z.boolean()]).optional(),
+
+ /**
+ * The previous navigation link configuration.
+ * Overrides the `pagination` global config or the link text and/or URL.
+ */
+ prev: PrevNextLinkConfigSchema(),
+ /**
+ * The next navigation link configuration.
+ * Overrides the `pagination` global config or the link text and/or URL.
+ */
+ next: PrevNextLinkConfigSchema(),
+
+ sidebar: z
+ .object({
+ /**
+ * The order of this page in the navigation.
+ * Pages are sorted by this value in ascending order. Then by slug.
+ * If not provided, pages will be sorted alphabetically by slug.
+ * If two pages have the same order value, they will be sorted alphabetically by slug.
+ */
+ order: z.number().optional(),
+
+ /**
+ * The label for this page in the navigation.
+ * Defaults to the page `title` if not set.
+ */
+ label: z.string().optional(),
+
+ /**
+ * Prevents this page from being included in autogenerated sidebar groups.
+ */
+ hidden: z.boolean().default(false),
+ /**
+ * Adds a badge to the sidebar link.
+ * Can be a string or an object with a variant and text.
+ * Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'.
+ * Passing only a string defaults to the 'default' variant which uses the site accent color.
+ */
+ badge: BadgeConfigSchema(),
+ })
+ .default({}),
+
+ /** Display an announcement banner at the top of this page. */
+ banner: z
+ .object({
+ /** The content of the banner. Supports HTML syntax. */
+ content: z.string(),
+ })
+ .optional(),
+
+ /** Pagefind indexing for this page - set to false to disable. */
+ pagefind: z.boolean().default(true),
+ });
}
Oh, and one last thing I forgot is to mention generating a changeset for this one. |
Co-authored-by: Lorenzo Lewis <[email protected]>
…to latest starlight codebase
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Took a pass at getting this cleaned up so we can finally ship it! Quick summary of the changes I made:
Thanks again for everyone’s patience on this one! |
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.
Amazing update to make it to the finish line. 👏
The code is very clean, I love the stricter schema. Also tested locally and it works great.
I guess we could potentially have a component to render either <Image/>
or <img/>
to avoid some repetition, but it happens only 2 times in a single component so it may be better to keep it as is.
This looks good to me 🚀 (altho I almost feel a bit bad with the new alt
documentation that we do not use it in our index page 😅)
Great work everyone.
Oh, actually, I should double check. I think Edit: Will run a test, but the change I was thinking of released in 2.8.3, so would definitely be in scope: withastro/astro#7643 |
`<Image />` supports SVGs since Astro v2.8.3
Oh, totally missed that one, would be great indeed. |
Update: Yup! |
The only minor feedback I have is that |
Yes, that’s true (also the case with the current hero image support). It was chosen to distinguish |
* main: (22 commits) fix(docs-i18n-tracker): update `translations` import (withastro#1025) [ci] format i18n(zh-cn): Update css-and-tailwind.mdx (withastro#1018) [ci] format i18n(zh-cn): Update authoring-content.md (withastro#1016) i18n(ko-KR): update `configuration.mdx` (withastro#1015) i18n(ko-KR): update `sidebar.mdx` (withastro#1014) i18n(ko-KR): update `i18n.mdx` (withastro#1013) [ci] format i18n(ko-KR): update `frontmatter.md` (withastro#1017) [ci] format i18n(pt-BR): Update `css-and-tailwind.mdx`, `authoring-content.md` and `overrides.md` (withastro#1009) [ci] format [ci] release (withastro#996) Fix Prettier-compatibility of i18n test fixture Refactor translation system to be reusable in non-Astro code (withastro#1003) Add social icons to mobile menu footer (withastro#988) [ci] format Add Galician language support (withastro#1004) feat: add support for light / dark hero images (withastro#280) ...
* Update frontmatter.md #280 * Update frontmatter.md * Update frontmatter.md * improve wording
What kind of changes does this PR include?
Description