Skip to content

Commit

Permalink
🚧 algolia docsearch
Browse files Browse the repository at this point in the history
  • Loading branch information
Mortaro committed Aug 14, 2022
1 parent a4eb4b6 commit c965b8a
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 243 deletions.
8 changes: 0 additions & 8 deletions icons/Search.njs

This file was deleted.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"tailwind": "npx tailwindcss-cli build -o src/tailwind.css"
},
"dependencies": {
"@docsearch/js": "^3.2.0",
"@tailwindcss/typography": "^0.4.0",
"glob": "^8.0.1",
"nullstack-google-analytics": "github:Mortaro/nullstack-google-analytics#next",
Expand All @@ -29,4 +30,4 @@
"remarkable-meta": "^1.0.1",
"yaml": "^1.10.0"
}
}
}
116 changes: 16 additions & 100 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,26 @@
import { readdirSync, readFileSync, writeFileSync } from 'fs';
import { readdirSync } from 'fs';
import Nullstack from "nullstack";
import path from 'path';
import Application from "./src/Application";
import prismjs from 'prismjs';
import { Remarkable } from 'remarkable';
import meta from 'remarkable-meta';
import 'prismjs/components/prism-jsx.min';

const context = Nullstack.start(Application);

const { worker, project, environment } = context;

function slugify(string) {
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z ]/g, "").toLowerCase()
}

const locales = ['en-US', 'pt-BR']
context.articles = {}

for (const locale of locales) {
context.articles[locale] = {}
const articles = readdirSync(path.join(__dirname, `../i18n/${locale}`, 'articles'));
// preload files for workers
if (locale === 'en-US') {
const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations'));
worker.preload = [
...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1),
...illustrations.map((illustration) => '/illustrations/' + illustration),
'/en-US.json',
'/arrow.webp',
'/stars.webp',
'/footer.webp',
'/contributors',
'/roboto-v20-latin-300.woff2',
'/roboto-v20-latin-500.woff2',
'/crete-round-v9-latin-regular.woff2',
]
}
const map = {}
for (const article of articles) {
const content = readFileSync(path.join(__dirname, `../i18n/${locale}`, 'articles', article), 'utf-8')
// preload articles markdown
const md = new Remarkable({
highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript')
});
md.use(meta);
md.use((md) => {
const originalRender = md.renderer.rules.link_open;
md.renderer.rules.link_open = function () {
let result = originalRender.apply(null, arguments);
const regexp = /href="([^"]*)"/;
const href = regexp.exec(result)[1];
if (!href.startsWith('/')) {
result = result.replace('>', ' target="_blank" rel="noopener">');
}
return result;
};
});
md.use((md) => {
md.renderer.rules.heading_open = function (tokens, i) {
const { content } = tokens[i + 1];
const { hLevel } = tokens[i];
const id = content.toLowerCase().split(/[^a-z]/).join('-');
return `<h${hLevel} id="${id}"><a href="#${id}">`;
}
md.renderer.rules.heading_close = function (tokens, i) {
const { hLevel } = tokens[i];
return `</a></h${hLevel}>`;
}
});
context.articles[locale][article] = {
html: md.render(content),
...md.meta,
}
// generate word map for search
const lines = []
let shouldSkip = false
for (const line of content.split("\n")) {
if (line.startsWith('```')) {
shouldSkip = !shouldSkip
} else if (!shouldSkip && !(line.includes('[') && line.includes(']'))) {
lines.push(line)
}
}
const words = lines.join(" ").split(" ")
const wordMap = {}
for (const word of words) {
const slug = slugify(word)
if (!slug) continue
if (!wordMap[slug]) {
wordMap[slug] = 1
} else {
wordMap[slug]++
}
}
const key = article.replace('.md', '')
map[key] = {
...md.meta,
href: locale === 'en-US' ? `/${key}` : `/${locale.toLowerCase()}/${key}`,
words: wordMap
}
}
const json = environment.development ? JSON.stringify(map, null, 2) : JSON.stringify(map)
writeFileSync(`public/${locale}.json`, json)
}

const { worker, project } = context;

const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations'));
const articles = readdirSync(path.join(__dirname, `../i18n/en-US`, 'articles'));
worker.preload = [
...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1),
...illustrations.map((illustration) => '/illustrations/' + illustration),
'/arrow.webp',
'/stars.webp',
'/footer.webp',
'/contributors',
'/roboto-v20-latin-300.woff2',
'/roboto-v20-latin-500.woff2',
'/crete-round-v9-latin-regular.woff2',
]

project.name = 'Nullstack';
project.domain = 'nullstack.app';
Expand Down
6 changes: 2 additions & 4 deletions src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Footer from './Footer';
import Header from './Header';
import Home from './Home';
import Loader from './Loader';
import Search from './Search.njs';
import "./tailwind.css";
import Waifu from './Waifu';

