Skip to content

Commit d23ce76

Browse files
JeanMecheAndrewKushnir
authored andcommitted
docs(docs-infra): leverage the search on 404
This introduces a search result matching the requested url when the page couldn't not be found.
1 parent 6f6b240 commit d23ce76

File tree

6 files changed

+294
-43
lines changed

6 files changed

+294
-43
lines changed

adev/shared-docs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
export * from './directives/index';
1414
export * from './components/index';
1515
export * from './interfaces/index';
16+
export * from './pipes/index';
1617
export * from './providers/index';
1718
export * from './services/index';
1819
export * from './utils/index';

adev/shared-docs/services/search.service.ts

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -53,47 +53,7 @@ export class Search {
5353
loader: async ({params: query, abortSignal}) => {
5454
// Until we have a better alternative we debounce by awaiting for a short delay.
5555
await wait(SEARCH_DELAY, abortSignal);
56-
57-
return this.client
58-
.search([
59-
{
60-
indexName: this.config.algolia.indexName,
61-
params: {
62-
query: query,
63-
maxValuesPerFacet: MAX_VALUE_PER_FACET,
64-
attributesToRetrieve: [
65-
'hierarchy.lvl0',
66-
'hierarchy.lvl1',
67-
'hierarchy.lvl2',
68-
'hierarchy.lvl3',
69-
'hierarchy.lvl4',
70-
'hierarchy.lvl5',
71-
'hierarchy.lvl6',
72-
'content',
73-
'type',
74-
'url',
75-
],
76-
hitsPerPage: 20,
77-
snippetEllipsisText: '…',
78-
highlightPreTag: '<ɵ>',
79-
highlightPostTag: '</ɵ>',
80-
attributesToHighlight: [],
81-
attributesToSnippet: [
82-
'hierarchy.lvl1:10',
83-
'hierarchy.lvl2:10',
84-
'hierarchy.lvl3:10',
85-
'hierarchy.lvl4:10',
86-
'hierarchy.lvl5:10',
87-
'hierarchy.lvl6:10',
88-
'content:10',
89-
],
90-
},
91-
type: 'default',
92-
},
93-
])
94-
.then((response: SearchResponses<unknown>) => {
95-
return this.parseResult(response);
96-
});
56+
return this.searchWithQuery(query);
9757
},
9858
});
9959

@@ -208,6 +168,49 @@ export class Search {
208168
})
209169
.join('');
210170
}
171+
172+
public searchWithQuery(query: string): Promise<SearchResultItem[] | undefined> {
173+
return this.client
174+
.search([
175+
{
176+
indexName: this.config.algolia.indexName,
177+
params: {
178+
query: query,
179+
maxValuesPerFacet: MAX_VALUE_PER_FACET,
180+
attributesToRetrieve: [
181+
'hierarchy.lvl0',
182+
'hierarchy.lvl1',
183+
'hierarchy.lvl2',
184+
'hierarchy.lvl3',
185+
'hierarchy.lvl4',
186+
'hierarchy.lvl5',
187+
'hierarchy.lvl6',
188+
'content',
189+
'type',
190+
'url',
191+
],
192+
hitsPerPage: 20,
193+
snippetEllipsisText: '…',
194+
highlightPreTag: '<ɵ>',
195+
highlightPostTag: '</ɵ>',
196+
attributesToHighlight: [],
197+
attributesToSnippet: [
198+
'hierarchy.lvl1:10',
199+
'hierarchy.lvl2:10',
200+
'hierarchy.lvl3:10',
201+
'hierarchy.lvl4:10',
202+
'hierarchy.lvl5:10',
203+
'hierarchy.lvl6:10',
204+
'content:10',
205+
],
206+
},
207+
type: 'default',
208+
},
209+
])
210+
.then((response: SearchResponses<unknown>) => {
211+
return this.parseResult(response);
212+
});
213+
}
211214
}
212215

