Unverified Commit 4a3e03ad authored by Pavel Ševčík's avatar Pavel Ševčík
Browse files

Added blog, added RSS, advanced SEO

parent ddf2674a
......@@ -6,3 +6,9 @@ node_modules
out
!src
# These files are generated during the build
public/robots.txt
public/sitemap.xml
public/rss.xml
---
title: 'What is RoninDojo'
excerpt: 'If you are reading this you are likely already a Samourai Wallet user who is looking to enhance your privacy and be a sovereign individual in the Bitcoin world. That is exactly what we stand for here at RoninDojo, we build tools to enhance your ability to fight for your privacy. In the wake of the COVID pandemic and government lockdowns, our privacy and our data is more valuable and sacred than ever. You should not only care about who knows that you have bitcoin but also how to obfuscate your transactions, manage all of your own XPUBs, verify all of our transactions, and truly be your own bank. This post will dive into who we are at RoninDojo, our history, and why you should in fact run your own RoninDojo.'
date: '2021-06-15T08:00:00'
author:
name: BTCxZelko
---
If you are reading this you are likely already a **Samourai Wallet** user who is looking to enhance your privacy and be a sovereign individual in the Bitcoin world. That is exactly what we stand for here at RoninDojo, we build tools to enhance your ability to fight for your **privacy**. In the wake of the COVID pandemic and government lockdowns, our privacy and our data is more valuable and sacred than ever. You should not only care about who knows that you have bitcoin but also how to obfuscate your transactions, manage all of your own XPUBs, verify all of our transactions, and truly be your own bank. This post will dive into who we are at **RoninDojo**, our history, and why you should in fact run your own RoninDojo.
## History
In the summer of 2018, scrolling thru Bitcoin Twitter I saw a fierce debate over the best coinjoin implementation between **Wasabi and Whirlpool**. Noticing that I had already had a Samourai Wallet installed, I figured I would try to get Whirlpool running. Not being a person with literally ANY technical knowledge or skills, to say that least it was difficult. Being on the Samourai Wallet telegram group I had plenty of hand holding and got my first mixes done and **I began my journey**. Now that I had a small taste of privacy, I was addicted and wanted more, and when Dojo 1.0 was released, I had to have it. Tirelessly attempting to get it running on a Raspberry Pi 4 was difficult to say the least. Once I had it running and spent time helping other get it running as well, I decided to start writing guides on how to do this. That is when I met my now Co-Founder, **s2l1/ GuerraMoneta**. Together we wrote guides on how to install Dojo on a Pi 4 and Odriod N2, get whirlpool-cli running, connect Electrum Rust Server and RPC Explorer. Neither one of us had any coding backgrounds, but we were determined to make this experience easier for everyone else. **That was when RoninDojo was born**.
Since then we began automating the experience, utilizing **Manjaro-ARM** as a powerful base Operating System needed for Dojo and supported nearly every Single Board Computer (SBC). We began learning bash and scripts and adding everything together. Eventually, we gained community support and added likewhoa and Pavel Sevcik as added help for backend and frontend development.
## Why RoninDojo?
Choosing RoninDojo is simple: If you are a Samourai user this is the only node you’ll need. We work directly with the Samourai team on optimizations, feature requests, and implementations together. We offer a stack that is extremely focused and pointed at what you will actually use and need as a **privacy-focused Bitcoiner**.
While we could take a “kitchen sink” approach and add every bitcoin node app, we selectly have chosen what is best to use. Things like Lightning Network support not only are not fully developed yet, most people who install them don’t use them as their primary transaction method. To be direct, it is mostly novelty tinkering at this point. I do however, tip my hat to anyone developing on LN and on projects they believe will better the bitcoin space. But until Samourai integrates LN we see no need to support it.
Another major difference between RoninDojo and other node implementation projects is that our system is designed as **Dojo first**. This means that we build around the default installation setup, the way the Dojo Development team designed the program to be used. Rather than just adding it to their existing stack. This is a very important distinction, as adding it an existing stack can cause a multitude of issues if the maintainers of the project are not well versed in the project, which to my knowledge none have been involved with Dojo as long as s2l1 or myself.
The last thing to note, we built this project as non-technical users who had no command line experience, we learned everything you see implemented today. We believe in the importance of **education** and learning how things work and what is happening under the hood. As a result, we have some of the best documentation, wiki page, and support team around. We believe in handing you the weapons to fight for privacy but also give you the knowledge to better understand how and why things work the way they do. And hopefully, you can look to us as a beacon of hope that you can also learn how to utilize command line and better arm yourself and trust 3rd parties even less.
So be the sovereign individual you wish to be, arm yourself with the cutting edge **privacy weapons** from Samourai Wallet, and remember:
##### “Privacy is a Human FIGHT!”
module.exports = {
siteUrl: "https://ronindojo.io",
generateRobotsTxt: true, // (optional)
changefreq: "weekly",
};
......@@ -8,6 +8,7 @@
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"postbuild": "next-sitemap",
"start": "next start",
"lint:js": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:css": "stylelint src/css/*.css",
......@@ -18,15 +19,18 @@
"dependencies": {
"gray-matter": "^4.0.2",
"next": "^10.2.0",
"next-sitemap": "^1.6.111",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remark": "^13.0.0",
"remark-html": "^13.0.1"
"remark-html": "^13.0.1",
"rss": "^1.2.2"
},
"devDependencies": {
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/rss": "^0.0.28",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.25.0",
......
import React, { FC, Fragment } from "react";
import React, { FC } from "react";
import Link from "next/link";
export const Layout: FC = ({ children }) => {
return (
<Fragment>
<div className="layout">
<nav id="home" className="grey darken-4" role="navigation">
<div className="nav-wrapper container">
<Link href="/">
......@@ -42,6 +42,11 @@ export const Layout: FC = ({ children }) => {
<a>Resources</a>
</Link>
</li>
<li>
<Link href="/blog">
<a>Blog</a>
</Link>
</li>
<li>
<a target="_blank" rel="noreferrer" href="https://wiki.ronindojo.io">
Wiki
......@@ -56,30 +61,27 @@ export const Layout: FC = ({ children }) => {
</li>
<li>
<Link href="/project">
<a className="white-text">
Project
</a>
<a className="white-text">Project</a>
</Link>
</li>
<li>
<Link href="/#features">
<a className="white-text">
Features
</a>
<a className="white-text">Features</a>
</Link>
</li>
<li>
<Link href="/#products">
<a className="white-text">
Products
</a>
<a className="white-text">Products</a>
</Link>
</li>
<li>
<Link href="/#resources">
<a className="white-text">
Resources
</a>
<a className="white-text">Resources</a>
</Link>
</li>
<li>
<Link href="/blog">
<a className="white-text">Blog</a>
</Link>
</li>
<li>
......@@ -94,7 +96,9 @@ export const Layout: FC = ({ children }) => {
</div>
</nav>
{children}
<div className="content">
{children}
</div>
<footer id="resources" className="page-footer grey darken-3">
<div className="container">
......@@ -122,9 +126,7 @@ export const Layout: FC = ({ children }) => {
</li>
<li>
<Link href="/project">
<a className="red-text text-darken-2">
Project
</a>
<a className="red-text text-darken-2">Project</a>
</Link>
</li>
<li>
......@@ -191,6 +193,6 @@ export const Layout: FC = ({ children }) => {
</div>
</div>
</footer>
</Fragment>
</div>
);
};
import React, { FC } from "react";
import { Post } from "../../types";
import Link from "next/link";
interface Props {
post: Post;
}
export const Excerpt: FC<Props> = ({ post }) => {
return (
<div style={{ marginBottom: "1rem" }}>
<h3 style={{ marginBottom: "0.1rem" }}>
<Link href={`/blog/${post.slug}/`}>
<a className="red-text text-darken-2">{post.title}</a>
</Link>
</h3>
<small className="grey-text">
{new Date(post.date).toLocaleDateString("en-GB", {
timeZone: "UTC",
year: "numeric",
day: "numeric",
month: "long",
})}{" "}
- {post.author.name}
</small>
<p>{post.excerpt}</p>
<Link href={`/blog/${post.slug}/`}>
<a className="red-text text-darken-2">Read more »</a>
</Link>
</div>
);
};
import React, { FC } from "react";
import Link from "next/link";
interface Props {
currentPage: number;
pages: number;
}
export const Pagination: FC<Props> = ({ currentPage, pages }) => {
const prevLink = currentPage === 2 ? `/blog/` : `/blog/page/${currentPage - 1}/`;
return (
<div className="white-text" style={{ marginTop: "2rem" }}>
{currentPage !== 1 && (
<Link href={prevLink}>
<a className="red-text text-darken-2">Previous</a>
</Link>
)}{" "}
<span style={{ marginLeft: "0.5rem", marginRight: "0.5rem" }}>
{currentPage} of {pages}
</span>{" "}
{currentPage !== pages && (
<Link href={`/blog/page/${currentPage + 1}/`}>
<a className="red-text text-darken-2">Next</a>
</Link>
)}
</div>
);
};
......@@ -13,6 +13,10 @@ nav .brand-logo {
color: rgb(192, 19, 19);
}
nav ul a:link:hover {
text-decoration: none;
}
body {
background-color: #424242;
}
......@@ -20,6 +24,7 @@ body {
p {
line-height: 2rem;
color: #eee;
font-weight: 300;
}
.grey img {
......
......@@ -8,6 +8,21 @@
* id > class > element
*/
html, body, #__next {
min-height: 100%;
height: 100%;
}
.layout {
display: flex;
flex-direction: column;
height: 100%;
}
.content {
flex: 1;
}
h1 {
font-family: "Hammersmith One", sans-serif;
padding-top: 50px;
......@@ -17,6 +32,11 @@ h4 h5 {
font-family: "Roboto Slab", serif;
}
a:link:hover {
color: #d32f2f;
text-decoration: underline;
}
@media only screen and (max-width: 768px) {
#home {
min-height: 61px;
......
import { promises as fs } from "fs";
import { join } from "path";
import matter from "gray-matter";
import { Post } from "../types";
export const POSTS_PER_PAGE = 5;
const postsDirectory = join(process.cwd(), "_posts");
export const getPostSlugs = async (): Promise<string[]> => {
const postPaths = await fs.readdir(postsDirectory);
const slugs = postPaths.filter((path) => path.endsWith(".md")).map((path) => path.replace(/\.md$/, ""));
return slugs;
};
export const getPostBySlug = async (slug: string): Promise<Post> => {
const fullPath = join(postsDirectory, `${slug}.md`);
const fileContents = await fs.readFile(fullPath, "utf-8");
const { data, content } = matter(fileContents);
return {
...data,
slug,
content,
} as Post;
};
export const getAllPosts = async (): Promise<Post[]> => {
const slugs = await getPostSlugs();
const allPosts = await Promise.all(slugs.map((slug) => getPostBySlug(slug)));
const sortedPosts = [...allPosts].sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
return sortedPosts;
};
import { promises as fs } from "fs";
import { join } from "path";
import RSS from "rss";
import { Post } from "../types";
export const generateRSS = async (posts: Post[]): Promise<void> => {
const feed = new RSS({
title: "RoninDojo.io",
description:
'RoninDojo is a Samourai Wallet Community-driven Bitcoin full node stack optimized for Single Board Computers, like the Raspberry Pi 4. Our user interfaces and powerful features are fit for any level Bitcoiner. RoninDojo is focused on providing the tools you need to safely and securely use your Samourai Wallet and Hardware Wallet so you can "Be your own Master!".',
feed_url: `https://ronindojo.io/rss.xml`,
site_url: "https://ronindojo.io",
image_url: `https://ronindojo.io/assets/favicon-32x32.png`,
managingEditor: "ronindojo@ronindojo.io (RoninDojo)",
webMaster: "ronindojo@ronindojo.io (RoninDojo)",
copyright: `${new Date().getFullYear()} Ronin Dev Group`,
language: "en",
pubDate: new Date('2020-10-01').toISOString(),
ttl: 60,
});
posts.forEach((post) => {
feed.item({
title: post.title,
description: post.excerpt,
url: `https://ronindojo.io/blog/${post.slug}`,
author: post.author.name,
date: new Date(post.date || 0).toISOString(),
});
});
const path = join(process.cwd(), "public", "rss.xml");
await fs.writeFile(path, feed.xml(), "utf8");
console.log(`generated RSS feed`);
};
import remark from "remark";
import html from "remark-html";
export const markdownToHtml = async (markdown: string): Promise<string> => {
const result = await remark().use(html).process(markdown);
return result.toString("utf-8");
};
......@@ -31,8 +31,10 @@ const MyApp = ({ Component, pageProps }) => {
<link rel="canonical" href="https://ronindojo.io" />
<link rel="alternate" type="application/rss+xml" title="RoninDojo RSS" href="https://ronindojo.io/rss.xml" />
<link
href="https://fonts.googleapis.com/css2?family=Hammersmith+One&family=Raleway:wght@300&family=Roboto+Slab&display=swap"
href="https://fonts.googleapis.com/css2?family=Hammersmith+One&family=Raleway:wght@300;400;500&family=Roboto+Slab&display=swap"
rel="stylesheet"
/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
......
import React from "react";
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { getPostBySlug, getPostSlugs } from "../../lib/blog";
import { markdownToHtml } from "../../lib/markdownToHtml";
import { Post } from "../../types";
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const PostPage: NextPage<Props> = ({ post }) => {
return (
<div className="container">
<article className="section">
<Head>
<title>{post.title} | RoninDojo.io</title>
<meta property="og:title" content={`${post.title} | RoninDojo.io`} />
<meta name="twitter:title" content={`${post.title} | RoninDojo.io`} />
<meta property="og:url" content={`https://ronindojo.io/blog/${post.slug}/`} />
<link rel="canonical" href={`https://ronindojo.io/blog/${post.slug}/`} />
<meta property="og:type" content="article" />
<meta property="og:description" content={post.excerpt} />
<meta name="twitter:description" content={post.excerpt} />
</Head>
<h1 className="white-text" style={{ marginBottom: "0.1rem" }}>
{post.title}
</h1>
<div>
<small className="grey-text">
{new Date(post.date).toLocaleDateString("en-GB", {
timeZone: "UTC",
year: "numeric",
day: "numeric",
month: "long",
})}{" "}
- {post.author.name}
</small>
</div>
<div dangerouslySetInnerHTML={{ __html: post.content }} className="white-text" />
</article>
<Link href="/blog/">
<a className="red-text text-darken-2">« Back to Blog</a>
</Link>
</div>
);
};
export default PostPage;
type Params = {
slug: string;
};
export const getStaticProps: GetStaticProps<{ post: Post }, Params> = async ({ params }) => {
const post = await getPostBySlug(params.slug);
const content = await markdownToHtml(post.content || "");
return {
props: {
post: {
...post,
content,
},
},
};
};
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const slugs = await getPostSlugs();
return {
paths: slugs.map((slug) => {
return {
params: {
slug,
},
};
}),
fallback: false,
};
};
import React, { Fragment } from "react";
import { GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import Head from "next/head";
import { Post } from "../../types";
import { getAllPosts, POSTS_PER_PAGE } from "../../lib/blog";
import { Excerpt } from "../../components/blog/Excerpt";
import { Pagination } from "../../components/blog/Pagination";
import { generateRSS } from "../../lib/generateRSS";
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const BlogPage: NextPage<Props> = ({ posts, pages }) => {
return (
<div className="container">
<Head>
<title>Blog | RoninDojo.io</title>
<meta property="og:title" content="Blog | RoninDojo.io" />
<meta name="twitter:title" content="Blog | RoninDojo.io" />
<meta property="og:url" content="https://ronindojo.io/blog/" />
<link rel="canonical" href="https://ronindojo.io/blog/" />
{pages > 1 && <link rel="next" href={`https://ronindojo.io/blog/page/2/`} />}
</Head>
<div className="section">
<h1 className="white-text">Blog</h1>
<div>
{posts.map((post) => (
<Fragment key={post.date}>
<Excerpt post={post} />
<hr />
</Fragment>
))}
</div>
<Pagination currentPage={1} pages={pages} />
</div>
</div>
);
};
export default BlogPage;
export const getStaticProps: GetStaticProps<{ posts: Post[]; pages: number }> = async () => {
const posts = await getAllPosts();
const pages = Math.ceil(posts.length / POSTS_PER_PAGE);
await generateRSS(posts);
return {
props: {
posts: posts.slice(0, POSTS_PER_PAGE),
pages,
},
};
};
import React, { Fragment } from "react";
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import Head from "next/head";
import { Post } from "../../../types";
import { getAllPosts, getPostSlugs, POSTS_PER_PAGE } from "../../../lib/blog";
import { Excerpt } from "../../../components/blog/Excerpt";
import { Pagination } from "../../../components/blog/Pagination";
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const BlogPagesPage: NextPage<Props> = ({ posts, page, pages }) => {
const prevLink = page === 2 ? `/blog/` : `/blog/page/${page - 1}/`;
return (
<div className="container">
<Head>
<title>Blog | RoninDojo.io</title>
<meta property="og:title" content="Blog | RoninDojo.io" />
<meta name="twitter:title" content="Blog | RoninDojo.io" />
<meta property="og:url" content={`https://ronindojo.io/blog/page/${page}/`} />
<link rel="canonical" href={`https://ronindojo.io/blog/page/${page}/`} />
{page !== 1 && <link rel="prev" href={`https://ronindojo.io${prevLink}`} />}
{page !== pages && <link rel="next" href={`https://ronindojo.io/blog/page/${page + 1}/`} />}
</Head>
<div className="section">
<h1 className="white-text">Blog</h1>
<div>
{posts.map((post) => (
<Fragment key={post.date}>
<Excerpt post={post} />
<hr />
</Fragment>
))}
</div>
<Pagination currentPage={page} pages={pages} />
</div>
</div>
);
};
export default BlogPagesPage;
type Params = {
page: string;
};
type StaticProps = {
posts: Post[];
pages: number;
page: number;
};
export const getStaticProps: GetStaticProps<StaticProps, Params> = async ({ params }) => {
const allPosts = await getAllPosts();
const pages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
const page = Number(params.page);
const posts = allPosts.slice(page * POSTS_PER_PAGE - POSTS_PER_PAGE, page * POSTS_PER_PAGE);
return {
props: {
posts,
pages,
page,
},
};
};
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const slugs = await getPostSlugs();
const pages =