Expand All @@ -34,6 +33,7 @@ class Application extends Nullstack {
if (localStorage['mode']) {
context.mode = localStorage['mode'];
if (context.mode === 'dark') {
document.querySelector('html').setAttribute('data-theme', context.mode)
context.oppositeMode = 'light';
}
}
Expand All @@ -53,13 +53,11 @@ class Application extends Nullstack {
render({ router, mode }) {
const locale = router.url.startsWith('/pt-br') ? 'pt-BR' : 'en-US';
return (
<body class={mode}>
<body data-theme={mode} class={mode}>
<div class="dark:bg-gray-900 dark:text-white">
<Header locale={locale} />
<HiringBanner />

<Search locale={locale} persistent key="search" />

<Home route="/" locale="en-US" persistent />
<Home route="/pt-br" locale="pt-BR" persistent />

Expand Down
47 changes: 42 additions & 5 deletions src/Article.njs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import Translatable from './Translatable';
import YAML from 'yaml';
import Arrow from '../icons/Arrow';
import './Article.scss';
import prismjs from 'prismjs';
import { Remarkable } from 'remarkable';
import meta from 'remarkable-meta';

class Article extends Translatable {

Expand All @@ -18,11 +21,45 @@ class Article extends Translatable {
}
}

static async getArticleByKey({ articles, locale, key }) {
if (articles[locale][`${key}.md`]) {
return articles[locale][`${key}.md`]
static async getArticleByKey({ locale, key }) {
await import('prismjs/components/prism-jsx.min');
let path = `i18n/${locale}/articles/${key}.md`;
if (!existsSync(path)) {
path = `i18n/${locale}/articles/404.md`;
}
const text = readFileSync(path, 'utf-8');
const md = new Remarkable({
highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript')
});
md.use(meta);
md.use((md) => {
const originalRender = md.renderer.rules.link_open;
md.renderer.rules.link_open = function () {
let result = originalRender.apply(null, arguments);
const regexp = /href="([^"]*)"/;
const href = regexp.exec(result)[1];
if (!href.startsWith('/')) {
result = result.replace('>', ' target="_blank" rel="noopener">');
}
return result;
};
});
md.use((md) => {
md.renderer.rules.heading_open = function (tokens, i) {
const { content } = tokens[i + 1];
const { hLevel } = tokens[i];
const id = content.toLowerCase().split(/[^a-z]/).join('-');
return `<h${hLevel} id="${id}"><a href="#${id}">`;
}
md.renderer.rules.heading_close = function (tokens, i) {
const { hLevel } = tokens[i];
return `</a></h${hLevel}>`;
}
});
return {
html: md.render(text),
...md.meta
}
return articles[locale][`404.md`]
}

static async getArticlesList({ locale }) {
Expand Down
38 changes: 21 additions & 17 deletions src/Header.njs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Brasil from "../icons/Brasil";
import Gringo from "../icons/Gringo";
import GitHub from "../icons/GitHub";
import Discord from "../icons/Discord";
import Search from "../icons/Search";
import docsearch from '@docsearch/js';
import '@docsearch/css';

class Header extends Translatable {

Expand Down Expand Up @@ -46,10 +47,16 @@ class Header extends Translatable {
context.mode = context.oppositeMode;
context.oppositeMode = nextOppositeMode;
window.localStorage.setItem('mode', context.mode);
document.querySelector('html').setAttribute('data-theme', context.mode)
}

toggleSearch({ instances }) {
instances.search.open()
startDocSearch(context) {
docsearch({
container: context.element,
appId: 'R2IYF7ETH7',
apiKey: '599cec31baffa4868cae4e79f180729b',
indexName: 'docsearch',
});
}

render({ mode, oppositeMode, locale }) {
Expand All @@ -62,17 +69,17 @@ class Header extends Translatable {
<a {...this.i18n.home}>
<Logo height="30" light={mode === "dark"} />
</a>
<div class="flex items-center sm:hidden">
<button onclick={this.toggleSearch} title={this.i18n.search.title} class="flex sm:hidden text-pink-600 h-10 w-10 items-center justify-center">
<Search size={25} />
</button>
<button
title={this.i18n.menu.title}
onclick={{ expanded: !this.expanded }}
>
{this.expanded && <Close size={25} class="text-gray-900 dark:text-white" />}
{!this.expanded && <Hamburger size={25} class="text-gray-900 dark:text-white" />}
</button>
<div class="flex gap-4">
<div id="docsearch" ref={this.startDocSearch} />
<div class="flex items-center sm:hidden">
<button
title={this.i18n.menu.title}
onclick={{ expanded: !this.expanded }}
>
{this.expanded && <Close size={25} class="text-gray-900 dark:text-white" />}
{!this.expanded && <Hamburger size={25} class="text-gray-900 dark:text-white" />}
</button>
</div>
</div>
</div>
<nav class={['flex items-center flex-wrap sm:px-0 mt-2 sm:mt-0', !this.expanded && 'hidden sm:flex']}>
Expand All @@ -81,9 +88,6 @@ class Header extends Translatable {
<Link onclick={this.toggleMode} title={this.i18n.mode[oppositeMode]} mobile />
</nav>
<div class={['flex w-full sm:w-auto mt-4 sm:mt-0 sm:space-x-2 items-center', !this.expanded && 'hidden sm:flex']}>
<button onclick={this.toggleSearch} title={this.i18n.search.title} class="hidden sm:flex text-pink-600 h-10 w-10 items-center justify-center">
<Search size={25} />
</button>
<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">
{locale === 'pt-BR' && <Gringo size={30} />}
{locale !== 'pt-BR' && <Brasil size={30} />}
Expand Down
Loading

0 comments on commit c965b8a

Please sign in to comment.