Skip to content

Commit c965b8a

Browse files
committed
🚧 algolia docsearch
1 parent a4eb4b6 commit c965b8a

File tree

7 files changed

+83
-243
lines changed

7 files changed

+83
-243
lines changed

icons/Search.njs

Lines changed: 0 additions & 8 deletions
This file was deleted.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"tailwind": "npx tailwindcss-cli build -o src/tailwind.css"
1919
},
2020
"dependencies": {
21+
"@docsearch/js": "^3.2.0",
2122
"@tailwindcss/typography": "^0.4.0",
2223
"glob": "^8.0.1",
2324
"nullstack-google-analytics": "github:Mortaro/nullstack-google-analytics#next",
@@ -29,4 +30,4 @@
2930
"remarkable-meta": "^1.0.1",
3031
"yaml": "^1.10.0"
3132
}
32-
}
33+
}

server.js

Lines changed: 16 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,26 @@
1-
import { readdirSync, readFileSync, writeFileSync } from 'fs';
1+
import { readdirSync } from 'fs';
22
import Nullstack from "nullstack";
33
import path from 'path';
44
import Application from "./src/Application";
5-
import prismjs from 'prismjs';
6-
import { Remarkable } from 'remarkable';
7-
import meta from 'remarkable-meta';
85
import 'prismjs/components/prism-jsx.min';
96

107
const context = Nullstack.start(Application);
118

12-
const { worker, project, environment } = context;
13-
14-
function slugify(string) {
15-
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z ]/g, "").toLowerCase()
16-
}
17-
18-
const locales = ['en-US', 'pt-BR']
19-
context.articles = {}
20-
21-
for (const locale of locales) {
22-
context.articles[locale] = {}
23-
const articles = readdirSync(path.join(__dirname, `../i18n/${locale}`, 'articles'));
24-
// preload files for workers
25-
if (locale === 'en-US') {
26-
const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations'));
27-
worker.preload = [
28-
...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1),
29-
...illustrations.map((illustration) => '/illustrations/' + illustration),
30-
'/en-US.json',
31-
'/arrow.webp',
32-
'/stars.webp',
33-
'/footer.webp',
34-
'/contributors',
35-
'/roboto-v20-latin-300.woff2',
36-
'/roboto-v20-latin-500.woff2',
37-
'/crete-round-v9-latin-regular.woff2',
38-
]
39-
}
40-
const map = {}
41-
for (const article of articles) {
42-
const content = readFileSync(path.join(__dirname, `../i18n/${locale}`, 'articles', article), 'utf-8')
43-
// preload articles markdown
44-
const md = new Remarkable({
45-
highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript')
46-
});
47-
md.use(meta);
48-
md.use((md) => {
49-
const originalRender = md.renderer.rules.link_open;
50-
md.renderer.rules.link_open = function () {
51-
let result = originalRender.apply(null, arguments);
52-
const regexp = /href="([^"]*)"/;
53-
const href = regexp.exec(result)[1];
54-
if (!href.startsWith('/')) {
55-
result = result.replace('>', ' target="_blank" rel="noopener">');
56-
}
57-
return result;
58-
};
59-
});
60-
md.use((md) => {
61-
md.renderer.rules.heading_open = function (tokens, i) {
62-
const { content } = tokens[i + 1];
63-
const { hLevel } = tokens[i];
64-
const id = content.toLowerCase().split(/[^a-z]/).join('-');
65-
return `<h${hLevel} id="${id}"><a href="#${id}">`;
66-
}
67-
md.renderer.rules.heading_close = function (tokens, i) {
68-
const { hLevel } = tokens[i];
69-
return `</a></h${hLevel}>`;
70-
}
71-
});
72-
context.articles[locale][article] = {
73-
html: md.render(content),
74-
...md.meta,
75-
}
76-
// generate word map for search
77-
const lines = []
78-
let shouldSkip = false
79-
for (const line of content.split("\n")) {
80-
if (line.startsWith('```')) {
81-
shouldSkip = !shouldSkip
82-
} else if (!shouldSkip && !(line.includes('[') && line.includes(']'))) {
83-
lines.push(line)
84-
}
85-
}
86-
const words = lines.join(" ").split(" ")
87-
const wordMap = {}
88-
for (const word of words) {
89-
const slug = slugify(word)
90-
if (!slug) continue
91-
if (!wordMap[slug]) {
92-
wordMap[slug] = 1
93-
} else {
94-
wordMap[slug]++
95-
}
96-
}
97-
const key = article.replace('.md', '')
98-
map[key] = {
99-
...md.meta,
100-
href: locale === 'en-US' ? `/${key}` : `/${locale.toLowerCase()}/${key}`,
101-
words: wordMap
102-
}
103-
}
104-
const json = environment.development ? JSON.stringify(map, null, 2) : JSON.stringify(map)
105-
writeFileSync(`public/${locale}.json`, json)
106-
}
107-
9+
const { worker, project } = context;
10+
11+
const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations'));
12+
const articles = readdirSync(path.join(__dirname, `../i18n/en-US`, 'articles'));
13+
worker.preload = [
14+
...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1),
15+
...illustrations.map((illustration) => '/illustrations/' + illustration),
16+
'/arrow.webp',
17+
'/stars.webp',
18+
'/footer.webp',
19+
'/contributors',
20+
'/roboto-v20-latin-300.woff2',
21+
'/roboto-v20-latin-500.woff2',
22+
'/crete-round-v9-latin-regular.woff2',
23+
]
10824