213216
function matched(snippet: SnippetResult | undefined): boolean {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<main class="docs-viewer">
2+
<header class="docs-header">
3+
<div class="docs-page-title">
4+
<h1 tabindex="-1"><span class="docs-emoji">Page Not Found 🙃</span></h1>
5+
</div>
6+
</header>
7+
<p>
8+
We couldn't find what you were looking for. We have initiated a search for the term extracted
9+
from the URL.
10+
</p>
11+
<p>
12+
If you think this is a mistake, please
13+
<a
14+
href="https://github.com/angular/angular/issues/new?template=3-docs-bug.yaml"
15+
target="_blank"
16+
>
17+
open an issue</a
18+
>
19+
so we can fix it.
20+
</p>
21+
22+
<!-- Search results that match the queried URL that we couldn't find -->
23+
@if (searchResults().length > 0) {
24+
<!-- The search results are faded in if/once we have results -->
25+
<div animate.enter="enter-animation">
26+
<br /><br />
27+
<h2>Feeling lucky?</h2>
28+
<p>Maybe what you're looking for is in the list below:</p>
29+
30+
<ul class="docs-search-results docs-mini-scroll-track">
31+
@for (result of searchResults(); track $index) {
32+
<li docsSearchItem [item]="result" class="docs-search-result">
33+
<a
34+
[relativeTo]="null"
35+
[routerLink]="'/' + result.url | relativeLink: 'pathname'"
36+
[fragment]="result.url | relativeLink: 'hash'"
37+
>
38+
<div>
39+
<p class="docs-search-result__label">
40+
<!-- Icon -->
41+
<span class="docs-search-result-icon" aria-hidden="true">
42+
<i role="presentation" class="material-symbols-outlined docs-icon-small">
43+
{{ result.type === 'code' ? 'code' : 'description' }}
44+
</i>
45+
</span>
46+
47+
<!-- Page title -->
48+
<span [innerHtml]="result.labelHtml"></span>
49+
@if (result.package) {
50+
<span
51+
[innerHTML]="result.package"
52+
class="docs-search-result__label__package"
53+
></span>
54+
}
55+
</p>
56+
57+
@if (result.subLabelHtml) {
58+
<p class="docs-search-result__sub-label">
59+
<span class="docs-search-result-icon" aria-hidden="true">
60+
<i role="presentation" class="material-symbols-outlined docs-icon-small">
61+
grid_3x3
62+
</i>
63+
</span>
64+
<span [innerHtml]="result.subLabelHtml"></span>
65+
</p>
66+
}
67+
68+
@if (result.contentHtml) {
69+
<p class="docs-search-result__content" [innerHtml]="result.contentHtml"></p>
70+
}
71+
</div>
72+
<p class="docs-search-result__category">
73+
{{ result.category }}
74+
</p>
75+
</a>
76+
</li>
77+
}
78+
</ul>
79+
</div>
80+
}
81+
</main>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
:host {
2+
display: block;
3+
padding-top: var(--layout-padding);
4+
padding-bottom: var(--layout-padding);
5+
}
6+
7+
.docs-viewer {
8+
display: flex;
9+
flex-direction: column;
10+
padding: 0px;
11+
box-sizing: border-box;
12+
padding-inline: var(--layout-padding);
13+
}
14+
15+
.enter-animation {
16+
animation: slide-fade 1s;
17+
}
18+
@keyframes slide-fade {
19+
from {
20+
opacity: 0;
21+
transform: translateY(20px);
22+
}
23+
to {
24+
opacity: 1;
25+
transform: translateY(0);
26+
}
27+
}
28+
29+
.docs-search-results {
30+
list-style-type: none;
31+
padding-inline: 0;
32+
padding-block-start: 1rem;
33+
padding-block-end: 1rem;
34+
margin: 0;
35+
max-width: 750px;
36+
37+
.docs-search-result {
38+
border-inline-start: 2px solid var(--senary-contrast);
39+
margin-inline-start: 1rem;
40+
display: block;
41+
font-size: 0.875rem;
42+
43+
.docs-search-result-icon {
44+
display: inline-block;
45+
46+
i {
47+
display: flex;
48+
align-items: center;
49+
font-size: 1.2rem;
50+
}
51+
}
52+
53+
/**
54+
* This rule needs ng-deep to be applied to elements that are created via a [innerHTML] binding
55+
*/
56+
::ng-deep {
57+
mark {
58+
background: #e62600;
59+
background: var(--red-to-orange-horizontal-gradient);
60+
background-clip: text;
61+
-webkit-background-clip: text;
62+
color: transparent;
63+
}
64+
}
65+
66+
a {
67+
color: var(--secondary-contrast);
68+
display: flex;
69+
justify-content: space-between;
70+
padding: 1rem;
71+
gap: 0.5rem;
72+
}
73+
74+
&__label,
75+
&__sub-label,
76+
&__content {
77+
transition: color 0.3s ease;
78+
}
79+
80+
&__label,
81+
&__sub-label {
82+
display: flex;
83+
align-items: center;
84+
gap: 0.75rem;
85+
margin: 0;
86+
}
87+
88+
&__label {
89+
font-weight: 600;
90+
flex-wrap: wrap;
91+
92+
&__package {
93+
font-size: 0.75rem;
94+
}
95+
}
96+
97+
&__content,
98+
&__category {
99+
margin: 0;
100+
}
101+
102+
&__sub-label,
103+
&__content {
104+
margin-block-start: 0.375rem;
105+
margin-inline-start: 2rem;
106+
color: var(--quaternary-contrast);
107+
}
108+
109+
&__category {
110+
font-weight: 400;
111+
color: var(--quaternary-contrast);
112+
}
113+
114+
&.active {
115+
background-color: var(--septenary-contrast); // stylelint-disable-line
116+
}
117+
118+
&:hover,
119+
&.active {
120+
background-color: var(--octonary-contrast); // stylelint-disable-line
121+
border-inline-start: 2px solid var(--primary-contrast);
122+
123+
.docs-search-result__label,
124+
.docs-search-result__sub-label,
125+
.docs-search-result__content {
126+
color: var(--primary-contrast);
127+
}
128+
}
129+
}
130+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Component, inject, signal} from '@angular/core';
10+
import {ActivatedRoute, RouterLink, UrlSegment} from '@angular/router';
11+
import {Search, SearchItem, RelativeLink, SearchResultItem} from '@angular/docs';
12+
13+
@Component({
14+
imports: [SearchItem, RouterLink, RelativeLink],
15+
templateUrl: './not-found.html',
16+
styleUrl: './not-found.scss',
17+
})
18+
export class NotFound {
19+
private readonly search = inject(Search);
20+
protected readonly searchResults = signal<SearchResultItem[]>([]);
21+
22+
constructor() {
23+
const activatedRoute = inject(ActivatedRoute);
24+
const searchTerms = this.extractSearchTerm(activatedRoute.snapshot.url);
25+
if (searchTerms) {
26+
// We're using the one-shot query request to not interfere with the main search signal
27+
this.search.searchWithQuery(searchTerms).then((results) => {
28+
this.searchResults.set(results ?? []);
29+
});
30+
}
31+
}
32+
33+
private extractSearchTerm(url: UrlSegment[]): string {
34+
return url.join(' ').replace(/[-_/]/g, ' ');
35+
}
36+
}

adev/src/app/routing/routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export const routes: Route[] = [
158158
// Error page
159159
{
160160
path: '**',
161-
loadComponent: () => import('../features/docs/docs.component'),
162-
resolve: {'docContent': contentResolver('error')},
161+
loadComponent: () => import('../features/not-found/not-found').then((m) => m.NotFound),
162+
data: {label: 'Page not found'},
163163
},
164164
];

0 commit comments

Comments
 (0)