Skip to content

Commit 8ea800d

Browse files
makishvili3y3
authored andcommitted
feat(anchors): Revert rel=nofollow and add invisible title for anchor links
1 parent c8e8f0a commit 8ea800d

File tree

3 files changed

+57
-21
lines changed

3 files changed

+57
-21
lines changed

src/scss/_common.scss

+25
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,31 @@
341341
line-height: 0;
342342
}
343343

344+
/*
345+
Hides element visually, but leaves it visible for search crawlers and screen readers
346+
347+
https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
348+
https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
349+
*/
350+
.visually-hidden {
351+
position: absolute;
352+
353+
overflow: hidden;
354+
clip: rect(0 0 0 0);
355+
356+
width: 1px;
357+
height: 1px;
358+
margin: -1px;
359+
360+
padding: 0;
361+
362+
white-space: nowrap;
363+
364+
border: 0;
365+
366+
clip-path: inset(100%);
367+
}
368+
344369
// highlight.js colors
345370
--yfm-color-hljs-background: #{$codeBackgroundColor};
346371
--yfm-color-hljs-subst: #444;

src/transform/plugins/anchors/index.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import {MarkdownItPluginCb} from '../typings';
99

1010
const slugify: (str: string, opts: {}) => string = require('slugify');
1111

12-
function createLinkTokens(state: StateCore, id: string, setId = false) {
12+
function createLinkTokens(state: StateCore, id: string, title: string, setId = false) {
1313
const open = new state.Token('link_open', 'a', 1);
14-
const text = new state.Token('text', '', 0);
1514
const close = new state.Token('link_close', 'a', -1);
1615

1716
if (setId) {
@@ -20,10 +19,15 @@ function createLinkTokens(state: StateCore, id: string, setId = false) {
2019
open.attrSet('href', '#' + id);
2120
open.attrSet('class', 'yfm-anchor');
2221
open.attrSet('aria-hidden', 'true');
23-
open.attrSet('rel', 'nofollow');
24-
text.content = '';
2522

26-
return [open, text, close];
23+
// SEO: render invisible heading title because link must have text content.
24+
const spanOpen = new state.Token('span_open', 'span', 1);
25+
const spanText = new state.Token('text', '', 0);
26+
const spanClose = new state.Token('span_close', 'span', -1);
27+
spanOpen.attrSet('class', 'visually-hidden');
28+
spanText.content = title;
29+
30+
return [open, spanOpen, spanText, spanClose, close];
2731
}
2832

2933
const getCustomIds = (content: string) => {
@@ -123,15 +127,15 @@ const index: MarkdownItPluginCb<Options> = (
123127
}
124128

125129
const allAnchorIds = customIds ? customIds : [id];
126-
130+
const anchorTitle = removeCustomId(title).replace(/`/g, '');
127131
allAnchorIds.forEach((customId) => {
128132
const setId = id !== customId;
129-
const linkTokens = createLinkTokens(state, customId, setId);
133+
const linkTokens = createLinkTokens(state, customId, anchorTitle, setId);
130134

131135
inlineToken.children?.unshift(...linkTokens);
132136

133137
if (supportGithubAnchors) {
134-
const ghLinkTokens = createLinkTokens(state, ghId, true);
138+
const ghLinkTokens = createLinkTokens(state, ghId, anchorTitle, true);
135139
inlineToken.children?.unshift(...ghLinkTokens);
136140
}
137141
});

test/anchors.test.ts

+20-13
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,26 @@ describe('Anchors', () => {
2222
log.clear();
2323
});
2424

25+
it('should add single anchor with auto naming', () => {
26+
expect(transformYfm('## Test\n' + '\n' + 'Content\n')).toBe(
27+
'<h2 id="test"><a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
28+
'<p>Content</p>\n',
29+
);
30+
});
31+
2532
it('should add single anchor', () => {
2633
expect(transformYfm('## Test {#test1}\n' + '\n' + 'Content\n')).toBe(
27-
'<h2 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
34+
'<h2 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
2835
'<p>Content</p>\n',
2936
);
3037
});
3138

3239
it('should add multiple anchors', () => {
3340
expect(transformYfm('## Test {#test1} {#test2} {#test3}\n' + '\n' + 'Content\n')).toBe(
3441
'<h2 id="test1">' +
35-
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
36-
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
37-
'<a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
42+
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
43+
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
44+
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
3845
'<p>Content</p>\n',
3946
);
4047
});
@@ -49,9 +56,9 @@ describe('Anchors', () => {
4956
'{% include [test](./mocks/include-anchor.md) %}\n',
5057
),
5158
).toBe(
52-
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
59+
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
5360
'<p>Content before include</p>\n' +
54-
'<h1 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Title</h1>\n' +
61+
'<h1 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
5562
'<p>Content</p>\n',
5663
);
5764
});
@@ -66,20 +73,20 @@ describe('Anchors', () => {
6673
'{% include [test](./mocks/include-multiple-anchors.md) %}\n',
6774
),
6875
).toBe(
69-
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
76+
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
7077
'<p>Content before include</p>\n' +
7178
'<h1 id="test1">' +
72-
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
73-
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
74-
'<a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Title</h1>\n' +
79+
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
80+
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
81+
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
7582
'<p>Content</p>\n',
7683
);
7784
});
7885

7986
it('should be transliterated correctly', () => {
8087
expect(transformYfm('## Максимальный размер дисков \n' + '\n' + 'Content\n')).toBe(
8188
'<h2 id="maksimalnyj-razmer-diskov">' +
82-
'<a href="#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
89+
'<a href="#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Максимальный размер дисков</span></a>' +
8390
'Максимальный размер дисков' +
8491
'</h2>\n' +
8592
'<p>Content</p>\n',
@@ -89,7 +96,7 @@ describe('Anchors', () => {
8996
it('should be removed fences after transliteration', () => {
9097
expect(transformYfm('## `Test`\n' + '\n' + 'Content\n')).toBe(
9198
'<h2 id="test">' +
92-
'<a href="#test" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a><code>Test</code>' +
99+
'<a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a><code>Test</code>' +
93100
'</h2>\n' +
94101
'<p>Content</p>\n',
95102
);
@@ -106,7 +113,7 @@ describe('Anchors', () => {
106113
),
107114
).toBe(
108115
'<p>Content before include</p>\n' +
109-
'<h2 id="anchor"><a href="#anchor" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Subtitle</h2>\n' +
116+
'<h2 id="anchor"><a href="#anchor" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Subtitle</span></a>Subtitle</h2>\n' +
110117
'<p>Subcontent</p>\n' +
111118
'<p>After include</p>\n',
112119
);

0 commit comments

Comments
 (0)