diff --git a/packages/site_shared/analysis_options.yaml b/packages/site_shared/analysis_options.yaml new file mode 100644 index 00000000000..95c3595413a --- /dev/null +++ b/packages/site_shared/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:analysis_defaults/analysis.yaml + +formatter: + trailing_commas: preserve diff --git a/packages/site_shared/build.yaml b/packages/site_shared/build.yaml new file mode 100644 index 00000000000..2f015507221 --- /dev/null +++ b/packages/site_shared/build.yaml @@ -0,0 +1,14 @@ +builders: + stylesHashBuilder: + import: "package:site_shared/src/builders/styles_hash_builder.dart" + builder_factories: ["stylesHashBuilder"] + build_extensions: + "web/assets/css/main.css": + - "lib/src/style_hash.dart" + auto_apply: dependents + build_to: source + required_inputs: + - ".css" + defaults: + dev_options: + fixed_hash: true diff --git a/site/lib/_sass/components/_alert.scss b/packages/site_shared/lib/_sass/_alert.scss similarity index 100% rename from site/lib/_sass/components/_alert.scss rename to packages/site_shared/lib/_sass/_alert.scss diff --git a/site/lib/_sass/components/_breadcrumbs.scss b/packages/site_shared/lib/_sass/_breadcrumbs.scss similarity index 100% rename from site/lib/_sass/components/_breadcrumbs.scss rename to packages/site_shared/lib/_sass/_breadcrumbs.scss diff --git a/site/lib/_sass/components/_button.scss b/packages/site_shared/lib/_sass/_button.scss similarity index 99% rename from site/lib/_sass/components/_button.scss rename to packages/site_shared/lib/_sass/_button.scss index e4df9379615..a1c08059172 100644 --- a/site/lib/_sass/components/_button.scss +++ b/packages/site_shared/lib/_sass/_button.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'base/mixins'; a { color: var(--site-link-fgColor); diff --git a/site/lib/_sass/components/_card.scss b/packages/site_shared/lib/_sass/_card.scss similarity index 99% rename from site/lib/_sass/components/_card.scss rename to packages/site_shared/lib/_sass/_card.scss index 7c9a31b48a8..f82561f6dc3 100644 --- a/site/lib/_sass/components/_card.scss +++ b/packages/site_shared/lib/_sass/_card.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'base/mixins'; .card-list { display: flex; diff --git a/site/lib/_sass/components/_code.scss b/packages/site_shared/lib/_sass/_code.scss similarity index 100% rename from site/lib/_sass/components/_code.scss rename to packages/site_shared/lib/_sass/_code.scss diff --git a/site/lib/_sass/components/_cookie-notice.scss b/packages/site_shared/lib/_sass/_cookie-notice.scss similarity index 100% rename from site/lib/_sass/components/_cookie-notice.scss rename to packages/site_shared/lib/_sass/_cookie-notice.scss diff --git a/site/lib/_sass/components/_dropdown.scss b/packages/site_shared/lib/_sass/_dropdown.scss similarity index 98% rename from site/lib/_sass/components/_dropdown.scss rename to packages/site_shared/lib/_sass/_dropdown.scss index d121e252617..4858b9da31d 100644 --- a/site/lib/_sass/components/_dropdown.scss +++ b/packages/site_shared/lib/_sass/_dropdown.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'base/mixins'; .dropdown { .dropdown-content { diff --git a/packages/site_shared/lib/_sass/_menu-toggle.scss b/packages/site_shared/lib/_sass/_menu-toggle.scss new file mode 100644 index 00000000000..ea0d9618f75 --- /dev/null +++ b/packages/site_shared/lib/_sass/_menu-toggle.scss @@ -0,0 +1,26 @@ +// Toggle between menu and close buttons if sidenav is open or not. +body:not(.sidenav-closed) #menu-toggle { + @media (min-width: 1024px) { + display: none; + } +} + +#menu-toggle span.material-symbols { + &:first-child { + display: inline; + } + + &:last-child { + display: none; + } +} + +body.open_menu #menu-toggle span.material-symbols { + &:first-child { + display: none; + } + + &:last-child { + display: inline; + } +} diff --git a/site/lib/_sass/components/_quiz.scss b/packages/site_shared/lib/_sass/_quiz.scss similarity index 100% rename from site/lib/_sass/components/_quiz.scss rename to packages/site_shared/lib/_sass/_quiz.scss diff --git a/site/lib/_sass/components/_site-switcher.scss b/packages/site_shared/lib/_sass/_site-switcher.scss similarity index 97% rename from site/lib/_sass/components/_site-switcher.scss rename to packages/site_shared/lib/_sass/_site-switcher.scss index 58b3fb0e331..3725a7ff933 100644 --- a/site/lib/_sass/components/_site-switcher.scss +++ b/packages/site_shared/lib/_sass/_site-switcher.scss @@ -1,5 +1,3 @@ -@use '../base/mixins'; - #site-switcher { position: relative; @@ -64,4 +62,4 @@ letter-spacing: normal; } } -} +} \ No newline at end of file diff --git a/site/lib/_sass/components/_stepper.scss b/packages/site_shared/lib/_sass/_stepper.scss similarity index 100% rename from site/lib/_sass/components/_stepper.scss rename to packages/site_shared/lib/_sass/_stepper.scss diff --git a/site/lib/_sass/components/_summary-card.scss b/packages/site_shared/lib/_sass/_summary-card.scss similarity index 100% rename from site/lib/_sass/components/_summary-card.scss rename to packages/site_shared/lib/_sass/_summary-card.scss diff --git a/site/lib/_sass/components/_tabs.scss b/packages/site_shared/lib/_sass/_tabs.scss similarity index 98% rename from site/lib/_sass/components/_tabs.scss rename to packages/site_shared/lib/_sass/_tabs.scss index 17381350e7c..8872ecc973d 100644 --- a/site/lib/_sass/components/_tabs.scss +++ b/packages/site_shared/lib/_sass/_tabs.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'base/mixins'; .tab-pane { display: none; diff --git a/site/lib/_sass/components/_theming.scss b/packages/site_shared/lib/_sass/_theming.scss similarity index 100% rename from site/lib/_sass/components/_theming.scss rename to packages/site_shared/lib/_sass/_theming.scss diff --git a/site/lib/_sass/components/_tooltip.scss b/packages/site_shared/lib/_sass/_tooltip.scss similarity index 100% rename from site/lib/_sass/components/_tooltip.scss rename to packages/site_shared/lib/_sass/_tooltip.scss diff --git a/site/lib/_sass/base/_mixins.scss b/packages/site_shared/lib/_sass/base/_mixins.scss similarity index 100% rename from site/lib/_sass/base/_mixins.scss rename to packages/site_shared/lib/_sass/base/_mixins.scss diff --git a/packages/site_shared/lib/analytics.dart b/packages/site_shared/lib/analytics.dart new file mode 100644 index 00000000000..5c8fecb1023 --- /dev/null +++ b/packages/site_shared/lib/analytics.dart @@ -0,0 +1,21 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'src/analytics/analytics_server.dart' + if (dart.library.js_interop) 'src/analytics/analytics_web.dart'; + +/// Used to report analytic events. +final analytics = AnalyticsImplementation(); + +/// Contains methods for reporting analytics events. +abstract class Analytics { + @protected + void sendEvent(String eventName, Map parameters); + + void sendFeedback(bool helpful) { + sendEvent('feedback', {'feedback_type': helpful ? 'up' : 'down'}); + } +} diff --git a/site/lib/src/components/common/breadcrumbs.dart b/packages/site_shared/lib/common/breadcrumbs.dart similarity index 89% rename from site/lib/src/components/common/breadcrumbs.dart rename to packages/site_shared/lib/common/breadcrumbs.dart index 66d9bb4894f..71c7fb5b923 100644 --- a/site/lib/src/components/common/breadcrumbs.dart +++ b/packages/site_shared/lib/common/breadcrumbs.dart @@ -7,7 +7,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; -import '../../util.dart'; +import '../util.dart'; import 'material_icon.dart'; /// Breadcrumbs navigation component that @@ -18,11 +18,14 @@ import 'material_icon.dart'; /// - https://schema.org/BreadcrumbList /// - https://www.w3.org/TR/wai-aria-practices/examples/breadcrumb/index.html class PageBreadcrumbs extends StatelessComponent { - const PageBreadcrumbs({super.key}); + const PageBreadcrumbs({this.crumbs, super.key}); + + final List? crumbs; @override Component build(BuildContext context) { - final crumbs = _breadcrumbsForPage(context.pages, context.page); + final crumbs = + this.crumbs ?? _breadcrumbsForPage(context.pages, context.page); if (crumbs == null || crumbs.isEmpty) { return const Component.empty(); } @@ -54,7 +57,7 @@ class PageBreadcrumbs extends StatelessComponent { /// /// Uses page metadata to generate breadcrumb titles with fallbacks: /// `breadcrumb` > `shortTitle` > `title`. - List<_BreadcrumbItem>? _breadcrumbsForPage(List pages, Page page) { + List? _breadcrumbsForPage(List pages, Page page) { final pageUrl = page.url; // Only show breadcrumbs if the URL isn't empty. @@ -71,7 +74,7 @@ class PageBreadcrumbs extends StatelessComponent { .toList(growable: false); if (segments.isEmpty) return null; - final breadcrumbs = <_BreadcrumbItem>[]; + final breadcrumbs = []; var currentPath = ''; // Build breadcrumbs for each segment except the current page. @@ -88,7 +91,7 @@ class PageBreadcrumbs extends StatelessComponent { if (indexPage.breadcrumb case final indexBreadcrumb?) { breadcrumbs.add( - _BreadcrumbItem( + BreadcrumbItem( title: indexBreadcrumb, url: indexPage.url, ), @@ -104,7 +107,7 @@ class PageBreadcrumbs extends StatelessComponent { // Add the current page as the final breadcrumb. breadcrumbs.add( - _BreadcrumbItem( + BreadcrumbItem( title: pageBreadcrumb, url: pageUrl, ), @@ -127,8 +130,8 @@ extension on Page { } } -final class _BreadcrumbItem { - const _BreadcrumbItem({required this.title, required this.url}); +final class BreadcrumbItem { + const BreadcrumbItem({required this.title, required this.url}); final String title; final String url; @@ -142,7 +145,7 @@ final class _BreadcrumbItemComponent extends StatelessComponent { required this.isLast, }); - final _BreadcrumbItem crumb; + final BreadcrumbItem crumb; final int index; final bool isLast; diff --git a/site/lib/src/components/common/button.dart b/packages/site_shared/lib/common/button.dart similarity index 85% rename from site/lib/src/components/common/button.dart rename to packages/site_shared/lib/common/button.dart index 740af24cf37..f5c8ea178e6 100644 --- a/site/lib/src/components/common/button.dart +++ b/packages/site_shared/lib/common/button.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../util.dart'; +import '../util.dart'; import 'material_icon.dart'; /// A generic button component with different style variants. @@ -14,6 +14,7 @@ class Button extends StatelessComponent { const Button({ super.key, this.icon, + this.trailingIcon, this.href, this.content, this.style = ButtonStyle.text, @@ -30,6 +31,7 @@ class Button extends StatelessComponent { final String? title; final ButtonStyle style; final String? icon; + final String? trailingIcon; final String? id; final String? href; final Map attributes; @@ -48,7 +50,8 @@ class Button extends StatelessComponent { final mergedClasses = [ style.cssClass, - if (icon != null && content == null) 'icon-button', + if ((icon != null || trailingIcon != null) && content == null) + 'icon-button', ...?classes, ].toClasses; @@ -56,6 +59,7 @@ class Button extends StatelessComponent { if (icon case final iconId?) MaterialIcon(iconId), if (content case final contentText?) asRaw ? RawText(contentText) : .text(contentText), + if (trailingIcon case final iconId?) MaterialIcon(iconId), ]; if (href case final href?) { @@ -90,17 +94,3 @@ enum ButtonStyle { ButtonStyle.text => 'text-button', }; } - -class SegmentedButton extends StatelessComponent { - const SegmentedButton({ - super.key, - required this.children, - }); - - final List children; - - @override - Component build(BuildContext context) { - return span(classes: ['segmented-button'].toClasses, children); - } -} diff --git a/site/lib/src/components/common/card.dart b/packages/site_shared/lib/common/card.dart similarity index 99% rename from site/lib/src/components/common/card.dart rename to packages/site_shared/lib/common/card.dart index 5b1b5eb3200..7ae0782cefe 100644 --- a/site/lib/src/components/common/card.dart +++ b/packages/site_shared/lib/common/card.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../util.dart'; +import '../util.dart'; class Card extends StatelessComponent { /// Creates a card that can have a [header], [content], and [actions]. diff --git a/site/lib/src/components/common/chip.dart b/packages/site_shared/lib/common/chip.dart similarity index 99% rename from site/lib/src/components/common/chip.dart rename to packages/site_shared/lib/common/chip.dart index 41415e59693..a26485988de 100644 --- a/site/lib/src/components/common/chip.dart +++ b/packages/site_shared/lib/common/chip.dart @@ -6,8 +6,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; -import '../../util.dart'; -import '../util/global_event_listener.dart'; +import '../util.dart'; +import '../utils/global_event_listener.dart'; import 'material_icon.dart'; /// A set of Material Design-like chips for configuration. diff --git a/site/lib/src/components/common/client/collapse_button.dart b/packages/site_shared/lib/common/client/collapse_button.dart similarity index 100% rename from site/lib/src/components/common/client/collapse_button.dart rename to packages/site_shared/lib/common/client/collapse_button.dart diff --git a/site/lib/src/components/common/client/cookie_notice.dart b/packages/site_shared/lib/common/client/cookie_notice.dart similarity index 88% rename from site/lib/src/components/common/client/cookie_notice.dart rename to packages/site_shared/lib/common/client/cookie_notice.dart index b4362a5aca0..8bcbb90cf31 100644 --- a/site/lib/src/components/common/client/cookie_notice.dart +++ b/packages/site_shared/lib/common/client/cookie_notice.dart @@ -6,13 +6,20 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; -import '../../../util.dart'; +import '../../util.dart'; import '../button.dart'; /// The cookie banner to show on a user's first time visiting the site. @client final class CookieNotice extends StatefulComponent { - const CookieNotice({super.key}); + const CookieNotice({ + super.key, + required this.host, + this.alwaysDarkMode = false, + }); + + final String host; + final bool alwaysDarkMode; @override State createState() => _CookieNoticeState(); @@ -60,13 +67,16 @@ final class _CookieNoticeState extends State { Component build(BuildContext context) { return section( id: 'cookie-notice', - classes: [if (showNotice) 'show'].toClasses, + classes: [ + if (showNotice) 'show', + if (component.alwaysDarkMode) 'always-dark-mode', + ].toClasses, attributes: {'data-nosnippet': 'true'}, [ div(classes: 'container', [ - const p([ + p([ .text( - 'docs.flutter.dev uses cookies from Google to deliver and ' + '${component.host} uses cookies from Google to deliver and ' 'enhance the quality of its services and to analyze traffic.', ), ]), diff --git a/site/lib/src/components/common/client/copy_button.dart b/packages/site_shared/lib/common/client/copy_button.dart similarity index 56% rename from site/lib/src/components/common/client/copy_button.dart rename to packages/site_shared/lib/common/client/copy_button.dart index fffc9dc0aba..136525e71d4 100644 --- a/site/lib/src/components/common/client/copy_button.dart +++ b/packages/site_shared/lib/common/client/copy_button.dart @@ -11,11 +11,13 @@ import '../button.dart'; class CopyButton extends StatefulComponent { const CopyButton({ this.buttonText, + this.toCopy, this.classes = const [], this.title, }); final String? title; + final String? toCopy; final String? buttonText; final List classes; @@ -32,38 +34,42 @@ class _CopyButtonState extends State { @override void initState() { if (kIsWeb) { - // Extract the code content and unhide the copy button on the client. - context.binding.addPostFrameCallback(() { - setState(() { - final codeElement = buttonKey.currentNode - ?.closest('.code-block-wrapper') - ?.querySelector('pre code') - ?.cloneNode(true); - if (codeElement == null) return; - - // Filter out hidden elements like the terminal sign or folding icons. - final iterator = web.document.createNodeIterator( - codeElement, - /* NodeFilter.SHOW_ELEMENT */ 1, - ); - web.Node? currentNode; - while ((currentNode = iterator.nextNode()) != null) { - final element = currentNode as web.Element; - if (element.getAttribute('aria-hidden') == 'true') { - element.remove(); + if (component.toCopy != null) { + content = component.toCopy; + } else { + // Extract the code content and unhide the copy button on the client. + context.binding.addPostFrameCallback(() { + setState(() { + final codeElement = buttonKey.currentNode + ?.closest('.code-block-wrapper') + ?.querySelector('pre code') + ?.cloneNode(true); + if (codeElement == null) return; + + // Filter out hidden elements like the terminal sign or folding icons. + final iterator = web.document.createNodeIterator( + codeElement, + /* NodeFilter.SHOW_ELEMENT */ 1, + ); + web.Node? currentNode; + while ((currentNode = iterator.nextNode()) != null) { + final element = currentNode as web.Element; + if (element.getAttribute('aria-hidden') == 'true') { + element.remove(); + } } - } - // Remove zero-width spaces - content = codeElement.textContent?.replaceAll('\u200B', ''); - }); + // Remove zero-width spaces + content = codeElement.textContent?.replaceAll('\u200B', ''); + }); - assert( - content != null, - 'CopyButton: Unable to find code content to copy. ' - 'Is the CopyButton inside a code block?', - ); - }); + assert( + content != null, + 'CopyButton: Unable to find code content to copy. ' + 'Is the CopyButton inside a code block?', + ); + }); + } } super.initState(); diff --git a/site/lib/src/components/common/client/download_button.dart b/packages/site_shared/lib/common/client/download_button.dart similarity index 100% rename from site/lib/src/components/common/client/download_button.dart rename to packages/site_shared/lib/common/client/download_button.dart diff --git a/site/lib/src/components/common/client/feedback.dart b/packages/site_shared/lib/common/client/feedback.dart similarity index 98% rename from site/lib/src/components/common/client/feedback.dart rename to packages/site_shared/lib/common/client/feedback.dart index 721bee0d308..543fcdd6ef8 100644 --- a/site/lib/src/components/common/client/feedback.dart +++ b/packages/site_shared/lib/common/client/feedback.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../../analytics/analytics.dart'; +import '../../analytics.dart'; import '../button.dart'; /// Provides the user options to provide feedback on the specified page. @@ -83,7 +83,8 @@ enum _FeedbackState { unhelpful( 'Thank you for your feedback! ' 'Please let us know what we can do to improve.', - ); + ) + ; const _FeedbackState(this.introduction); diff --git a/site/lib/src/components/common/client/on_this_page_button.dart b/packages/site_shared/lib/common/client/on_this_page_button.dart similarity index 100% rename from site/lib/src/components/common/client/on_this_page_button.dart rename to packages/site_shared/lib/common/client/on_this_page_button.dart diff --git a/site/lib/src/components/common/client/page_header_options.dart b/packages/site_shared/lib/common/client/page_header_options.dart similarity index 100% rename from site/lib/src/components/common/client/page_header_options.dart rename to packages/site_shared/lib/common/client/page_header_options.dart diff --git a/site/lib/src/components/common/client/simple_tooltip.dart b/packages/site_shared/lib/common/client/simple_tooltip.dart similarity index 83% rename from site/lib/src/components/common/client/simple_tooltip.dart rename to packages/site_shared/lib/common/client/simple_tooltip.dart index 869527f5de5..12e9f1fba84 100644 --- a/site/lib/src/components/common/client/simple_tooltip.dart +++ b/packages/site_shared/lib/common/client/simple_tooltip.dart @@ -4,7 +4,7 @@ import 'package:jaspr/jaspr.dart'; -import '../../util/component_ref.dart'; +import '../../utils/component_ref.dart'; import '../tooltip.dart'; @client @@ -21,8 +21,8 @@ class SimpleTooltip extends StatelessComponent { @override Component build(BuildContext context) { return Tooltip( - target: target.component, - content: content.component, + target: target, + content: content, ); } } diff --git a/site/lib/src/components/common/dropdown.dart b/packages/site_shared/lib/common/dropdown.dart similarity index 98% rename from site/lib/src/components/common/dropdown.dart rename to packages/site_shared/lib/common/dropdown.dart index bf177204590..3ec74b55727 100644 --- a/site/lib/src/components/common/dropdown.dart +++ b/packages/site_shared/lib/common/dropdown.dart @@ -7,7 +7,7 @@ import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; -import '../util/global_event_listener.dart'; +import '../utils/global_event_listener.dart'; /// A dropdown with a toggle button and expandable content. final class Dropdown extends StatefulComponent { diff --git a/site/lib/src/components/common/fragment_target.dart b/packages/site_shared/lib/common/fragment_target.dart similarity index 100% rename from site/lib/src/components/common/fragment_target.dart rename to packages/site_shared/lib/common/fragment_target.dart diff --git a/site/lib/src/components/common/material_icon.dart b/packages/site_shared/lib/common/material_icon.dart similarity index 98% rename from site/lib/src/components/common/material_icon.dart rename to packages/site_shared/lib/common/material_icon.dart index d4752447d0d..d3569a0522f 100644 --- a/site/lib/src/components/common/material_icon.dart +++ b/packages/site_shared/lib/common/material_icon.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../util.dart'; +import '../util.dart'; /// A Material Symbols icon rendered as a span element. class MaterialIcon extends StatelessComponent { diff --git a/site/lib/src/components/common/search.dart b/packages/site_shared/lib/common/search.dart similarity index 100% rename from site/lib/src/components/common/search.dart rename to packages/site_shared/lib/common/search.dart diff --git a/site/lib/src/components/common/tabs.dart b/packages/site_shared/lib/common/tabs.dart similarity index 99% rename from site/lib/src/components/common/tabs.dart rename to packages/site_shared/lib/common/tabs.dart index 2f9a680ce0d..400e9bd8d5e 100644 --- a/site/lib/src/components/common/tabs.dart +++ b/packages/site_shared/lib/common/tabs.dart @@ -6,7 +6,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; -import '../../util.dart'; +import '../util.dart'; /// A tabs component where children tabs can be switched between by the user. class DashTabs extends CustomComponent { diff --git a/site/lib/src/components/common/tags.dart b/packages/site_shared/lib/common/tags.dart similarity index 100% rename from site/lib/src/components/common/tags.dart rename to packages/site_shared/lib/common/tags.dart diff --git a/site/lib/src/components/common/tooltip.dart b/packages/site_shared/lib/common/tooltip.dart similarity index 98% rename from site/lib/src/components/common/tooltip.dart rename to packages/site_shared/lib/common/tooltip.dart index 932f174ff24..f485b63b31f 100644 --- a/site/lib/src/components/common/tooltip.dart +++ b/packages/site_shared/lib/common/tooltip.dart @@ -6,8 +6,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; -import '../../util.dart'; -import '../util/global_event_listener.dart'; +import '../util.dart'; +import '../utils/global_event_listener.dart'; class Tooltip extends StatefulComponent { const Tooltip({ diff --git a/site/lib/src/components/common/wrapped_code_block.dart b/packages/site_shared/lib/common/wrapped_code_block.dart similarity index 99% rename from site/lib/src/components/common/wrapped_code_block.dart rename to packages/site_shared/lib/common/wrapped_code_block.dart index 3bc7596c140..232143db38f 100644 --- a/site/lib/src/components/common/wrapped_code_block.dart +++ b/packages/site_shared/lib/common/wrapped_code_block.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../util.dart'; +import '../util.dart'; import 'client/collapse_button.dart'; import 'client/copy_button.dart'; import 'material_icon.dart'; diff --git a/site/lib/src/components/common/youtube_embed.dart b/packages/site_shared/lib/common/youtube_embed.dart similarity index 98% rename from site/lib/src/components/common/youtube_embed.dart rename to packages/site_shared/lib/common/youtube_embed.dart index fffba49ec0e..dbec6ef6e02 100644 --- a/site/lib/src/components/common/youtube_embed.dart +++ b/packages/site_shared/lib/common/youtube_embed.dart @@ -6,7 +6,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; -import '../../util.dart'; +import '../util.dart'; class YoutubeEmbed with CustomComponentBase { const YoutubeEmbed(); diff --git a/site/lib/src/components/dartpad/dartpad_injector.dart b/packages/site_shared/lib/dartpad/dartpad_injector.dart similarity index 90% rename from site/lib/src/components/dartpad/dartpad_injector.dart rename to packages/site_shared/lib/dartpad/dartpad_injector.dart index 95c0981d579..a560b12cbcc 100644 --- a/site/lib/src/components/dartpad/dartpad_injector.dart +++ b/packages/site_shared/lib/dartpad/dartpad_injector.dart @@ -4,7 +4,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../util/retake_element.dart'; + +import '../src/utils/retake_element.dart'; import 'embedded_dartpad.dart'; /// Prepares a code block that will be replaced with an embedded @@ -79,16 +80,7 @@ class _DartPadInjectorState extends State { if (kIsWeb) { // During hydration, extract the content from the pre-rendered code block. - final elem = retakeElement(context, (elem) { - return elem.tagName.toLowerCase() == 'pre'; - }); - - if (elem == null) { - content = ''; - } else { - elem.parentNode?.removeChild(elem); - content = elem.textContent ?? ''; - } + content = extractContent(context as Element); } } diff --git a/site/lib/src/components/dartpad/embedded_dartpad.dart b/packages/site_shared/lib/dartpad/embedded_dartpad.dart similarity index 100% rename from site/lib/src/components/dartpad/embedded_dartpad.dart rename to packages/site_shared/lib/dartpad/embedded_dartpad.dart diff --git a/site/lib/src/extensions/code_block_processor.dart b/packages/site_shared/lib/extensions/code_block_processor.dart similarity index 98% rename from site/lib/src/extensions/code_block_processor.dart rename to packages/site_shared/lib/extensions/code_block_processor.dart index 64aa04e2074..79dc29089eb 100644 --- a/site/lib/src/extensions/code_block_processor.dart +++ b/packages/site_shared/lib/extensions/code_block_processor.dart @@ -11,17 +11,19 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:meta/meta.dart'; import 'package:opal/opal.dart' as opal; -import '../components/common/wrapped_code_block.dart'; -import '../components/dartpad/dartpad_injector.dart'; -import '../highlight/theme/dark.dart'; -import '../highlight/theme/light.dart'; -import '../highlight/token_renderer.dart' as highlighter; +import '../common/wrapped_code_block.dart'; +import '../dartpad/dartpad_injector.dart'; +import '../src/highlight/theme/dark.dart'; +import '../src/highlight/theme/light.dart'; +import '../src/highlight/token_renderer.dart' as highlighter; final class CodeBlockProcessor implements PageExtension { static final opal.LanguageRegistry _languageRegistry = opal.LanguageRegistry.withDefaults(); - const CodeBlockProcessor(); + const CodeBlockProcessor({required this.defaultTitle}); + + final String defaultTitle; @override Future> apply(Page page, List nodes) async { @@ -55,7 +57,7 @@ final class CodeBlockProcessor implements PageExtension { return ComponentNode( DartPadWrapper( content: lines.join('\n'), - title: title ?? 'Runnable Flutter example', + title: title ?? defaultTitle, theme: metadata['theme'], height: metadata['height'], runAutomatically: metadata['run'] == 'true', diff --git a/site/lib/src/components/layout/menu_toggle.dart b/packages/site_shared/lib/layout/menu_toggle.dart similarity index 100% rename from site/lib/src/components/layout/menu_toggle.dart rename to packages/site_shared/lib/layout/menu_toggle.dart diff --git a/site/lib/src/components/layout/site_switcher.dart b/packages/site_shared/lib/layout/site_switcher.dart similarity index 72% rename from site/lib/src/components/layout/site_switcher.dart rename to packages/site_shared/lib/layout/site_switcher.dart index 811a308957e..d0fceb87cc4 100644 --- a/site/lib/src/components/layout/site_switcher.dart +++ b/packages/site_shared/lib/layout/site_switcher.dart @@ -5,26 +5,28 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../util.dart'; import '../common/button.dart'; import '../common/dropdown.dart'; +import '../util.dart'; @client final class SiteSwitcher extends StatelessComponent { - const SiteSwitcher(); + const SiteSwitcher({this.isFlutter = true, super.key}); + + final bool isFlutter; @override Component build(BuildContext _) { - return const Dropdown( + return Dropdown( id: 'site-switcher', - toggle: Button(icon: 'apps', title: 'Visit related sites.'), + toggle: const Button(icon: 'apps', title: 'Visit related sites.'), content: nav( classes: 'dropdown-menu', attributes: {'role': 'menu'}, [ - ul( - [ - _SiteWordMarkListEntry( + ul([ + if (isFlutter) ...[ + const _SiteWordMarkListEntry( name: 'Flutter', href: 'https://flutter.dev', ), @@ -32,40 +34,47 @@ final class SiteSwitcher extends StatelessComponent { name: 'Flutter', subtype: 'Docs', href: '/', - current: true, + current: isFlutter, ), - _SiteWordMarkListEntry( + const _SiteWordMarkListEntry( name: 'Flutter', subtype: 'API', href: 'https://api.flutter.dev', ), - _SiteWordMarkListEntry( + const _SiteWordMarkListEntry( name: 'Flutter', subtype: 'Blog', href: 'https://blog.flutter.dev', ), - Component.element( + const Component.element( tag: 'li', classes: 'dropdown-divider', attributes: {'aria-hidden': 'true', 'role': 'separator'}, ), - _SiteWordMarkListEntry( + ], + _SiteWordMarkListEntry( + name: 'Dart', + href: 'https://dart.dev', + dart: true, + current: !isFlutter, + ), + if (!isFlutter) + const _SiteWordMarkListEntry( name: 'Dart', - href: 'https://dart.dev', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'DartPad', - href: 'https://dartpad.dev', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'pub.dev', - href: 'https://pub.dev', - dart: true, + subtype: 'API', + href: 'https://api.dart.dev', ), - ], - ), + const _SiteWordMarkListEntry( + name: 'DartPad', + href: 'https://dartpad.dev', + dart: true, + ), + const _SiteWordMarkListEntry( + name: 'pub.dev', + href: 'https://pub.dev', + dart: true, + ), + ]), ], ), ); diff --git a/site/lib/src/components/layout/theme_switcher.dart b/packages/site_shared/lib/layout/theme_switcher.dart similarity index 99% rename from site/lib/src/components/layout/theme_switcher.dart rename to packages/site_shared/lib/layout/theme_switcher.dart index a06e647c477..5674bc86d33 100644 --- a/site/lib/src/components/layout/theme_switcher.dart +++ b/packages/site_shared/lib/layout/theme_switcher.dart @@ -21,7 +21,8 @@ final class ThemeSwitcher extends StatefulComponent { enum _Theme { light('Light', 'Switch to the light theme.', 'light_mode'), dark('Dark', 'Switch to the dark theme.', 'dark_mode'), - auto('Automatic', 'Match theme to device theme.', 'night_sight_auto'); + auto('Automatic', 'Match theme to device theme.', 'night_sight_auto') + ; final String label; final String description; diff --git a/site/lib/src/markdown/alert_syntax.dart b/packages/site_shared/lib/markdown/alert_syntax.dart similarity index 100% rename from site/lib/src/markdown/alert_syntax.dart rename to packages/site_shared/lib/markdown/alert_syntax.dart diff --git a/site/lib/src/markdown/attribute_syntax.dart b/packages/site_shared/lib/markdown/attribute_syntax.dart similarity index 100% rename from site/lib/src/markdown/attribute_syntax.dart rename to packages/site_shared/lib/markdown/attribute_syntax.dart diff --git a/site/lib/src/markdown/fenced_code_block_syntax.dart b/packages/site_shared/lib/markdown/fenced_code_block_syntax.dart similarity index 100% rename from site/lib/src/markdown/fenced_code_block_syntax.dart rename to packages/site_shared/lib/markdown/fenced_code_block_syntax.dart diff --git a/site/lib/src/markdown/header_syntax.dart b/packages/site_shared/lib/markdown/header_syntax.dart similarity index 100% rename from site/lib/src/markdown/header_syntax.dart rename to packages/site_shared/lib/markdown/header_syntax.dart diff --git a/site/lib/src/markdown/markdown_parser.dart b/packages/site_shared/lib/markdown/markdown_parser.dart similarity index 98% rename from site/lib/src/markdown/markdown_parser.dart rename to packages/site_shared/lib/markdown/markdown_parser.dart index ea8bead4e26..05d44b53b51 100644 --- a/site/lib/src/markdown/markdown_parser.dart +++ b/packages/site_shared/lib/markdown/markdown_parser.dart @@ -14,7 +14,6 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:markdown/markdown.dart' as md; import 'package:markdown_description_list/markdown_description_list.dart'; -import '../extensions/registry.dart'; import 'alert_syntax.dart'; import 'attribute_syntax.dart'; import 'fenced_code_block_syntax.dart'; @@ -65,7 +64,7 @@ class DashMarkdown extends AsyncStatelessComponent { ? _defaultMarkdownDocument.parseInline(content) : _defaultMarkdownDocument.parse(content); var nodes = DashMarkdownParser.buildNodes(markdownNodes); - for (final extension in allNodeProcessingExtensions) { + for (final extension in currentPage.config.extensions) { nodes = await extension.apply(currentPage, nodes); } diff --git a/packages/site_shared/lib/src/analytics/analytics_server.dart b/packages/site_shared/lib/src/analytics/analytics_server.dart new file mode 100644 index 00000000000..42072ca7a68 --- /dev/null +++ b/packages/site_shared/lib/src/analytics/analytics_server.dart @@ -0,0 +1,18 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import '../../analytics.dart'; + +/// Server implementation of [Analytics]. +/// +/// Don't use directly. Access through [analytics] instead. +@internal +final class AnalyticsImplementation extends Analytics { + @override + void sendEvent(String eventName, Map parameters) { + // Ignore on the server. + } +} diff --git a/packages/site_shared/lib/src/analytics/analytics_web.dart b/packages/site_shared/lib/src/analytics/analytics_web.dart new file mode 100644 index 00000000000..a68ff944c7b --- /dev/null +++ b/packages/site_shared/lib/src/analytics/analytics_web.dart @@ -0,0 +1,32 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:universal_web/js_interop.dart'; +import 'package:universal_web/web.dart' as web; + +import '../../analytics.dart'; +import '../../util.dart'; + +/// Web implementation of [Analytics]. +/// +/// Don't use directly. Access through [analytics] instead. +@internal +final class AnalyticsImplementation extends Analytics { + @override + void sendEvent(String eventName, Map parameters) { + if (!productionBuild) { + return; + } + final dataLayer = web.window['dataLayer']; + if (dataLayer.isA()) { + (dataLayer as JSArray).toDart.add( + { + 'event': eventName, + ...parameters, + }.jsify(), + ); + } + } +} diff --git a/site/lib/src/builders/styles_hash_builder.dart b/packages/site_shared/lib/src/builders/styles_hash_builder.dart similarity index 89% rename from site/lib/src/builders/styles_hash_builder.dart rename to packages/site_shared/lib/src/builders/styles_hash_builder.dart index 1d1c0bfbf07..a71332fd03d 100644 --- a/site/lib/src/builders/styles_hash_builder.dart +++ b/packages/site_shared/lib/src/builders/styles_hash_builder.dart @@ -6,6 +6,8 @@ import 'dart:convert'; import 'package:build/build.dart'; import 'package:crypto/crypto.dart'; +Builder stylesHashBuilder(BuilderOptions options) => StylesHashBuilder(options); + class StylesHashBuilder implements Builder { const StylesHashBuilder(this.options); @@ -31,7 +33,7 @@ class StylesHashBuilder implements Builder { final outputContent = """ -// Generated by docs_flutter_dev_site|stylesHashBuilder. Do not edit. +// Generated by site_shared|stylesHashBuilder. Do not edit. // dart format off /// The generated hash of the `main.css` file. diff --git a/site/lib/src/highlight/theme/dark.dart b/packages/site_shared/lib/src/highlight/theme/dark.dart similarity index 100% rename from site/lib/src/highlight/theme/dark.dart rename to packages/site_shared/lib/src/highlight/theme/dark.dart diff --git a/site/lib/src/highlight/theme/light.dart b/packages/site_shared/lib/src/highlight/theme/light.dart similarity index 100% rename from site/lib/src/highlight/theme/light.dart rename to packages/site_shared/lib/src/highlight/theme/light.dart diff --git a/site/lib/src/highlight/token_renderer.dart b/packages/site_shared/lib/src/highlight/token_renderer.dart similarity index 100% rename from site/lib/src/highlight/token_renderer.dart rename to packages/site_shared/lib/src/highlight/token_renderer.dart diff --git a/site/lib/src/components/util/retake_element.dart b/packages/site_shared/lib/src/utils/retake_element.dart similarity index 100% rename from site/lib/src/components/util/retake_element.dart rename to packages/site_shared/lib/src/utils/retake_element.dart diff --git a/packages/site_shared/lib/src/utils/retake_element_vm.dart b/packages/site_shared/lib/src/utils/retake_element_vm.dart new file mode 100644 index 00000000000..3e0eae320fa --- /dev/null +++ b/packages/site_shared/lib/src/utils/retake_element_vm.dart @@ -0,0 +1,7 @@ +import 'package:jaspr/jaspr.dart'; + +Component retakeRef(BuildContext context, String id, [String? selector]) { + throw UnimplementedError(); +} + +String extractContent(BuildContext context) => ''; diff --git a/site/lib/src/components/util/retake_element_web.dart b/packages/site_shared/lib/src/utils/retake_element_web.dart similarity index 59% rename from site/lib/src/components/util/retake_element_web.dart rename to packages/site_shared/lib/src/utils/retake_element_web.dart index 36a1694baac..5631e5ac73f 100644 --- a/site/lib/src/components/util/retake_element_web.dart +++ b/packages/site_shared/lib/src/utils/retake_element_web.dart @@ -5,23 +5,54 @@ import 'package:jaspr/client.dart'; // ignore: implementation_imports import 'package:jaspr/src/dom/type_checks.dart'; +import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; -/// Retakes the element matching [predicate] during hydration. -web.Element? retakeElement( - BuildContext context, - bool Function(web.Element element) predicate, -) { +/// Retakes the element with the specified [id] ref during hydration. +Component retakeRef(BuildContext context, String id) { final r = (context as Element).parentRenderObjectElement?.renderObject; - if (r == null) return null; - final node = (r as DomRenderObject).retakeNode((node) { - return node.isElement && predicate(node as web.Element); + if (r == null) return const .empty(); + + var node = (r as DomRenderObject).retakeNode((node) { + return node.isComment && (node as web.Comment).data.startsWith('ref:$id'); }); - return node as web.Element?; + + if (node == null) return const .empty(); + + final nodes = [node]; + + node = node.nextSibling; + while (node != null) { + r.retakeNode((n) => n == node); + nodes.add(node); + + if (node.isComment && (node as web.Comment).data.startsWith('/ref:$id')) { + break; + } + + node = node.nextSibling; + } + + return .fragment([ + for (final node in nodes) RawNode(node), + ]); } -Component wrapNode(web.Node node) { - return RawNode(node); +/// Extracts the content of a
 block inside the given
+/// [element] during hydration.
+String extractContent(Element element) {
+  final r = element.parentRenderObjectElement?.renderObject as DomRenderObject?;
+  if (r == null) return '';
+
+  final code = r.retakeNode((node) {
+    return node.instanceOfString('Element') &&
+        (node as web.Element).tagName.toLowerCase() == 'pre';
+  });
+
+  if (code == null) return '';
+
+  code.parentNode?.removeChild(code);
+  return (code as web.Element).textContent ?? '';
 }
 
 class RawNode extends Component {
diff --git a/site/lib/src/components/tutorial/client/progress_ring.dart b/packages/site_shared/lib/tutorial/client/progress_ring.dart
similarity index 100%
rename from site/lib/src/components/tutorial/client/progress_ring.dart
rename to packages/site_shared/lib/tutorial/client/progress_ring.dart
diff --git a/site/lib/src/components/tutorial/client/quiz.dart b/packages/site_shared/lib/tutorial/client/quiz.dart
similarity index 95%
rename from site/lib/src/components/tutorial/client/quiz.dart
rename to packages/site_shared/lib/tutorial/client/quiz.dart
index f04b044568f..cbc939217ac 100644
--- a/site/lib/src/components/tutorial/client/quiz.dart
+++ b/packages/site_shared/lib/tutorial/client/quiz.dart
@@ -6,9 +6,9 @@ import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:universal_web/web.dart' as web;
 
-import '../../../models/quiz_model.dart';
-import '../../../util.dart';
 import '../../common/button.dart';
+import '../models/quiz_model.dart';
+import '../../util.dart';
 
 @client
 class InteractiveQuiz extends StatefulComponent {
@@ -89,7 +89,7 @@ class _InteractiveQuizState extends State {
             if (question == currentQuestion) 'active',
           ].toClasses,
           [
-            strong([.text(question.question)]),
+            strong([RawText(question.question)]),
             ol([
               for (final (index, option) in question.options.indexed)
                 li(
@@ -113,14 +113,14 @@ class _InteractiveQuizState extends State {
                   [
                     div(classes: 'question-wrapper', [
                       div(classes: 'question', [
-                        p([.text(option.text)]),
+                        p([RawText(option.text)]),
                       ]),
                       div(classes: 'solution', [
                         if (option.correct)
                           const p(classes: 'correct', [.text('That\'s right!')])
                         else
-                          const p(classes: 'incorrect', [.text('Not quite')]),
-                        p([.text(option.explanation)]),
+                          const p(classes: 'incorrect', [.text('Not quite.')]),
+                        p([RawText(option.explanation)]),
                       ]),
                     ]),
                   ],
@@ -144,7 +144,7 @@ class _InteractiveQuizState extends State {
               currentQuestionIndex--;
             });
           },
-          content: 'Previous',
+          content: 'Previous question',
         ),
         Button(
           key: nextButtonKey,
diff --git a/site/lib/src/components/tutorial/downloadable_snippet.dart b/packages/site_shared/lib/tutorial/downloadable_snippet.dart
similarity index 90%
rename from site/lib/src/components/tutorial/downloadable_snippet.dart
rename to packages/site_shared/lib/tutorial/downloadable_snippet.dart
index b11391a25b3..07818128e31 100644
--- a/site/lib/src/components/tutorial/downloadable_snippet.dart
+++ b/packages/site_shared/lib/tutorial/downloadable_snippet.dart
@@ -5,15 +5,17 @@
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 import 'package:path/path.dart' as path;
-
-import '../../extensions/code_block_processor.dart';
-import '../../util.dart';
 import '../common/client/copy_button.dart';
 import '../common/client/download_button.dart';
 import '../common/wrapped_code_block.dart';
+import '../extensions/code_block_processor.dart';
 
 class DownloadableSnippet extends CustomComponentBase {
-  const DownloadableSnippet();
+  const DownloadableSnippet({
+    required this.snippetsDirectoryPath,
+  });
+
+  final String snippetsDirectoryPath;
 
   @override
   Pattern get pattern => 'DownloadableSnippet';
@@ -33,7 +35,7 @@ class DownloadableSnippet extends CustomComponentBase {
       builder: (context) {
         final page = context.page;
         final snippet = page.loader.readPartialSync(
-          path.join(siteSrcDirectoryPath, '_snippets', src),
+          path.join(snippetsDirectoryPath, src),
           page,
         );
         final language = src.split('.').last;
diff --git a/site/lib/src/models/quiz_model.dart b/packages/site_shared/lib/tutorial/models/quiz_model.dart
similarity index 100%
rename from site/lib/src/models/quiz_model.dart
rename to packages/site_shared/lib/tutorial/models/quiz_model.dart
diff --git a/site/lib/src/models/summary_card_model.dart b/packages/site_shared/lib/tutorial/models/summary_card_model.dart
similarity index 100%
rename from site/lib/src/models/summary_card_model.dart
rename to packages/site_shared/lib/tutorial/models/summary_card_model.dart
diff --git a/site/lib/src/models/tutorial_model.dart b/packages/site_shared/lib/tutorial/models/tutorial_model.dart
similarity index 100%
rename from site/lib/src/models/tutorial_model.dart
rename to packages/site_shared/lib/tutorial/models/tutorial_model.dart
diff --git a/site/lib/src/components/tutorial/progress_ring.dart b/packages/site_shared/lib/tutorial/progress_ring.dart
similarity index 100%
rename from site/lib/src/components/tutorial/progress_ring.dart
rename to packages/site_shared/lib/tutorial/progress_ring.dart
diff --git a/packages/site_shared/lib/tutorial/quiz.dart b/packages/site_shared/lib/tutorial/quiz.dart
new file mode 100644
index 00000000000..7ce2770c0c5
--- /dev/null
+++ b/packages/site_shared/lib/tutorial/quiz.dart
@@ -0,0 +1,73 @@
+// Copyright 2025 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_content/jaspr_content.dart';
+import 'package:yaml/yaml.dart';
+
+import '../markdown/markdown_parser.dart';
+import 'client/quiz.dart';
+import 'models/quiz_model.dart';
+
+class Quiz extends CustomComponent {
+  const Quiz() : super.base();
+
+  @override
+  Component? create(Node node, NodesBuilder builder) {
+    if (node is! ElementNode || node.tag.toLowerCase() != 'quiz') {
+      return null;
+    }
+
+    final title = node.attributes['title'];
+
+    // If the quiz has an ID, load it from the page data.
+    if (node.attributes['id'] case final String quizId when quizId.isNotEmpty) {
+      return Builder(
+        builder: (context) {
+          final quizzes = context.page.data['quiz'] as Map?;
+          if (quizzes?[quizId] case final List quizData) {
+            return InteractiveQuiz(
+              title: title,
+              questions: quizData
+                  .map((q) => _parseQuestion(q as Map))
+                  .toList(growable: false),
+            );
+          }
+
+          throw ArgumentError('Failed to parse quiz with ID: $quizId');
+        },
+      );
+    }
+
+    // If the quiz does not have an ID, parse it from the content.
+    if (node.children?.whereType().isNotEmpty ?? false) {
+      throw Exception(
+        'Invalid Quiz content. Remove any leading empty lines to '
+        'avoid parsing as markdown.',
+      );
+    }
+
+    final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
+    final data = loadYamlNode(content);
+    assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.');
+    final questions = (data as YamlList).nodes
+        .map((n) => Question.fromMap(n as YamlMap))
+        .toList();
+    assert(questions.isNotEmpty, 'Quiz must contain at least one question.');
+    return InteractiveQuiz(title: title, questions: questions);
+  }
+}
+
+Question _parseQuestion(Map map) => Question(
+  parseMarkdownToHtml(map['question'] as String, inline: true),
+  (map['options'] as List)
+      .map((e) => _parseAnswer(e as Map))
+      .toList(),
+);
+
+AnswerOption _parseAnswer(Map map) => AnswerOption(
+  parseMarkdownToHtml(map['text'] as String, inline: true),
+  map['correct'] as bool? ?? false,
+  parseMarkdownToHtml(map['explanation'] as String),
+);
diff --git a/site/lib/src/components/tutorial/stepper.dart b/packages/site_shared/lib/tutorial/stepper.dart
similarity index 96%
rename from site/lib/src/components/tutorial/stepper.dart
rename to packages/site_shared/lib/tutorial/stepper.dart
index e9ccfdf43e3..0e7400eac54 100644
--- a/site/lib/src/components/tutorial/stepper.dart
+++ b/packages/site_shared/lib/tutorial/stepper.dart
@@ -5,9 +5,8 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../common/button.dart';
-import '../common/material_icon.dart';
+import 'package:site_shared/common/button.dart';
+import 'package:site_shared/common/material_icon.dart';
 
 class Stepper extends CustomComponent {
   const Stepper() : super.base();
diff --git a/site/lib/src/components/tutorial/summary_card.dart b/packages/site_shared/lib/tutorial/summary_card.dart
similarity index 97%
rename from site/lib/src/components/tutorial/summary_card.dart
rename to packages/site_shared/lib/tutorial/summary_card.dart
index 76b8a2e7dd1..54e421c8043 100644
--- a/site/lib/src/components/tutorial/summary_card.dart
+++ b/packages/site_shared/lib/tutorial/summary_card.dart
@@ -7,9 +7,9 @@ import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 import 'package:yaml/yaml.dart';
 
-import '../../markdown/markdown_parser.dart';
-import '../../models/summary_card_model.dart';
 import '../common/material_icon.dart';
+import '../markdown/markdown_parser.dart';
+import 'models/summary_card_model.dart';
 
 class SummaryCard extends CustomComponent {
   const SummaryCard() : super.base();
diff --git a/site/lib/src/components/tutorial/tutorial_outline.dart b/packages/site_shared/lib/tutorial/tutorial_outline.dart
similarity index 58%
rename from site/lib/src/components/tutorial/tutorial_outline.dart
rename to packages/site_shared/lib/tutorial/tutorial_outline.dart
index 672680636f8..dfd9a8801f8 100644
--- a/site/lib/src/components/tutorial/tutorial_outline.dart
+++ b/packages/site_shared/lib/tutorial/tutorial_outline.dart
@@ -6,11 +6,13 @@ import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 
-import '../../markdown/markdown_parser.dart';
-import '../../models/tutorial_model.dart';
+import '../markdown/markdown_parser.dart';
+import 'models/tutorial_model.dart';
 
 class TutorialOutline extends CustomComponentBase {
-  const TutorialOutline();
+  const TutorialOutline({this.showUnitTitle = true});
+
+  final bool showUnitTitle;
 
   @override
   Pattern get pattern => 'TutorialOutline';
@@ -30,22 +32,31 @@ class TutorialOutline extends CustomComponentBase {
         };
 
         return div(classes: 'tutorial-outline', [
-          ol([
-            for (final unit in model.units)
-              li([
-                .text(unit.title),
-                ol([
-                  for (final chapter in unit.chapters)
-                    li([
-                      a(href: chapter.url, [
-                        DashMarkdown(content: chapter.title, inline: true),
-                      ]),
-                    ]),
-                ]),
-              ]),
-          ]),
+          ol([for (final unit in model.units) ..._buildUnit(unit)]),
         ]);
       },
     );
   }
+
+  List _buildUnit(TutorialUnit unit) {
+    final chapters = [
+      for (final chapter in unit.chapters)
+        li([
+          a(href: chapter.url, [
+            DashMarkdown(content: chapter.title, inline: true),
+          ]),
+        ]),
+    ];
+
+    if (showUnitTitle) {
+      return [
+        li([
+          .text(unit.title),
+          ol(chapters),
+        ]),
+      ];
+    } else {
+      return chapters;
+    }
+  }
 }
diff --git a/packages/site_shared/lib/util.dart b/packages/site_shared/lib/util.dart
new file mode 100644
index 00000000000..a3aa7fe19ba
--- /dev/null
+++ b/packages/site_shared/lib/util.dart
@@ -0,0 +1,155 @@
+// Copyright 2025 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:universal_web/web.dart' as web;
+
+/// Whether this build of the site will be deployed to production.
+const productionBuild = bool.fromEnvironment('PRODUCTION');
+
+/// Converts the specified [text] into a standardized URL slug
+/// that can be used as the ID for headers and other anchors in HTML.
+String slugify(String text) => text
+    .toLowerCase()
+    .trim()
+    .replaceAll(_slugifyPunctuationToReplace, '-')
+    .replaceAll(_slugifyUnsupportedToRemove, '')
+    .replaceAll(_slugifyCharsToCombine, '-')
+    .replaceAll(_slugifyHyphenTrim, '');
+
+final RegExp _slugifyPunctuationToReplace = RegExp(r'[:._]');
+final RegExp _slugifyUnsupportedToRemove = RegExp(
+  r'[^\p{L}\p{N}\s:.-]',
+  unicode: true,
+);
+final RegExp _slugifyCharsToCombine = RegExp(r'[\s-]+');
+final RegExp _slugifyHyphenTrim = RegExp(r'^-+|-+$');
+
+final RegExp _attributePattern = RegExp(r'(\w+)="([^"]*)"');
+final RegExp _whitespacePattern = RegExp(r'\s+');
+final RegExp _wordPattern = RegExp(r'\S+(?:\s*|$)');
+final RegExp _trailingMarkdownLinkPattern = RegExp(r'(\[.+\]:\s*\S+\s*)+$');
+
+Map parseAttributes(String attributeString) {
+  final attributes = {};
+  final classes = [];
+
+  // Extract all key="value" pairs.
+  final keyValueMatches = _attributePattern.allMatches(attributeString);
+  for (final match in keyValueMatches) {
+    final key = match.group(1)!;
+    final value = match.group(2)!;
+    attributes[key] = value;
+  }
+
+  // Remove all key="value" pairs to process remaining tokens.
+  final remaining = attributeString.replaceAll(_attributePattern, '').trim();
+
+  // Split remaining content by whitespace to find IDs and classes.
+  final parts = remaining.split(_whitespacePattern);
+
+  for (final part in parts) {
+    if (part.isEmpty) continue;
+
+    if (part.startsWith('#')) {
+      attributes['id'] = part.substring(1);
+    } else if (part.startsWith('.')) {
+      classes.add(part.substring(1));
+    }
+  }
+
+  if (classes.isNotEmpty) {
+    attributes['class'] = classes.join(' ');
+  }
+
+  return attributes;
+}
+
+String truncateWords(String text, int maxWords) {
+  if (maxWords <= 0) {
+    return '';
+  }
+
+  final words = text.trim().split(_whitespacePattern);
+  if (words.length <= maxWords) {
+    return text;
+  }
+
+  final truncated = words.take(maxWords).join(' ');
+  return '$truncated...';
+}
+
+/// Truncates the given [text] to the specified number of words [maxWords],
+/// preserving all whitespace and line breaks, as well as any trailing Markdown
+/// link definitions at the end of the text.
+String truncateWordsMarkdown(String text, int maxWords) {
+  if (maxWords <= 0) {
+    return '';
+  }
+
+  final trailingLinks = _trailingMarkdownLinkPattern.firstMatch(text);
+  var endContent = '';
+
+  if (trailingLinks != null) {
+    text = text.substring(0, trailingLinks.start);
+    endContent = '\n${trailingLinks.group(0)!}';
+  }
+
+  final matches = _wordPattern.allMatches(text);
+  if (matches.length <= maxWords) {
+    return text + endContent;
+  }
+
+  final truncated = matches.map((m) => m.group(0)!).take(maxWords).join('');
+  return '$truncated...\n$endContent';
+}
+
+extension StringUnCapitalize on String {
+  String unCapitalize() =>
+      isEmpty ? this : substring(0, 1).toLowerCase() + substring(1);
+}
+
+extension ListToClasses on List {
+  /// Convert a list of classes into a single class string
+  /// that can be added to an HTML element.
+  String get toClasses => join(' ');
+}
+
+enum OperatingSystem {
+  windows('Windows'),
+  macos('macOS'),
+  linux('Linux'),
+  chromeos('ChromeOS');
+
+  const OperatingSystem(this.label);
+  final String label;
+}
+
+/// Get the user's current operating system, or
+/// `null` if not of one "macos", "windows", "linux", or "chromeos".
+OperatingSystem? getOS() {
+  final userAgent = web.window.navigator.userAgent;
+  if (userAgent.contains('Mac')) {
+    // macOS or iPhone
+    return OperatingSystem.macos;
+  }
+
+  if (userAgent.contains('Win')) {
+    // Windows
+    return OperatingSystem.windows;
+  }
+
+  if ((userAgent.contains('Linux') || userAgent.contains('X11')) &&
+      !userAgent.contains('Android')) {
+    // Linux, but not Android
+    return OperatingSystem.linux;
+  }
+
+  if (userAgent.contains('CrOS')) {
+    // ChromeOS
+    return OperatingSystem.chromeos;
+  }
+
+  // Anything else
+  return null;
+}
diff --git a/site/lib/src/components/util/component_ref.dart b/packages/site_shared/lib/utils/component_ref.dart
similarity index 58%
rename from site/lib/src/components/util/component_ref.dart
rename to packages/site_shared/lib/utils/component_ref.dart
index f4a0c499d03..40bc7d9a077 100644
--- a/site/lib/src/components/util/component_ref.dart
+++ b/packages/site_shared/lib/utils/component_ref.dart
@@ -2,10 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:nanoid2/nanoid2.dart';
 
-import 'retake_element.dart';
+import '../src/utils/retake_element.dart';
 
 /// A wrapper around [Component] to make it usable across server/client boundaries.
 ///
@@ -17,32 +18,32 @@ import 'retake_element.dart';
 /// On the server, wrap your component with `context.ref(yourComponent)`, and
 /// pass the resulting [ComponentRef] to your @client component.
 /// On the client, retrieve the original component by calling `myRef.component`.
-class ComponentRef {
-  const ComponentRef._(this.id);
+class ComponentRef extends StatelessComponent {
+  const ComponentRef._(this.id, [this._component = const .empty()]);
 
   final String id;
+  final Component _component;
 
-  Component get component {
-    return Builder(
-      builder: (context) {
-        if (!kIsWeb) {
-          final scope =
-              context
-                      .getElementForInheritedComponentOfExactType<
-                        ComponentRefScope
-                      >()
-                  as _ComponentRefScopeElement?;
-          return Component.wrapElement(
-            id: id,
-            child: scope!.getComponentById(id),
-          );
-        } else {
-          final elem = retakeElement(context, (elem) => elem.id == id);
-          assert(elem != null, 'Element with id "$id" not found');
-          return wrapNode(elem!);
-        }
-      },
-    );
+  @override
+  Component build(BuildContext context) {
+    if (!kIsWeb) {
+      final scope =
+          context
+                  .getElementForInheritedComponentOfExactType<
+                    ComponentRefScope
+                  >()
+              as _ComponentRefScopeElement?;
+      assert(scope != null, 'No ComponentRefScope found in context');
+      scope!.register(id, _component);
+
+      return Component.fragment([
+        RawText(''),
+        _component,
+        RawText(''),
+      ]);
+    }
+
+    return retakeRef(context, id);
   }
 
   @decoder
@@ -54,16 +55,8 @@ class ComponentRef {
   String toId() => id;
 }
 
-extension ComponentRefExtension on BuildContext {
-  /// Wraps a [Component] in a [ComponentRef] for use in @client components.
-  ComponentRef ref(Component child) {
-    final scope =
-        getElementForInheritedComponentOfExactType()
-            as _ComponentRefScopeElement?;
-    assert(scope != null, 'No ComponentRefScope found in context');
-    final ref = scope!.register(child);
-    return ref;
-  }
+ComponentRef ref(Component child) {
+  return ComponentRef._(nanoid(length: 8), child);
 }
 
 /// A scope for registering and retrieving component references.
@@ -94,9 +87,7 @@ class _ComponentRefScopeElement extends InheritedElement {
     return component!;
   }
 
-  ComponentRef register(Component child) {
-    final id = 'ref-${nanoid(length: 8)}';
+  void register(String id, Component child) {
     _registeredComponents[id] = child;
-    return ComponentRef._(id);
   }
 }
diff --git a/packages/site_shared/lib/utils/define_component.dart b/packages/site_shared/lib/utils/define_component.dart
new file mode 100644
index 00000000000..880e6f0f0dd
--- /dev/null
+++ b/packages/site_shared/lib/utils/define_component.dart
@@ -0,0 +1,29 @@
+import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_content/jaspr_content.dart';
+
+CustomComponent defineComponent(String name, Component child) {
+  return CustomComponent(
+    pattern: RegExp(name, caseSensitive: false),
+    builder: (_, _, _) => child,
+  );
+}
+
+CustomComponent defineComponentWithAttrs(
+  String name,
+  Component Function(Map attributes) factory,
+) {
+  return CustomComponent(
+    pattern: RegExp(name, caseSensitive: false),
+    builder: (_, attrs, _) => factory(attrs),
+  );
+}
+
+CustomComponent defineComponentWithChild(
+  String name,
+  Component Function(Map attributes, Component? child) factory,
+) {
+  return CustomComponent(
+    pattern: RegExp(name, caseSensitive: false),
+    builder: (_, attrs, child) => factory(attrs, child),
+  );
+}
diff --git a/site/lib/src/components/util/global_event_listener.dart b/packages/site_shared/lib/utils/global_event_listener.dart
similarity index 100%
rename from site/lib/src/components/util/global_event_listener.dart
rename to packages/site_shared/lib/utils/global_event_listener.dart
diff --git a/packages/site_shared/pubspec.yaml b/packages/site_shared/pubspec.yaml
new file mode 100644
index 00000000000..481f8a4375c
--- /dev/null
+++ b/packages/site_shared/pubspec.yaml
@@ -0,0 +1,25 @@
+name: site_shared
+publish_to: none
+
+resolution: workspace
+environment:
+  sdk: ^3.11.0
+
+dependencies:
+  build: ^4.0.5
+  collection: ^1.19.1
+  crypto: ^3.0.7
+  html: ^0.15.6
+  jaspr: ^0.22.4
+  jaspr_content: ^0.5.1
+  markdown: ^7.3.1
+  markdown_description_list: ^0.1.1
+  meta: ^1.18.2
+  nanoid2: ^2.0.1
+  opal: ^0.2.2
+  path: ^1.9.0
+  universal_web: ^1.1.1+1
+  yaml: ^3.1.3
+
+dev_dependencies:
+  analysis_defaults: any
diff --git a/pubspec.yaml b/pubspec.yaml
index 23981e17e37..2fd12250298 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,5 +7,6 @@ environment:
 workspace:
   - packages/analysis_defaults
   - packages/excerpter
+  - packages/site_shared
   - site
   - tool/dash_site
diff --git a/site/build.yaml b/site/build.yaml
index 4787d5a2661..b30a155a0fa 100644
--- a/site/build.yaml
+++ b/site/build.yaml
@@ -1,13 +1,3 @@
-builders:
-  stylesHashBuilder:
-    import: 'package:docs_flutter_dev_site/builders.dart'
-    builder_factories: [ 'stylesHashBuilder' ]
-    build_extensions:
-      "web/assets/css/main.css":
-        - "lib/src/style_hash.dart"
-    auto_apply: root_package
-    build_to: source
-
 targets:
   $default:
     builders:
@@ -27,6 +17,3 @@ targets:
             # Disable for now.
             # dart2wasm:
             #   args: [-O2, -Djaspr.flags.release=true]
-      docs_flutter_dev_site:stylesHashBuilder:
-        dev_options:
-          fixed_hash: true
diff --git a/site/lib/_sass/_site.scss b/site/lib/_sass/_site.scss
index c400cf86d58..162ba1f51e1 100644
--- a/site/lib/_sass/_site.scss
+++ b/site/lib/_sass/_site.scss
@@ -11,17 +11,10 @@
 @use 'base/utils';
 
 // Styles for individual components or content types, alphabetically ordered.
-@use 'components/alert';
 @use 'components/banner';
 @use 'components/books';
-@use 'components/breadcrumbs';
-@use 'components/button';
-@use 'components/card';
-@use 'components/code';
 @use 'components/content';
 @use 'components/collapsible';
-@use 'components/cookie-notice';
-@use 'components/dropdown';
 @use 'components/expansion-list';
 @use 'components/filter-search';
 @use 'components/footer';
@@ -33,18 +26,28 @@
 @use 'components/pagenav';
 @use 'components/platform-cards';
 @use 'components/pill';
-@use 'components/quiz';
 @use 'components/sidebar';
 @use 'components/side-menu';
-@use 'components/site-switcher';
-@use 'components/summary-card';
-@use 'components/stepper';
-@use 'components/tabs';
-@use 'components/theming';
-@use 'components/tooltip';
 @use 'components/trailing';
 @use 'components/tutorial_pages';
 
+// Shared styles from site_shared package, alphabetically ordered.
+@use 'package:site_shared/_sass/alert';
+@use 'package:site_shared/_sass/breadcrumbs';
+@use 'package:site_shared/_sass/button';
+@use 'package:site_shared/_sass/card';
+@use 'package:site_shared/_sass/code';
+@use 'package:site_shared/_sass/cookie-notice';
+@use 'package:site_shared/_sass/dropdown';
+@use 'package:site_shared/_sass/menu-toggle';
+@use 'package:site_shared/_sass/quiz';
+@use 'package:site_shared/_sass/site-switcher';
+@use 'package:site_shared/_sass/stepper';
+@use 'package:site_shared/_sass/summary-card';
+@use 'package:site_shared/_sass/tabs';
+@use 'package:site_shared/_sass/theming';
+@use 'package:site_shared/_sass/tooltip';
+
 // Styles for specific pages, alphabetically ordered.
 @use 'pages/glossary';
 @use 'pages/learning-resources-index';
diff --git a/site/lib/_sass/components/_header.scss b/site/lib/_sass/components/_header.scss
index ad24c34f596..2370c1e21e3 100644
--- a/site/lib/_sass/components/_header.scss
+++ b/site/lib/_sass/components/_header.scss
@@ -136,32 +136,6 @@
   }
 }
 
-body:not(.sidenav-closed) #menu-toggle {
-  @media (min-width: 1024px) {
-    display: none;
-  }
-}
-
-// Toggle between menu and close buttons if sidenav is open or not.
-#menu-toggle span.material-symbols {
-  &:first-child {
-    display: inline;
-  }
-
-  &:last-child {
-    display: none;
-  }
-}
-
-body.open_menu #menu-toggle span.material-symbols {
-  &:first-child {
-    display: none;
-  }
-
-  &:last-child {
-    display: inline;
-  }
-}
 
 #site-primary-logo {
   text-decoration: none;
diff --git a/site/lib/_sass/components/_next-prev-nav.scss b/site/lib/_sass/components/_next-prev-nav.scss
index ce2bde478b8..1897e453f13 100644
--- a/site/lib/_sass/components/_next-prev-nav.scss
+++ b/site/lib/_sass/components/_next-prev-nav.scss
@@ -1,4 +1,4 @@
-@use '../base/mixins';
+@use 'package:site_shared/_sass/base/mixins';
 
 #site-prev-next {
   display: flex;
diff --git a/site/lib/_sass/components/_sidebar.scss b/site/lib/_sass/components/_sidebar.scss
index db57724e097..ffa324a545c 100644
--- a/site/lib/_sass/components/_sidebar.scss
+++ b/site/lib/_sass/components/_sidebar.scss
@@ -1,4 +1,4 @@
-@use '../base/mixins';
+@use 'package:site_shared/_sass/base/mixins';
 
 #sidenav {
   margin: 0;
diff --git a/site/lib/_sass/pages/_glossary.scss b/site/lib/_sass/pages/_glossary.scss
index 74fbe7ad353..d7ab79e0672 100644
--- a/site/lib/_sass/pages/_glossary.scss
+++ b/site/lib/_sass/pages/_glossary.scss
@@ -1,4 +1,4 @@
-@use '../base/mixins';
+@use 'package:site_shared/_sass/base/mixins';
 
 body.glossary-page main {
   .glossary-card {
diff --git a/site/lib/_sass/pages/_learning-resources-index.scss b/site/lib/_sass/pages/_learning-resources-index.scss
index d19ecc8a80f..3527fa4ca80 100644
--- a/site/lib/_sass/pages/_learning-resources-index.scss
+++ b/site/lib/_sass/pages/_learning-resources-index.scss
@@ -1,4 +1,4 @@
-@use '../base/mixins';
+@use 'package:site_shared/_sass/base/mixins';
 
 #resource-filter-group-wrapper {
   border: 1px solid var(--site-inset-borderColor);
diff --git a/site/lib/_sass/pages/_search.scss b/site/lib/_sass/pages/_search.scss
index 30fa71479cc..2575342018d 100644
--- a/site/lib/_sass/pages/_search.scss
+++ b/site/lib/_sass/pages/_search.scss
@@ -1,4 +1,4 @@
-@use '../base/mixins';
+@use 'package:site_shared/_sass/base/mixins';
 
 #search-body {
   margin-block-start: 1.5rem;
diff --git a/site/lib/builders.dart b/site/lib/builders.dart
deleted file mode 100644
index e5605d1cbae..00000000000
--- a/site/lib/builders.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2025 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:build/build.dart';
-
-import 'src/builders/styles_hash_builder.dart' show StylesHashBuilder;
-
-Builder stylesHashBuilder(BuilderOptions options) => StylesHashBuilder(options);
diff --git a/site/lib/main.client.options.dart b/site/lib/main.client.options.dart
index 6ebbe20949f..0a025ef2cbe 100644
--- a/site/lib/main.client.options.dart
+++ b/site/lib/main.client.options.dart
@@ -6,36 +6,12 @@
 
 import 'package:jaspr/client.dart';
 
-import 'package:docs_flutter_dev_site/src/components/common/client/collapse_button.dart'
-    deferred as _collapse_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart'
-    deferred as _cookie_notice;
-import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart'
-    deferred as _copy_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/download_button.dart'
-    deferred as _download_button;
 import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart'
     deferred as _download_latest_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart'
-    deferred as _feedback;
-import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart'
-    deferred as _on_this_page_button;
 import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart'
     deferred as _os_selector;
-import 'package:docs_flutter_dev_site/src/components/common/client/page_header_options.dart'
-    deferred as _page_header_options;
-import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart'
-    deferred as _simple_tooltip;
-import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart'
-    deferred as _dartpad_injector;
 import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart'
     deferred as _pagenav;
-import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart'
-    deferred as _menu_toggle;
-import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart'
-    deferred as _site_switcher;
-import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart'
-    deferred as _theme_switcher;
 import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart'
     deferred as _archive_table;
 import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart'
@@ -44,12 +20,31 @@ import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_fil
     deferred as _learning_resource_filters;
 import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart'
     deferred as _learning_resource_filters_sidebar;
-import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart'
-    deferred as _quiz;
-import 'package:docs_flutter_dev_site/src/components/util/component_ref.dart'
-    as _component_ref;
-import 'package:docs_flutter_dev_site/src/models/quiz_model.dart'
-    as _quiz_model;
+import 'package:site_shared/common/client/collapse_button.dart'
+    deferred as _collapse_button;
+import 'package:site_shared/common/client/cookie_notice.dart'
+    deferred as _cookie_notice;
+import 'package:site_shared/common/client/copy_button.dart'
+    deferred as _copy_button;
+import 'package:site_shared/common/client/download_button.dart'
+    deferred as _download_button;
+import 'package:site_shared/common/client/feedback.dart' deferred as _feedback;
+import 'package:site_shared/common/client/on_this_page_button.dart'
+    deferred as _on_this_page_button;
+import 'package:site_shared/common/client/page_header_options.dart'
+    deferred as _page_header_options;
+import 'package:site_shared/common/client/simple_tooltip.dart'
+    deferred as _simple_tooltip;
+import 'package:site_shared/dartpad/dartpad_injector.dart'
+    deferred as _dartpad_injector;
+import 'package:site_shared/layout/menu_toggle.dart' deferred as _menu_toggle;
+import 'package:site_shared/layout/site_switcher.dart'
+    deferred as _site_switcher;
+import 'package:site_shared/layout/theme_switcher.dart'
+    deferred as _theme_switcher;
+import 'package:site_shared/tutorial/client/quiz.dart' deferred as _quiz;
+import 'package:site_shared/tutorial/models/quiz_model.dart' as _quiz_model;
+import 'package:site_shared/utils/component_ref.dart' as _component_ref;
 
 /// Default [ClientOptions] for use with your Jaspr project.
 ///
@@ -69,49 +64,82 @@ import 'package:docs_flutter_dev_site/src/models/quiz_model.dart'
 /// ```
 ClientOptions get defaultClientOptions => ClientOptions(
   clients: {
-    'collapse_button': ClientLoader(
+    'download_latest_button': ClientLoader(
+      (p) => _download_latest_button.DownloadLatestButton(
+        os: p['os'] as String,
+        arch: p['arch'] as String?,
+      ),
+      loader: _download_latest_button.loadLibrary,
+    ),
+    'os_selector': ClientLoader(
+      (p) => _os_selector.OsSelector(),
+      loader: _os_selector.loadLibrary,
+    ),
+    'pagenav': ClientLoader(
+      (p) => _pagenav.PageNav(
+        breadcrumbs: (p['breadcrumbs'] as List).cast(),
+        pageNumber: p['pageNumber'] as int?,
+        initialHeading: p['initialHeading'] as String,
+        content: _component_ref.ComponentRef.fromId(p['content'] as String),
+      ),
+      loader: _pagenav.loadLibrary,
+    ),
+    'archive_table': ClientLoader(
+      (p) => _archive_table.ArchiveTable(
+        os: p['os'] as String,
+        channel: p['channel'] as String,
+      ),
+      loader: _archive_table.loadLibrary,
+    ),
+    'glossary_search_section': ClientLoader(
+      (p) => _glossary_search_section.GlossarySearchSection(),
+      loader: _glossary_search_section.loadLibrary,
+    ),
+    'learning_resource_filters': ClientLoader(
+      (p) => _learning_resource_filters.LearningResourceFilters(),
+      loader: _learning_resource_filters.loadLibrary,
+    ),
+    'learning_resource_filters_sidebar': ClientLoader(
+      (p) =>
+          _learning_resource_filters_sidebar.LearningResourceFiltersSidebar(),
+      loader: _learning_resource_filters_sidebar.loadLibrary,
+    ),
+    'site_shared:collapse_button': ClientLoader(
       (p) => _collapse_button.CollapseButton(
         classes: (p['classes'] as List).cast(),
         title: p['title'] as String?,
       ),
       loader: _collapse_button.loadLibrary,
     ),
-    'cookie_notice': ClientLoader(
-      (p) => _cookie_notice.CookieNotice(),
+    'site_shared:cookie_notice': ClientLoader(
+      (p) => _cookie_notice.CookieNotice(
+        host: p['host'] as String,
+        alwaysDarkMode: p['alwaysDarkMode'] as bool,
+      ),
       loader: _cookie_notice.loadLibrary,
     ),
-    'copy_button': ClientLoader(
+    'site_shared:copy_button': ClientLoader(
       (p) => _copy_button.CopyButton(
         buttonText: p['buttonText'] as String?,
+        toCopy: p['toCopy'] as String?,
         classes: (p['classes'] as List).cast(),
         title: p['title'] as String?,
       ),
       loader: _copy_button.loadLibrary,
     ),
-    'download_button': ClientLoader(
+    'site_shared:download_button': ClientLoader(
       (p) => _download_button.DownloadButton(name: p['name'] as String),
       loader: _download_button.loadLibrary,
     ),
-    'download_latest_button': ClientLoader(
-      (p) => _download_latest_button.DownloadLatestButton(
-        os: p['os'] as String,
-        arch: p['arch'] as String?,
-      ),
-      loader: _download_latest_button.loadLibrary,
-    ),
-    'feedback': ClientLoader(
+    'site_shared:feedback': ClientLoader(
       (p) => _feedback.FeedbackComponent(issueUrl: p['issueUrl'] as String),
       loader: _feedback.loadLibrary,
     ),
-    'on_this_page_button': ClientLoader(
+    'site_shared:on_this_page_button': ClientLoader(
       (p) => _on_this_page_button.OnThisPageButton(),
       loader: _on_this_page_button.loadLibrary,
     ),
-    'os_selector': ClientLoader(
-      (p) => _os_selector.OsSelector(),
-      loader: _os_selector.loadLibrary,
-    ),
-    'page_header_options': ClientLoader(
+    'site_shared:page_header_options': ClientLoader(
       (p) => _page_header_options.PageHeaderOptions(
         title: p['title'] as String,
         sourceUrl: p['sourceUrl'] as String?,
@@ -119,14 +147,14 @@ ClientOptions get defaultClientOptions => ClientOptions(
       ),
       loader: _page_header_options.loadLibrary,
     ),
-    'simple_tooltip': ClientLoader(
+    'site_shared:simple_tooltip': ClientLoader(
       (p) => _simple_tooltip.SimpleTooltip(
         target: _component_ref.ComponentRef.fromId(p['target'] as String),
         content: _component_ref.ComponentRef.fromId(p['content'] as String),
       ),
       loader: _simple_tooltip.loadLibrary,
     ),
-    'dartpad_injector': ClientLoader(
+    'site_shared:dartpad_injector': ClientLoader(
       (p) => _dartpad_injector.DartPadInjector(
         title: p['title'] as String,
         theme: p['theme'] as String?,
@@ -135,48 +163,19 @@ ClientOptions get defaultClientOptions => ClientOptions(
       ),
       loader: _dartpad_injector.loadLibrary,
     ),
-    'pagenav': ClientLoader(
-      (p) => _pagenav.PageNav(
-        breadcrumbs: (p['breadcrumbs'] as List).cast(),
-        pageNumber: p['pageNumber'] as int?,
-        initialHeading: p['initialHeading'] as String,
-        content: _component_ref.ComponentRef.fromId(p['content'] as String),
-      ),
-      loader: _pagenav.loadLibrary,
-    ),
-    'menu_toggle': ClientLoader(
+    'site_shared:menu_toggle': ClientLoader(
       (p) => _menu_toggle.MenuToggle(),
       loader: _menu_toggle.loadLibrary,
     ),
-    'site_switcher': ClientLoader(
-      (p) => _site_switcher.SiteSwitcher(),
+    'site_shared:site_switcher': ClientLoader(
+      (p) => _site_switcher.SiteSwitcher(isFlutter: p['isFlutter'] as bool),
       loader: _site_switcher.loadLibrary,
     ),
-    'theme_switcher': ClientLoader(
+    'site_shared:theme_switcher': ClientLoader(
       (p) => _theme_switcher.ThemeSwitcher(),
       loader: _theme_switcher.loadLibrary,
     ),
-    'archive_table': ClientLoader(
-      (p) => _archive_table.ArchiveTable(
-        os: p['os'] as String,
-        channel: p['channel'] as String,
-      ),
-      loader: _archive_table.loadLibrary,
-    ),
-    'glossary_search_section': ClientLoader(
-      (p) => _glossary_search_section.GlossarySearchSection(),
-      loader: _glossary_search_section.loadLibrary,
-    ),
-    'learning_resource_filters': ClientLoader(
-      (p) => _learning_resource_filters.LearningResourceFilters(),
-      loader: _learning_resource_filters.loadLibrary,
-    ),
-    'learning_resource_filters_sidebar': ClientLoader(
-      (p) =>
-          _learning_resource_filters_sidebar.LearningResourceFiltersSidebar(),
-      loader: _learning_resource_filters_sidebar.loadLibrary,
-    ),
-    'quiz': ClientLoader(
+    'site_shared:quiz': ClientLoader(
       (p) => _quiz.InteractiveQuiz(
         title: p['title'] as String?,
         questions: (p['questions'] as List)
diff --git a/site/lib/main.server.dart b/site/lib/main.server.dart
index 2c897e0b8d0..1ee4e5e0e58 100644
--- a/site/lib/main.server.dart
+++ b/site/lib/main.server.dart
@@ -7,16 +7,25 @@ import 'package:jaspr_content/components/file_tree.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 import 'package:jaspr_content/theme.dart';
 import 'package:path/path.dart' as path;
+import 'package:site_shared/common/card.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/common/tabs.dart';
+import 'package:site_shared/common/youtube_embed.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
+import 'package:site_shared/tutorial/downloadable_snippet.dart';
+import 'package:site_shared/tutorial/progress_ring.dart';
+import 'package:site_shared/tutorial/quiz.dart';
+import 'package:site_shared/tutorial/stepper.dart';
+import 'package:site_shared/tutorial/summary_card.dart';
+import 'package:site_shared/tutorial/tutorial_outline.dart';
+import 'package:site_shared/utils/component_ref.dart';
+import 'package:site_shared/utils/define_component.dart';
 
 import 'main.server.options.dart'; // Generated. Do not remove or edit.
-import 'src/components/common/card.dart';
 import 'src/components/common/client/download_latest_button.dart';
 import 'src/components/common/client/os_selector.dart';
 import 'src/components/common/code_preview.dart';
 import 'src/components/common/dash_image.dart';
-import 'src/components/common/material_icon.dart';
-import 'src/components/common/tabs.dart';
-import 'src/components/common/youtube_embed.dart';
 import 'src/components/pages/architecture_recommendations.dart';
 import 'src/components/pages/archive_table.dart';
 import 'src/components/pages/devtools_release_notes_index.dart';
@@ -24,19 +33,11 @@ import 'src/components/pages/expansion_list.dart';
 import 'src/components/pages/learning_resource_index.dart';
 import 'src/components/pages/platforms_grid.dart';
 import 'src/components/pages/widget_catalog.dart';
-import 'src/components/tutorial/downloadable_snippet.dart';
-import 'src/components/tutorial/progress_ring.dart';
-import 'src/components/tutorial/quiz.dart';
-import 'src/components/tutorial/stepper.dart';
-import 'src/components/tutorial/summary_card.dart';
-import 'src/components/tutorial/tutorial_outline.dart';
-import 'src/components/util/component_ref.dart';
 import 'src/extensions/registry.dart';
 import 'src/layouts/doc_layout.dart';
 import 'src/layouts/toc_layout.dart';
 import 'src/layouts/tutorial_layout.dart';
 import 'src/loaders/data_processor.dart';
-import 'src/markdown/markdown_parser.dart';
 import 'src/pages/custom_pages.dart';
 import 'src/pages/robots_txt.dart';
 import 'src/templating/dash_template_engine.dart';
@@ -116,7 +117,9 @@ List get _embeddableComponents => [
   const Quiz(),
   const ProgressRing(),
   const SummaryCard(),
-  const DownloadableSnippet(),
+  DownloadableSnippet(
+    snippetsDirectoryPath: path.join(siteSrcDirectoryPath, '_snippets'),
+  ),
   const Stepper(),
   const WidgetCatalogCategories(),
   const TutorialOutline(),
@@ -124,36 +127,18 @@ List get _embeddableComponents => [
   const ArchitectureRecommendations(),
   const PlatformsGrid(),
   const PlatformCard(),
-  CustomComponent(
-    pattern: RegExp('Icon', caseSensitive: false),
-    builder: (_, attrs, _) => MaterialIcon.fromAttributes(attrs),
-  ),
-  CustomComponent(
-    pattern: RegExp('OSSelector', caseSensitive: false),
-    builder: (_, _, _) => const OsSelector(),
-  ),
-  CustomComponent(
-    pattern: RegExp('Card', caseSensitive: false),
-    builder: (_, attrs, child) => Card.fromAttributes(attrs, child),
-  ),
-  CustomComponent(
-    pattern: RegExp('LearningResourceIndex', caseSensitive: false),
-    builder: (_, _, _) => LearningResourceIndex(),
-  ),
-  CustomComponent(
-    pattern: RegExp('ArchiveTable'),
-    builder: (_, attrs, _) => ArchiveTable.fromAttributes(attrs),
-  ),
-  CustomComponent(
-    pattern: RegExp('DownloadLatestButton', caseSensitive: false),
-    builder: (_, attrs, _) => DownloadLatestButton.fromAttributes(attrs),
-  ),
-  CustomComponent(
-    pattern: RegExp('ExpansionList', caseSensitive: false),
-    builder: (_, attrs, _) => ExpansionList.fromAttributes(attrs),
+  defineComponentWithAttrs('Icon', MaterialIcon.fromAttributes),
+  defineComponent('OSSelector', const OsSelector()),
+  defineComponentWithChild('Card', Card.fromAttributes),
+  defineComponent('LearningResourceIndex', const LearningResourceIndex()),
+  defineComponentWithAttrs('ArchiveTable', ArchiveTable.fromAttributes),
+  defineComponentWithAttrs(
+    'DownloadLatestButton',
+    DownloadLatestButton.fromAttributes,
   ),
-  CustomComponent(
-    pattern: RegExp('DevToolsReleaseNotesIndex', caseSensitive: false),
-    builder: (_, _, _) => const DevToolsReleaseNotesIndex(),
+  defineComponentWithAttrs('ExpansionList', ExpansionList.fromAttributes),
+  defineComponent(
+    'DevToolsReleaseNotesIndex',
+    const DevToolsReleaseNotesIndex(),
   ),
 ];
diff --git a/site/lib/main.server.options.dart b/site/lib/main.server.options.dart
index 6262d37d117..13ba188effd 100644
--- a/site/lib/main.server.options.dart
+++ b/site/lib/main.server.options.dart
@@ -5,36 +5,12 @@
 // Generated with jaspr_builder
 
 import 'package:jaspr/server.dart';
-import 'package:docs_flutter_dev_site/src/components/common/client/collapse_button.dart'
-    as _collapse_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart'
-    as _cookie_notice;
-import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart'
-    as _copy_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/download_button.dart'
-    as _download_button;
 import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart'
     as _download_latest_button;
-import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart'
-    as _feedback;
-import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart'
-    as _on_this_page_button;
 import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart'
     as _os_selector;
-import 'package:docs_flutter_dev_site/src/components/common/client/page_header_options.dart'
-    as _page_header_options;
-import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart'
-    as _simple_tooltip;
-import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart'
-    as _dartpad_injector;
 import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart'
     as _pagenav;
-import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart'
-    as _menu_toggle;
-import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart'
-    as _site_switcher;
-import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart'
-    as _theme_switcher;
 import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart'
     as _archive_table;
 import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart'
@@ -43,9 +19,25 @@ import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_fil
     as _learning_resource_filters;
 import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart'
     as _learning_resource_filters_sidebar;
-import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart'
-    as _quiz;
 import 'package:jaspr_content/components/file_tree.dart' as _file_tree;
+import 'package:site_shared/common/client/collapse_button.dart'
+    as _collapse_button;
+import 'package:site_shared/common/client/cookie_notice.dart' as _cookie_notice;
+import 'package:site_shared/common/client/copy_button.dart' as _copy_button;
+import 'package:site_shared/common/client/download_button.dart'
+    as _download_button;
+import 'package:site_shared/common/client/feedback.dart' as _feedback;
+import 'package:site_shared/common/client/on_this_page_button.dart'
+    as _on_this_page_button;
+import 'package:site_shared/common/client/page_header_options.dart'
+    as _page_header_options;
+import 'package:site_shared/common/client/simple_tooltip.dart'
+    as _simple_tooltip;
+import 'package:site_shared/dartpad/dartpad_injector.dart' as _dartpad_injector;
+import 'package:site_shared/layout/menu_toggle.dart' as _menu_toggle;
+import 'package:site_shared/layout/site_switcher.dart' as _site_switcher;
+import 'package:site_shared/layout/theme_switcher.dart' as _theme_switcher;
+import 'package:site_shared/tutorial/client/quiz.dart' as _quiz;
 
 /// Default [ServerOptions] for use with your Jaspr project.
 ///
@@ -66,104 +58,119 @@ import 'package:jaspr_content/components/file_tree.dart' as _file_tree;
 ServerOptions get defaultServerOptions => ServerOptions(
   clientId: 'main.client.dart.js',
   clients: {
+    _download_latest_button.DownloadLatestButton:
+        ClientTarget<_download_latest_button.DownloadLatestButton>(
+          'download_latest_button',
+          params: __download_latest_buttonDownloadLatestButton,
+        ),
+    _os_selector.OsSelector: ClientTarget<_os_selector.OsSelector>(
+      'os_selector',
+    ),
+    _pagenav.PageNav: ClientTarget<_pagenav.PageNav>(
+      'pagenav',
+      params: __pagenavPageNav,
+    ),
+    _archive_table.ArchiveTable: ClientTarget<_archive_table.ArchiveTable>(
+      'archive_table',
+      params: __archive_tableArchiveTable,
+    ),
+    _glossary_search_section.GlossarySearchSection:
+        ClientTarget<_glossary_search_section.GlossarySearchSection>(
+          'glossary_search_section',
+        ),
+    _learning_resource_filters.LearningResourceFilters:
+        ClientTarget<_learning_resource_filters.LearningResourceFilters>(
+          'learning_resource_filters',
+        ),
+    _learning_resource_filters_sidebar.LearningResourceFiltersSidebar:
+        ClientTarget<
+          _learning_resource_filters_sidebar.LearningResourceFiltersSidebar
+        >('learning_resource_filters_sidebar'),
     _collapse_button.CollapseButton:
         ClientTarget<_collapse_button.CollapseButton>(
-          'collapse_button',
+          'site_shared:collapse_button',
           params: __collapse_buttonCollapseButton,
         ),
     _cookie_notice.CookieNotice: ClientTarget<_cookie_notice.CookieNotice>(
-      'cookie_notice',
+      'site_shared:cookie_notice',
+      params: __cookie_noticeCookieNotice,
     ),
     _copy_button.CopyButton: ClientTarget<_copy_button.CopyButton>(
-      'copy_button',
+      'site_shared:copy_button',
       params: __copy_buttonCopyButton,
     ),
     _download_button.DownloadButton:
         ClientTarget<_download_button.DownloadButton>(
-          'download_button',
+          'site_shared:download_button',
           params: __download_buttonDownloadButton,
         ),
-    _download_latest_button.DownloadLatestButton:
-        ClientTarget<_download_latest_button.DownloadLatestButton>(
-          'download_latest_button',
-          params: __download_latest_buttonDownloadLatestButton,
-        ),
     _feedback.FeedbackComponent: ClientTarget<_feedback.FeedbackComponent>(
-      'feedback',
+      'site_shared:feedback',
       params: __feedbackFeedbackComponent,
     ),
     _on_this_page_button.OnThisPageButton:
         ClientTarget<_on_this_page_button.OnThisPageButton>(
-          'on_this_page_button',
+          'site_shared:on_this_page_button',
         ),
-    _os_selector.OsSelector: ClientTarget<_os_selector.OsSelector>(
-      'os_selector',
-    ),
     _page_header_options.PageHeaderOptions:
         ClientTarget<_page_header_options.PageHeaderOptions>(
-          'page_header_options',
+          'site_shared:page_header_options',
           params: __page_header_optionsPageHeaderOptions,
         ),
     _simple_tooltip.SimpleTooltip: ClientTarget<_simple_tooltip.SimpleTooltip>(
-      'simple_tooltip',
+      'site_shared:simple_tooltip',
       params: __simple_tooltipSimpleTooltip,
     ),
     _dartpad_injector.DartPadInjector:
         ClientTarget<_dartpad_injector.DartPadInjector>(
-          'dartpad_injector',
+          'site_shared:dartpad_injector',
           params: __dartpad_injectorDartPadInjector,
         ),
-    _pagenav.PageNav: ClientTarget<_pagenav.PageNav>(
-      'pagenav',
-      params: __pagenavPageNav,
-    ),
     _menu_toggle.MenuToggle: ClientTarget<_menu_toggle.MenuToggle>(
-      'menu_toggle',
+      'site_shared:menu_toggle',
     ),
     _site_switcher.SiteSwitcher: ClientTarget<_site_switcher.SiteSwitcher>(
-      'site_switcher',
+      'site_shared:site_switcher',
+      params: __site_switcherSiteSwitcher,
     ),
     _theme_switcher.ThemeSwitcher: ClientTarget<_theme_switcher.ThemeSwitcher>(
-      'theme_switcher',
-    ),
-    _archive_table.ArchiveTable: ClientTarget<_archive_table.ArchiveTable>(
-      'archive_table',
-      params: __archive_tableArchiveTable,
+      'site_shared:theme_switcher',
     ),
-    _glossary_search_section.GlossarySearchSection:
-        ClientTarget<_glossary_search_section.GlossarySearchSection>(
-          'glossary_search_section',
-        ),
-    _learning_resource_filters.LearningResourceFilters:
-        ClientTarget<_learning_resource_filters.LearningResourceFilters>(
-          'learning_resource_filters',
-        ),
-    _learning_resource_filters_sidebar.LearningResourceFiltersSidebar:
-        ClientTarget<
-          _learning_resource_filters_sidebar.LearningResourceFiltersSidebar
-        >('learning_resource_filters_sidebar'),
     _quiz.InteractiveQuiz: ClientTarget<_quiz.InteractiveQuiz>(
-      'quiz',
+      'site_shared:quiz',
       params: __quizInteractiveQuiz,
     ),
   },
   styles: () => [..._file_tree.FileTree.styles],
 );
 
+Map __download_latest_buttonDownloadLatestButton(
+  _download_latest_button.DownloadLatestButton c,
+) => {'os': c.os, 'arch': c.arch};
+Map __pagenavPageNav(_pagenav.PageNav c) => {
+  'breadcrumbs': c.breadcrumbs,
+  'pageNumber': c.pageNumber,
+  'initialHeading': c.initialHeading,
+  'content': c.content.toId(),
+};
+Map __archive_tableArchiveTable(
+  _archive_table.ArchiveTable c,
+) => {'os': c.os, 'channel': c.channel};
 Map __collapse_buttonCollapseButton(
   _collapse_button.CollapseButton c,
 ) => {'classes': c.classes, 'title': c.title};
+Map __cookie_noticeCookieNotice(
+  _cookie_notice.CookieNotice c,
+) => {'host': c.host, 'alwaysDarkMode': c.alwaysDarkMode};
 Map __copy_buttonCopyButton(_copy_button.CopyButton c) => {
   'buttonText': c.buttonText,
+  'toCopy': c.toCopy,
   'classes': c.classes,
   'title': c.title,
 };
 Map __download_buttonDownloadButton(
   _download_button.DownloadButton c,
 ) => {'name': c.name};
-Map __download_latest_buttonDownloadLatestButton(
-  _download_latest_button.DownloadLatestButton c,
-) => {'os': c.os, 'arch': c.arch};
 Map __feedbackFeedbackComponent(
   _feedback.FeedbackComponent c,
 ) => {'issueUrl': c.issueUrl};
@@ -181,15 +188,9 @@ Map __dartpad_injectorDartPadInjector(
   'height': c.height,
   'runAutomatically': c.runAutomatically,
 };
-Map __pagenavPageNav(_pagenav.PageNav c) => {
-  'breadcrumbs': c.breadcrumbs,
-  'pageNumber': c.pageNumber,
-  'initialHeading': c.initialHeading,
-  'content': c.content.toId(),
-};
-Map __archive_tableArchiveTable(
-  _archive_table.ArchiveTable c,
-) => {'os': c.os, 'channel': c.channel};
+Map __site_switcherSiteSwitcher(
+  _site_switcher.SiteSwitcher c,
+) => {'isFlutter': c.isFlutter};
 Map __quizInteractiveQuiz(_quiz.InteractiveQuiz c) => {
   'title': c.title,
   'questions': c.questions.map((i) => i.toJson()).toList(),
diff --git a/site/lib/src/components/common/code_preview.dart b/site/lib/src/components/common/code_preview.dart
index c4adcda0328..f293eea115b 100644
--- a/site/lib/src/components/common/code_preview.dart
+++ b/site/lib/src/components/common/code_preview.dart
@@ -5,9 +5,9 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/wrapped_code_block.dart';
 
 import '../../util.dart';
-import 'wrapped_code_block.dart';
 
 /// A component that displays a preview area alongside a code block.
 ///
diff --git a/site/lib/src/components/common/dash_image.dart b/site/lib/src/components/common/dash_image.dart
index f39aa586326..050d6f094bf 100644
--- a/site/lib/src/components/common/dash_image.dart
+++ b/site/lib/src/components/common/dash_image.dart
@@ -5,8 +5,7 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../../markdown/markdown_parser.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
 class DashImage with CustomComponentBase {
   const DashImage();
diff --git a/site/lib/src/components/common/page_header.dart b/site/lib/src/components/common/page_header.dart
index 74178855586..d5ce5a898ab 100644
--- a/site/lib/src/components/common/page_header.dart
+++ b/site/lib/src/components/common/page_header.dart
@@ -5,12 +5,12 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/breadcrumbs.dart';
+import 'package:site_shared/common/client/page_header_options.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
-import '../../markdown/markdown_parser.dart';
 import '../../util.dart';
 import '../../utils/page_source_info.dart';
-import 'breadcrumbs.dart';
-import 'client/page_header_options.dart';
 
 final class PageHeader extends StatelessComponent {
   const PageHeader({
diff --git a/site/lib/src/components/common/prev_next.dart b/site/lib/src/components/common/prev_next.dart
index 5750e48a7d0..c0ec875ef29 100644
--- a/site/lib/src/components/common/prev_next.dart
+++ b/site/lib/src/components/common/prev_next.dart
@@ -4,10 +4,10 @@
 
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
-import '../../markdown/markdown_parser.dart';
 import '../../models/page_navigation_model.dart';
-import 'material_icon.dart';
 
 /// Previous and next page buttons to display at the end of a page
 /// in a connected series of pages, such as the language docs.
diff --git a/site/lib/src/components/layout/client/pagenav.dart b/site/lib/src/components/layout/client/pagenav.dart
index 1477716478e..83845183aa3 100644
--- a/site/lib/src/components/layout/client/pagenav.dart
+++ b/site/lib/src/components/layout/client/pagenav.dart
@@ -4,14 +4,14 @@
 
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
+import 'package:site_shared/common/dropdown.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/utils/component_ref.dart';
 import 'package:universal_web/js_interop.dart';
 import 'package:universal_web/web.dart' as web;
 
 import '../../../client/global_scripts.dart';
 import '../../../util.dart';
-import '../../common/dropdown.dart';
-import '../../common/material_icon.dart';
-import '../../util/component_ref.dart';
 
 @client
 class PageNav extends StatefulComponent {
@@ -113,7 +113,7 @@ class _PageNavState extends State {
           ]),
         ],
       ),
-      content: component.content.component,
+      content: component.content,
     );
   }
 
diff --git a/site/lib/src/components/layout/header.dart b/site/lib/src/components/layout/header.dart
index 4b3322b71c6..1190c7c1fac 100644
--- a/site/lib/src/components/layout/header.dart
+++ b/site/lib/src/components/layout/header.dart
@@ -5,14 +5,14 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/button.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/layout/menu_toggle.dart';
+import 'package:site_shared/layout/site_switcher.dart';
+import 'package:site_shared/layout/theme_switcher.dart';
 
 import '../../util.dart';
 import '../../utils/active_nav.dart';
-import '../common/button.dart';
-import '../common/material_icon.dart';
-import 'menu_toggle.dart';
-import 'site_switcher.dart';
-import 'theme_switcher.dart';
 
 /// The site-wide top navigation bar.
 class DashHeader extends StatelessComponent {
diff --git a/site/lib/src/components/layout/sidenav.dart b/site/lib/src/components/layout/sidenav.dart
index 57a4aad7cce..eada2c17f4f 100644
--- a/site/lib/src/components/layout/sidenav.dart
+++ b/site/lib/src/components/layout/sidenav.dart
@@ -5,11 +5,11 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/material_icon.dart';
 
 import '../../models/sidenav_model.dart';
 import '../../util.dart';
 import '../../utils/active_nav.dart';
-import '../common/material_icon.dart';
 
 /// The site-wide side navigation menu,
 /// with entries loaded from the `src/data/sidenav.yml` file.
diff --git a/site/lib/src/components/layout/toc.dart b/site/lib/src/components/layout/toc.dart
index 07362c1b6c6..bc9cd6e4c2e 100644
--- a/site/lib/src/components/layout/toc.dart
+++ b/site/lib/src/components/layout/toc.dart
@@ -5,13 +5,13 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/client/on_this_page_button.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
+import 'package:site_shared/utils/component_ref.dart';
 
-import '../../markdown/markdown_parser.dart';
 import '../../models/page_navigation_model.dart';
 import '../../util.dart';
-import '../common/client/on_this_page_button.dart';
-import '../common/material_icon.dart';
-import '../util/component_ref.dart';
 import 'client/pagenav.dart';
 
 final class DashTableOfContents extends StatelessComponent {
@@ -68,7 +68,7 @@ final class PageNavBar extends StatelessComponent {
       ],
       pageNumber: linkedPageTitle != null ? currentLinkedPageNumber : null,
       initialHeading: currentTitle,
-      content: context.ref(
+      content: ref(
         div([
           if (data.pageEntries.isEmpty) ...[
             a(
diff --git a/site/lib/src/components/layout/trailing_content.dart b/site/lib/src/components/layout/trailing_content.dart
index 39ec4ac558b..1bfd0101e30 100644
--- a/site/lib/src/components/layout/trailing_content.dart
+++ b/site/lib/src/components/layout/trailing_content.dart
@@ -5,9 +5,9 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/client/feedback.dart';
 
 import '../../utils/page_source_info.dart';
-import '../common/client/feedback.dart';
 
 /// The trailing content of a content documentation page, such as
 /// its last updated information, report an issue links, and similar.
diff --git a/site/lib/src/components/pages/architecture_recommendations.dart b/site/lib/src/components/pages/architecture_recommendations.dart
index 5477bbc4a11..5453d4a7d25 100644
--- a/site/lib/src/components/pages/architecture_recommendations.dart
+++ b/site/lib/src/components/pages/architecture_recommendations.dart
@@ -5,8 +5,7 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../../markdown/markdown_parser.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
 class ArchitectureRecommendations extends CustomComponentBase {
   const ArchitectureRecommendations();
diff --git a/site/lib/src/components/pages/expansion_list.dart b/site/lib/src/components/pages/expansion_list.dart
index 25aed291081..94a9e1b3543 100644
--- a/site/lib/src/components/pages/expansion_list.dart
+++ b/site/lib/src/components/pages/expansion_list.dart
@@ -7,8 +7,8 @@ import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 import 'package:path/path.dart' as path;
+import 'package:site_shared/markdown/markdown_parser.dart';
 
-import '../../markdown/markdown_parser.dart';
 import '../../util.dart';
 
 class ExpansionListItem {
diff --git a/site/lib/src/components/pages/glossary_search_section.dart b/site/lib/src/components/pages/glossary_search_section.dart
index 440746f834e..4dad9c69f99 100644
--- a/site/lib/src/components/pages/glossary_search_section.dart
+++ b/site/lib/src/components/pages/glossary_search_section.dart
@@ -4,10 +4,9 @@
 
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
+import 'package:site_shared/common/search.dart';
 import 'package:universal_web/web.dart' as web;
 
-import '../common/search.dart';
-
 @client
 class GlossarySearchSection extends StatefulComponent {
   const GlossarySearchSection({super.key});
diff --git a/site/lib/src/components/pages/learning_resource_filters.dart b/site/lib/src/components/pages/learning_resource_filters.dart
index 870780c44b6..d1597bb1d88 100644
--- a/site/lib/src/components/pages/learning_resource_filters.dart
+++ b/site/lib/src/components/pages/learning_resource_filters.dart
@@ -6,14 +6,14 @@ import 'dart:math';
 
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/common/search.dart';
+import 'package:site_shared/utils/global_event_listener.dart';
 import 'package:universal_web/js_interop.dart';
 import 'package:universal_web/web.dart' as web;
 
 import '../../analytics/analytics.dart';
 import '../../models/learning_resource_model.dart';
-import '../common/material_icon.dart';
-import '../common/search.dart';
-import '../util/global_event_listener.dart';
 import 'learning_resource_filters_sidebar.dart';
 
 @client
diff --git a/site/lib/src/components/pages/learning_resource_filters_sidebar.dart b/site/lib/src/components/pages/learning_resource_filters_sidebar.dart
index c918d05692a..e64e5adbd8f 100644
--- a/site/lib/src/components/pages/learning_resource_filters_sidebar.dart
+++ b/site/lib/src/components/pages/learning_resource_filters_sidebar.dart
@@ -4,11 +4,11 @@
 
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
+import 'package:site_shared/common/material_icon.dart';
 
 import '../../analytics/analytics.dart';
 import '../../models/learning_resource_model.dart';
 import '../../util.dart';
-import '../common/material_icon.dart';
 import 'learning_resource_filters.dart';
 
 @client
diff --git a/site/lib/src/components/pages/learning_resource_index.dart b/site/lib/src/components/pages/learning_resource_index.dart
index 139d075f92e..f39ecdbf2cd 100644
--- a/site/lib/src/components/pages/learning_resource_index.dart
+++ b/site/lib/src/components/pages/learning_resource_index.dart
@@ -12,7 +12,7 @@ import 'learning_resource_filters.dart';
 import 'learning_resource_filters_sidebar.dart';
 
 final class LearningResourceIndex extends StatelessComponent {
-  LearningResourceIndex({super.key});
+  const LearningResourceIndex({super.key});
 
   @override
   Component build(BuildContext context) {
diff --git a/site/lib/src/components/pages/platforms_grid.dart b/site/lib/src/components/pages/platforms_grid.dart
index 66793e7e81f..9e688b91123 100644
--- a/site/lib/src/components/pages/platforms_grid.dart
+++ b/site/lib/src/components/pages/platforms_grid.dart
@@ -5,10 +5,9 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../../markdown/markdown_parser.dart';
-import '../common/button.dart';
-import '../common/material_icon.dart';
+import 'package:site_shared/common/button.dart';
+import 'package:site_shared/common/material_icon.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
 class PlatformsGrid extends CustomComponentBase {
   const PlatformsGrid();
diff --git a/site/lib/src/components/pages/widget_catalog.dart b/site/lib/src/components/pages/widget_catalog.dart
index 61e968225b3..3839a6d1ad0 100644
--- a/site/lib/src/components/pages/widget_catalog.dart
+++ b/site/lib/src/components/pages/widget_catalog.dart
@@ -6,8 +6,8 @@ import 'package:collection/collection.dart';
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
-import '../../markdown/markdown_parser.dart';
 import '../../models/widget_catalog_model.dart';
 import '../../util.dart';
 
diff --git a/site/lib/src/components/tutorial/quiz.dart b/site/lib/src/components/tutorial/quiz.dart
deleted file mode 100644
index d552df4f8c3..00000000000
--- a/site/lib/src/components/tutorial/quiz.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2025 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:jaspr/jaspr.dart';
-import 'package:jaspr_content/jaspr_content.dart';
-import 'package:yaml/yaml.dart';
-
-import '../../models/quiz_model.dart';
-import 'client/quiz.dart';
-
-class Quiz extends CustomComponent {
-  const Quiz() : super.base();
-
-  @override
-  Component? create(Node node, NodesBuilder builder) {
-    if (node is ElementNode && node.tag.toLowerCase() == 'quiz') {
-      if (node.children?.whereType().isNotEmpty ?? false) {
-        throw Exception(
-          'Invalid Quiz content. Remove any leading empty lines to '
-          'avoid parsing as markdown.',
-        );
-      }
-
-      final title = node.attributes['title'];
-
-      final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
-      final data = loadYamlNode(content);
-      assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.');
-      final questions = (data as YamlList).nodes
-          .map((n) => Question.fromMap(n as YamlMap))
-          .toList();
-      assert(questions.isNotEmpty, 'Quiz must contain at least one question.');
-      return InteractiveQuiz(title: title, questions: questions);
-    }
-    return null;
-  }
-}
diff --git a/site/lib/src/components/util/retake_element_vm.dart b/site/lib/src/components/util/retake_element_vm.dart
deleted file mode 100644
index 775262eedd1..00000000000
--- a/site/lib/src/components/util/retake_element_vm.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2025 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:jaspr/jaspr.dart';
-import 'package:universal_web/web.dart' as web;
-
-web.Element? retakeElement(
-  BuildContext context,
-  bool Function(web.Element element) predicate,
-) {
-  throw UnimplementedError();
-}
-
-Component wrapNode(web.Node node) {
-  throw UnimplementedError();
-}
diff --git a/site/lib/src/extensions/attribute_processor.dart b/site/lib/src/extensions/attribute_processor.dart
index 2b5b4419112..3621f6b779c 100644
--- a/site/lib/src/extensions/attribute_processor.dart
+++ b/site/lib/src/extensions/attribute_processor.dart
@@ -3,8 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../markdown/attribute_syntax.dart';
+import 'package:site_shared/markdown/attribute_syntax.dart';
 
 /// A node-processing, page extension for Jaspr Content that looks for
 /// attribute markers from [AttributeBlockSyntax] and [AttributeInlineSyntax],
diff --git a/site/lib/src/extensions/glossary_link_processor.dart b/site/lib/src/extensions/glossary_link_processor.dart
index f1602547136..b325f7888be 100644
--- a/site/lib/src/extensions/glossary_link_processor.dart
+++ b/site/lib/src/extensions/glossary_link_processor.dart
@@ -5,9 +5,9 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/client/simple_tooltip.dart';
+import 'package:site_shared/utils/component_ref.dart';
 
-import '../components/common/client/simple_tooltip.dart';
-import '../components/util/component_ref.dart';
 import '../pages/glossary.dart';
 
 /// A node-processing, page extension for Jaspr Content that looks for links to
@@ -52,8 +52,8 @@ class GlossaryLinkProcessor implements PageExtension {
             Builder(
               builder: (context) {
                 return SimpleTooltip(
-                  target: context.ref(target),
-                  content: context.ref(content),
+                  target: ref(target),
+                  content: ref(content),
                 );
               },
             ),
diff --git a/site/lib/src/extensions/registry.dart b/site/lib/src/extensions/registry.dart
index fb51180b84b..47ddf7735d7 100644
--- a/site/lib/src/extensions/registry.dart
+++ b/site/lib/src/extensions/registry.dart
@@ -3,9 +3,9 @@
 // found in the LICENSE file.
 
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/extensions/code_block_processor.dart';
 
 import 'attribute_processor.dart';
-import 'code_block_processor.dart';
 import 'glossary_link_processor.dart';
 import 'header_extractor.dart';
 import 'header_processor.dart';
@@ -20,7 +20,7 @@ const List allNodeProcessingExtensions = [
   HeaderExtractorExtension(),
   HeaderWrapperExtension(),
   TableWrapperExtension(),
-  CodeBlockProcessor(),
+  CodeBlockProcessor(defaultTitle: 'Runnable Flutter example'),
   GlossaryLinkProcessor(),
   TutorialNavigationExtension(),
   TutorialStructureExtension(),
diff --git a/site/lib/src/extensions/tutorial_prefetch_processor.dart b/site/lib/src/extensions/tutorial_prefetch_processor.dart
index 27ba7b19748..e58bf5e9213 100644
--- a/site/lib/src/extensions/tutorial_prefetch_processor.dart
+++ b/site/lib/src/extensions/tutorial_prefetch_processor.dart
@@ -5,8 +5,7 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
-
-import '../models/tutorial_model.dart';
+import 'package:site_shared/tutorial/models/tutorial_model.dart';
 
 /// A page extension for Jaspr Content that adds page navigation and a
 /// prefetch link for the next unit to the current tutorial page.
diff --git a/site/lib/src/layouts/dash_layout.dart b/site/lib/src/layouts/dash_layout.dart
index 72e756f55fb..48e398334b9 100644
--- a/site/lib/src/layouts/dash_layout.dart
+++ b/site/lib/src/layouts/dash_layout.dart
@@ -5,8 +5,8 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/client/cookie_notice.dart';
 
-import '../components/common/client/cookie_notice.dart';
 import '../components/layout/footer.dart';
 import '../components/layout/header.dart';
 import '../components/layout/sidenav.dart';
@@ -208,7 +208,7 @@ try {
           href: '#site-content-title',
           [.text('Skip to main content')],
         ),
-        const CookieNotice(),
+        const CookieNotice(host: 'docs.flutter.dev'),
         const DashHeader(),
         div(id: 'site-below-header', [
           div(id: 'site-main-row', [
diff --git a/site/lib/src/layouts/doc_layout.dart b/site/lib/src/layouts/doc_layout.dart
index cee1cb2ae74..1e81b3d525f 100644
--- a/site/lib/src/layouts/doc_layout.dart
+++ b/site/lib/src/layouts/doc_layout.dart
@@ -76,7 +76,6 @@ class DocLayout extends FlutterDocsLayout {
               ),
 
               child,
-
               PrevNext(
                 previousPage: PageNavigationEntry.fromData(pageData['prev']),
                 nextPage: PageNavigationEntry.fromData(pageData['next']),
diff --git a/site/lib/src/layouts/toc_layout.dart b/site/lib/src/layouts/toc_layout.dart
index 9401e9ffd23..85265f4f3fc 100644
--- a/site/lib/src/layouts/toc_layout.dart
+++ b/site/lib/src/layouts/toc_layout.dart
@@ -5,8 +5,8 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/card.dart';
 
-import '../components/common/card.dart';
 import 'doc_layout.dart';
 
 class TocLayout extends DocLayout {
diff --git a/site/lib/src/layouts/tutorial_layout.dart b/site/lib/src/layouts/tutorial_layout.dart
index c148b36fb4e..9567b718d43 100644
--- a/site/lib/src/layouts/tutorial_layout.dart
+++ b/site/lib/src/layouts/tutorial_layout.dart
@@ -4,8 +4,8 @@
 
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/tutorial/models/tutorial_model.dart';
 
-import '../models/tutorial_model.dart';
 import 'doc_layout.dart';
 
 class TutorialLayout extends DocLayout {
diff --git a/site/lib/src/pages/glossary.dart b/site/lib/src/pages/glossary.dart
index c3a2590159d..69a236a3c8a 100644
--- a/site/lib/src/pages/glossary.dart
+++ b/site/lib/src/pages/glossary.dart
@@ -5,11 +5,11 @@
 import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
+import 'package:site_shared/common/button.dart';
+import 'package:site_shared/common/card.dart';
+import 'package:site_shared/markdown/markdown_parser.dart';
 
-import '../components/common/button.dart';
-import '../components/common/card.dart';
 import '../components/pages/glossary_search_section.dart';
-import '../markdown/markdown_parser.dart';
 import '../util.dart';
 
 /// Different types of resources that glossary terms might link to.
diff --git a/site/lib/src/pages/widget_catalog.dart b/site/lib/src/pages/widget_catalog.dart
index 82d83955202..f4a8bb72c1c 100644
--- a/site/lib/src/pages/widget_catalog.dart
+++ b/site/lib/src/pages/widget_catalog.dart
@@ -5,9 +5,9 @@ import 'package:jaspr/dom.dart';
 import 'package:jaspr/jaspr.dart';
 import 'package:jaspr_content/jaspr_content.dart';
 import 'package:path/path.dart' as path;
+import 'package:site_shared/markdown/markdown_parser.dart';
 
 import '../components/pages/widget_catalog.dart';
-import '../markdown/markdown_parser.dart';
 import '../models/widget_catalog_model.dart';
 import '../util.dart';
 
diff --git a/site/lib/src/style_hash.dart b/site/lib/src/style_hash.dart
index ce114383cc9..bd8e030c02b 100644
--- a/site/lib/src/style_hash.dart
+++ b/site/lib/src/style_hash.dart
@@ -1,4 +1,4 @@
-// Generated by docs_flutter_dev_site|stylesHashBuilder. Do not edit.
+// Generated by site_shared|stylesHashBuilder. Do not edit.
 // dart format off
 
 /// The generated hash of the `main.css` file.
diff --git a/site/pubspec.yaml b/site/pubspec.yaml
index e037c9f2bc3..64fcb40d63b 100644
--- a/site/pubspec.yaml
+++ b/site/pubspec.yaml
@@ -7,29 +7,22 @@ environment:
   sdk: ^3.11.0
 
 dependencies:
-  build: ^4.0.4
   collection: ^1.19.1
-  crypto: ^3.0.7
   html: ^0.15.6
   http: ^1.6.0
   jaspr: ^0.22.3
   jaspr_content: ^0.5.0
   # Used as our template engine.
   liquify: 1.3.1
-  markdown: ^7.3.0
-  markdown_description_list: ^0.1.1
   meta: ^1.18.1
-  nanoid2: ^2.0.1
-  # Used for syntax highlighting.
-  opal: ^0.2.2
   path: ^1.9.1
   pub_semver: ^2.2.0
+  site_shared: any
   universal_web: ^1.1.1+1
   yaml: ^3.1.3
 
 dev_dependencies:
-  analysis_defaults:
-    path: ../packages/analysis_defaults
+  analysis_defaults: any
   build_runner: ^2.11.0
   build_web_compilers: ^4.4.9
   jaspr_builder: ^0.22.3