Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import '../dropdown.dart';
final class PageHeaderOptions extends StatefulComponent {
const PageHeaderOptions({
required this.title,
this.markdownUrl,
this.sourceUrl,
this.issueUrl,
super.key,
});

final String title;
final String? markdownUrl;
final String? sourceUrl;
final String? issueUrl;

Expand Down Expand Up @@ -91,12 +93,28 @@ final class _PageHeaderOptionsState extends State<PageHeaderOptions> {
),
],
),
if (component.markdownUrl case final markdownUrl?) ...[
li(
[
Button(
icon: 'markdown',
content: 'View as Markdown',
href: markdownUrl,
attributes: const {
'target': '_blank',
'rel': 'noopener',
},
),
],
),
if (component.issueUrl != null) const DropdownDivider(),
],
Comment thread
parlough marked this conversation as resolved.
if (component.sourceUrl case final sourceUrl?)
li(
[
Button(
icon: 'docs',
content: 'View source',
content: 'Open on GitHub',
href: sourceUrl,
attributes: const {
'target': '_blank',
Expand All @@ -110,7 +128,7 @@ final class _PageHeaderOptionsState extends State<PageHeaderOptions> {
[
Button(
icon: 'bug_report',
content: 'Report issue',
content: 'Report an issue',
href: issueUrl,
attributes: const {
'target': '_blank',
Expand Down
12 changes: 12 additions & 0 deletions packages/site_shared/lib/components/common/dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,15 @@ final class DropdownState extends State<Dropdown> {
);
}
}

/// A separator between groups of dropdown menu items.
final class DropdownDivider extends StatelessComponent {
const DropdownDivider({super.key});

@override
Component build(BuildContext _) => const Component.element(
tag: 'li',
classes: 'dropdown-divider',
attributes: {'aria-hidden': 'true', 'role': 'separator'},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ final class SiteSwitcher extends StatelessComponent {
subtype: 'Blog',
href: 'https://blog.flutter.dev',
),
const Component.element(
tag: 'li',
classes: 'dropdown-divider',
attributes: {'aria-hidden': 'true', 'role': 'separator'},
),
const DropdownDivider(),
],
_SiteWordMarkListEntry(
name: 'Dart',
Expand Down
4 changes: 4 additions & 0 deletions sites/docs/lib/_sass/components/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ main {
background-color: var(--site-raised-bgColor);
border: var(--site-outline) 1px solid;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);

.dropdown-divider {
background-color: var(--site-outline);
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions sites/docs/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ ClientOptions get defaultClientOptions => ClientOptions(
'site_shared:page_header_options': ClientLoader(
(p) => _page_header_options.PageHeaderOptions(
title: p['title'] as String,
markdownUrl: p['markdownUrl'] as String?,
sourceUrl: p['sourceUrl'] as String?,
issueUrl: p['issueUrl'] as String?,
),
Expand Down
19 changes: 2 additions & 17 deletions sites/docs/lib/main.server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import 'src/layouts/toc_layout.dart';
import 'src/layouts/tutorial_layout.dart';
import 'src/loaders/data_processor.dart';
import 'src/pages/custom_pages.dart';
import 'src/pages/markdown.dart';
import 'src/pages/robots_txt.dart';
import 'src/templating/dash_template_engine.dart';

Expand Down Expand Up @@ -82,23 +83,7 @@ Component get _docsFlutterDevSite => ContentApp.custom(
theme: const ContentTheme.none(),
secondaryOutputs: [
const RobotsTxtOutput(),
MarkdownOutput(
createHeader: (page) {
final header = StringBuffer();
if (page.data.page['title'] case final String title
when title.isNotEmpty) {
header.writeln('# $title');

if (page.data.page['description'] case final String description
when description.isNotEmpty) {
header.writeln();
header.writeln('> $description');
}
}

return header.toString();
},
),
canonicalMarkdownOutput,
],
),
);
Expand Down
7 changes: 6 additions & 1 deletion sites/docs/lib/main.server.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,12 @@ Map<String, Object?> __feedbackFeedbackComponent(
) => {'issueUrl': c.issueUrl};
Map<String, Object?> __page_header_optionsPageHeaderOptions(
_page_header_options.PageHeaderOptions c,
) => {'title': c.title, 'sourceUrl': c.sourceUrl, 'issueUrl': c.issueUrl};
) => {
'title': c.title,
'markdownUrl': c.markdownUrl,
'sourceUrl': c.sourceUrl,
'issueUrl': c.issueUrl,
};
Map<String, Object?> __simple_tooltipSimpleTooltip(
_simple_tooltip.SimpleTooltip c,
) => {'target': c.target.toId(), 'content': c.content.toId()};
Expand Down
5 changes: 4 additions & 1 deletion sites/docs/lib/src/components/common/page_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:site_shared/components/common/client/page_header_options.dart';
import 'package:site_shared/markdown.dart';
import 'package:site_shared/util.dart';

import '../../pages/markdown.dart';
import '../../utils/page_source_info.dart';

final class PageHeader extends StatelessComponent {
Expand All @@ -28,7 +29,8 @@ final class PageHeader extends StatelessComponent {

@override
Component build(BuildContext context) {
final sourceInfo = context.page.sourceInfo;
final page = context.page;
final sourceInfo = page.sourceInfo;

return header(
id: 'site-content-title',
Expand All @@ -47,6 +49,7 @@ final class PageHeader extends StatelessComponent {
),
PageHeaderOptions(
title: title,
markdownUrl: canonicalMarkdownOutput.routeForPage(page),
sourceUrl: sourceInfo.sourceUrl,
issueUrl: sourceInfo.issueUrl,
),
Expand Down
8 changes: 8 additions & 0 deletions sites/docs/lib/src/layouts/flutter_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../components/layout/footer.dart';
import '../components/layout/header.dart';
import '../components/layout/sidenav.dart';
import '../models/sidenav_model.dart';
import '../pages/markdown.dart';
import '../style_hash.dart';

/// The base Jaspr Content layout for wrapping site content.
Expand Down Expand Up @@ -57,6 +58,13 @@ abstract class FlutterDocsLayout extends DashLayout {
@override
Iterable<Component> buildExtraHead(Page page) {
return [
if (canonicalMarkdownOutput.routeForPage(page) case final markdownRoute?)
link(
rel: 'alternate',
type: 'text/markdown',
href: markdownRoute,
attributes: const {'title': 'Markdown'},
),
Builder(
builder: (context) {
final pageData = page.data.page;
Expand Down
47 changes: 47 additions & 0 deletions sites/docs/lib/src/pages/markdown.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2026 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_content/jaspr_content.dart';

/// The Markdown secondary output shared by routing and page layouts.
final canonicalMarkdownOutput = CanonicalMarkdownOutput(
createHeader: _createHeader,
);

/// Outputs a secondary Markdown file at the canonical page URL with a `.md`
/// suffix.
final class CanonicalMarkdownOutput extends MarkdownOutput {
CanonicalMarkdownOutput({super.createHeader});

/// Returns the secondary Markdown route for [page], if one is generated.
String? routeForPage(Page page) {
if (pattern.matchAsPrefix(page.path) == null) return null;
return createRoute(page.url);
}

@override
String createRoute(String route) {
// Keep Markdown URLs absolute and avoid a `/.md` suffix.
final absoluteRoute = route.startsWith('/') ? route : '/$route';
final normalizedRoute = absoluteRoute.endsWith('/')
? absoluteRoute.substring(0, absoluteRoute.length - 1)
: absoluteRoute;
return normalizedRoute.isEmpty ? '/index.md' : '$normalizedRoute.md';
}
Comment thread
parlough marked this conversation as resolved.
}

String _createHeader(Page page) {
final header = StringBuffer();
if (page.data.page['title'] case final String title when title.isNotEmpty) {
header.writeln('# $title');

if (page.data.page['description'] case final String description
when description.isNotEmpty) {
header.writeln();
header.writeln('> $description');
}
}

return header.toString();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: DevTools 2.21.1 release notes
shortTitle: 2.21.1 release notes
breadcrumb: 2.22.1
breadcrumb: 2.21.1
description: Release notes for Dart and Flutter DevTools version 2.21.1.
showToc: false
---
Expand Down
2 changes: 1 addition & 1 deletion tool/dash_site/lib/src/commands/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Future<int> buildSite(Site site, {required bool productionRelease}) async {
'--no-managed-build-options',
'--sitemap-domain=${site.baseUrl}',
// Exclude secondary Markdown output files from sitemap.
r'--sitemap-exclude=\.html\.md$',
r'--sitemap-exclude=\.md$',
'--dart-define=PRODUCTION=$productionRelease',
],
workingDirectory: site.directory,
Expand Down
Loading