10925
project.name = 'Nullstack';
11026
project.domain = 'nullstack.app';

src/Application.njs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Footer from './Footer';
99
import Header from './Header';
1010
import Home from './Home';
1111
import Loader from './Loader';
12-
import Search from './Search.njs';
1312
import "./tailwind.css";
1413
import Waifu from './Waifu';
1514

@@ -34,6 +33,7 @@ class Application extends Nullstack {
3433
if (localStorage['mode']) {
3534
context.mode = localStorage['mode'];
3635
if (context.mode === 'dark') {
36+
document.querySelector('html').setAttribute('data-theme', context.mode)
3737
context.oppositeMode = 'light';
3838
}
3939
}
@@ -53,13 +53,11 @@ class Application extends Nullstack {
5353
render({ router, mode }) {
5454
const locale = router.url.startsWith('/pt-br') ? 'pt-BR' : 'en-US';
5555
return (
56-
<body class={mode}>
56+
<body data-theme={mode} class={mode}>
5757
<div class="dark:bg-gray-900 dark:text-white">
5858
<Header locale={locale} />
5959
<HiringBanner />
6060

61-
<Search locale={locale} persistent key="search" />
62-
6361
<Home route="/" locale="en-US" persistent />
6462
<Home route="/pt-br" locale="pt-BR" persistent />
6563

src/Article.njs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { readFileSync } from 'fs';
1+
import { readFileSync, existsSync } from 'fs';
22
import Translatable from './Translatable';
33
import YAML from 'yaml';
44
import Arrow from '../icons/Arrow';
55
import './Article.scss';
6+
import prismjs from 'prismjs';
7+
import { Remarkable } from 'remarkable';
8+
import meta from 'remarkable-meta';
69

710
class Article extends Translatable {
811

@@ -18,11 +21,45 @@ class Article extends Translatable {
1821
}
1922
}
2023

21-
static async getArticleByKey({ articles, locale, key }) {
22-
if (articles[locale][`${key}.md`]) {
23-
return articles[locale][`${key}.md`]
24+
static async getArticleByKey({ locale, key }) {
25+
await import('prismjs/components/prism-jsx.min');
26+
let path = `i18n/${locale}/articles/${key}.md`;
27+
if (!existsSync(path)) {
28+
path = `i18n/${locale}/articles/404.md`;
29+
}
30+
const text = readFileSync(path, 'utf-8');
31+
const md = new Remarkable({
32+
highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript')
33+
});
34+
md.use(meta);
35+
md.use((md) => {
36+
const originalRender = md.renderer.rules.link_open;
37+
md.renderer.rules.link_open = function () {
38+
let result = originalRender.apply(null, arguments);
39+
const regexp = /href="([^"]*)"/;
40+
const href = regexp.exec(result)[1];
41+
if (!href.startsWith('/')) {
42+
result = result.replace('>', ' target="_blank" rel="noopener">');
43+
}
44+
return result;
45+
};
46+
});
47+
md.use((md) => {
48+
md.renderer.rules.heading_open = function (tokens, i) {
49+
const { content } = tokens[i + 1];
50+
const { hLevel } = tokens[i];
51+
const id = content.toLowerCase().split(/[^a-z]/).join('-');
52+
return `<h${hLevel} id="${id}"><a href="#${id}">`;
53+
}
54+
md.renderer.rules.heading_close = function (tokens, i) {
55+
const { hLevel } = tokens[i];
56+
return `</a></h${hLevel}>`;
57+
}
58+
});
59+
return {
60+
html: md.render(text),
61+
...md.meta
2462
}
25-
return articles[locale][`404.md`]
2663
}
2764

2865
static async getArticlesList({ locale }) {

src/Header.njs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import Brasil from "../icons/Brasil";
77
import Gringo from "../icons/Gringo";
88
import GitHub from "../icons/GitHub";
99
import Discord from "../icons/Discord";
10-
import Search from "../icons/Search";
10+
import docsearch from '@docsearch/js';
11+
import '@docsearch/css';
1112

1213
class Header extends Translatable {
1314

@@ -46,10 +47,16 @@ class Header extends Translatable {
4647
context.mode = context.oppositeMode;
4748
context.oppositeMode = nextOppositeMode;
4849
window.localStorage.setItem('mode', context.mode);
50+
document.querySelector('html').setAttribute('data-theme', context.mode)
4951
}
5052

51-
toggleSearch({ instances }) {
52-
instances.search.open()
53+
startDocSearch(context) {
54+
docsearch({
55+
container: context.element,
56+
appId: 'R2IYF7ETH7',
57+
apiKey: '599cec31baffa4868cae4e79f180729b',
58+
indexName: 'docsearch',
59+
});
5360
}
5461

5562
render({ mode, oppositeMode, locale }) {
@@ -62,17 +69,17 @@ class Header extends Translatable {
6269
<a {...this.i18n.home}>
6370
<Logo height="30" light={mode === "dark"} />
6471
</a>
65-
<div class="flex items-center sm:hidden">
66-
<button onclick={this.toggleSearch} title={this.i18n.search.title} class="flex sm:hidden text-pink-600 h-10 w-10 items-center justify-center">
67-
<Search size={25} />
68-
</button>
69-
<button
70-
title={this.i18n.menu.title}
71-
onclick={{ expanded: !this.expanded }}
72-
>
73-
{this.expanded && <Close size={25} class="text-gray-900 dark:text-white" />}
74-
{!this.expanded && <Hamburger size={25} class="text-gray-900 dark:text-white" />}
75-
</button>
72+
<div class="flex gap-4">
73+
<div id="docsearch" ref={this.startDocSearch} />
74+
<div class="flex items-center sm:hidden">
75+
<button
76+
title={this.i18n.menu.title}
77+
onclick={{ expanded: !this.expanded }}
78+
>
79+
{this.expanded && <Close size={25} class="text-gray-900 dark:text-white" />}
80+
{!this.expanded && <Hamburger size={25} class="text-gray-900 dark:text-white" />}
81+
</button>
82+
</div>
7683
</div>
7784
</div>
7885
<nav class={['flex items-center flex-wrap sm:px-0 mt-2 sm:mt-0', !this.expanded && 'hidden sm:flex']}>
@@ -81,9 +88,6 @@ class Header extends Translatable {
8188
<Link onclick={this.toggleMode} title={this.i18n.mode[oppositeMode]} mobile />
8289
</nav>
8390
<div class={['flex w-full sm:w-auto mt-4 sm:mt-0 sm:space-x-2 items-center', !this.expanded && 'hidden sm:flex']}>
84-
<button onclick={this.toggleSearch} title={this.i18n.search.title} class="hidden sm:flex text-pink-600 h-10 w-10 items-center justify-center">
85-
<Search size={25} />
86-
</button>
8791
<a href={this.i18n.language.href} title={this.i18n.language.title} class="hidden sm:flex text-pink-600 h-10 w-10 items-center justify-center">
8892
{locale === 'pt-BR' && <Gringo size={30} />}
8993
{locale !== 'pt-BR' && <Brasil size={30} />}

0 commit comments

Comments
 (0)