mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-03-21 17:24:28 -05:00
feat(blog): add /blog page generation, add /blog preview generation
This commit is contained in:
parent
8d50d0b559
commit
a78baa1b61
20
blogposts/_example.md
Normal file
20
blogposts/_example.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
title: "Title"
|
||||
author: "Monty"
|
||||
author_image: "https://www.github.com/montylion.png"
|
||||
date: "13 September 2021"
|
||||
caption: "The caption that will show up on the card for this post on the /blogs page"
|
||||
cover_image: "An url to the image that will also show up on the card for this post on the /blogs page"
|
||||
---
|
||||
|
||||
This is a series of paragraphs. According to Wikipedia, a paragraph (from the Ancient Greek παράγραφος, parágraphos, "to write beside") is a self-contained unit of discourse in writing dealing with a particular point or idea.
|
||||
|
||||
A paragraph consists of one or more sentences. Though not required by the syntax of any language, paragraphs are usually an expected part of formal writing, used to organize longer prose.
|
||||
|
||||
*This example uses material from the Wikipedia article ["Paragraph"](https://en.wikipedia.org/wiki/Paragraph), which is released under the [Creative Commons Attribution-Share-Alike License 3.0](https://creativecommons.org/licenses/by-sa/3.0/)*
|
||||
|
||||

|
||||
|
||||
<iframe src="https://www.youtube.com/embed/wjWQwRlmNho" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
Blogposts whose filename starts with a _ will not show up on the /blogs page, but will still be accessible from the url.
|
||||
24
blogposts/progress-report-sept-21.md
Normal file
24
blogposts/progress-report-sept-21.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
title: "Progress report for Sept. 2021"
|
||||
author: "The Dev Team"
|
||||
author_image: "https://www.github.com/PretendoNetwork.png"
|
||||
date: "14 Sept. 2021"
|
||||
caption: "Here's our progress report for September 2021 <strong>(early draft, do not leak)</strong>"
|
||||
cover_image: "https://media.discordapp.net/attachments/413884110667251722/884271460724658216/image0.jpg"
|
||||
---
|
||||
|
||||
This is just a test post.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis placerat sapien nibh, a elementum urna finibus sed. Aliquam semper venenatis tempor. Donec tincidunt suscipit dapibus. Integer id diam dictum risus hendrerit efficitur. Sed in blandit arcu, ac feugiat libero. Donec quis accumsan leo, ac scelerisque sapien.
|
||||
|
||||

|
||||
|
||||
Mauris non blandit tortor. Quisque tristique maximus lorem vel malesuada. Donec nulla eros, vehicula at odio at, fermentum tempus turpis. Duis tempor augue metus, id posuere est lobortis at. Maecenas eget accumsan justo, eget aliquam quam. Etiam gravida lacus ac nibh maximus fermentum. Etiam gravida dolor at libero lacinia sollicitudin.
|
||||
|
||||
<iframe src="https://www.youtube.com/embed/NW0Ir9tRSJY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
Pellentesque dictum ligma arcu, eget molestie nulla porta id. Ut luctus felis turpis, a fringilla neque venenatis non. Phasellus mattis tristique dui id lobortis. Mauris facilisis risus nec pellentesque commodo.
|
||||
|
||||
Sed facilisis molestie eros vulputate accumsan. Phasellus feugiat auctor lacus, eu varius arcu tempor nec. Phasellus sodales aliquam mi non fringilla. Proin elementum velit in nulla iaculis malesuada.
|
||||
|
||||
Bottom text.
|
||||
15
blogposts/todo.md
Normal file
15
blogposts/todo.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: "Todo"
|
||||
author: "Monty"
|
||||
author_image: "https://www.github.com/montylion.png"
|
||||
date: "19 January 2038"
|
||||
caption: "A todo list"
|
||||
cover_image: "https://i.imgur.com/sOWLwO5.png"
|
||||
---
|
||||
|
||||
- <input type="checkbox" onclick="return false;">
|
||||
Redesign by the one true design god jvs, then
|
||||
- <input type="checkbox" onclick="return false;">
|
||||
Add mobile support
|
||||
- <input type="checkbox" onclick="return false;">
|
||||
Add custom opengraph for each post
|
||||
3222
package-lock.json
generated
3222
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -23,9 +23,14 @@
|
|||
"express-handlebars": "^4.0.4",
|
||||
"express-locale": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"ioredis": "^4.26.0",
|
||||
"marked": "^3.0.4",
|
||||
"morgan": "^1.10.0",
|
||||
"redis-json": "^5.0.0",
|
||||
"trello": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
.all-blog-posts {
|
||||
max-width: 900px;
|
||||
.blog-card {
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 1100px;
|
||||
margin-top: 30px;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-card .left {
|
||||
flex: 2 0 55%;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.blog-card .right {
|
||||
display: flex;
|
||||
border-radius: 0 10px 10px 0;
|
||||
height: 260px;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.blog-card .right img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.blog-card h2 {
|
||||
color: white;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.blog-card p, .post-info {
|
||||
|
|
@ -11,22 +41,17 @@
|
|||
font-weight: bold;
|
||||
color: var(--text);
|
||||
}
|
||||
.blog-card iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
.blog-card img {
|
||||
max-width: 100%;
|
||||
margin: 10px auto;
|
||||
display: block;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.blog-card .post-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
}
|
||||
.blog-card .post-info > * + * {
|
||||
margin-left: .5em;
|
||||
|
|
@ -38,4 +63,9 @@
|
|||
font-weight: bold;
|
||||
color: var(--text);
|
||||
align-items: center;
|
||||
height: 32px; /* So the post doesn't shift when the image gets loaded */
|
||||
}
|
||||
.blog-card .profile img {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
max-width: 100%;
|
||||
}
|
||||
58
public/assets/css/blogpost.css
Normal file
58
public/assets/css/blogpost.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.blog-card {
|
||||
padding: 60px;
|
||||
margin: 0 auto;
|
||||
max-width: 1100px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.blog-card .title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blog-card p, .post-info {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.blog a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.blog-card .date {
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
color: var(--text);
|
||||
}
|
||||
.blog-card iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.blog-card img {
|
||||
max-width: 100%;
|
||||
margin: 10px auto;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.blog-card .post-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.blog-card .post-info > * + * {
|
||||
margin-left: .5em;
|
||||
}
|
||||
.blog-card .profile {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 30px auto;
|
||||
grid-gap: 10px;
|
||||
font-weight: bold;
|
||||
color: var(--text);
|
||||
align-items: center;
|
||||
height: 52px; /* So the post doesn't shift when the image gets loaded */
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
|
@ -2,6 +2,10 @@ const { Router } = require('express');
|
|||
const util = require('../util');
|
||||
const router = new Router();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const marked = require('marked');
|
||||
const matter = require('gray-matter');
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
|
||||
|
|
@ -10,10 +14,54 @@ router.get('/', async (request, response) => {
|
|||
|
||||
const localeString = reqLocale.toString();
|
||||
|
||||
const fileList = fs.readdirSync('blogposts');
|
||||
|
||||
// We get the info for each blogpost, ignoring the ones starting with _
|
||||
const postList = fileList
|
||||
.filter(filename => !filename.startsWith('_'))
|
||||
.map((filename) => {
|
||||
const slug = filename.replace('.md', '');
|
||||
const rawPost = fs.readFileSync(path.join('blogposts', `${filename}`), 'utf-8');
|
||||
const { data: postInfo } = matter(rawPost);
|
||||
return {
|
||||
slug, postInfo
|
||||
};
|
||||
});
|
||||
postList.sort((a, b) => {
|
||||
return new Date(b.postInfo.date) - new Date(a.postInfo.date);
|
||||
});
|
||||
|
||||
response.render('blog', {
|
||||
layout: 'main',
|
||||
locale,
|
||||
localeString,
|
||||
postList
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:slug', async (request, response) => {
|
||||
|
||||
const reqLocale = request.locale;
|
||||
const locale = util.getLocale(reqLocale.region, reqLocale.language);
|
||||
|
||||
const localeString = reqLocale.toString();
|
||||
|
||||
// Get the name of the post from the URL
|
||||
const postName = request.params.slug;
|
||||
|
||||
// Get the markdown file corresponding to the post
|
||||
const rawPost = fs.readFileSync(path.join('blogposts', `${postName}.md`), 'utf-8');
|
||||
// Convert the post info into JSON and separate it and the content
|
||||
const { data: postInfo, content } = matter(rawPost);
|
||||
// Convert the content into HTML
|
||||
const htmlPost = marked(content);
|
||||
|
||||
response.render('blogpost', {
|
||||
layout: 'main',
|
||||
locale,
|
||||
localeString,
|
||||
postInfo,
|
||||
htmlPost
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,47 +12,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="all-blog-posts">
|
||||
<div class="purple-card blog-card">
|
||||
|
||||
<div class="post-info">
|
||||
<span>Published by</span>
|
||||
<div class="profile">
|
||||
<img class="profile-picture" src="https://jipfr.nl/icon.png" alt>
|
||||
<span>Jip Frijlink</span>
|
||||
{{#each postList }}
|
||||
<a href="/blog/{{this.slug}}" class="purple-card blog-card">
|
||||
<div class="left">
|
||||
<h2>{{{ this.postInfo.title }}}</h2>
|
||||
<p>{{{ this.postInfo.caption }}}</p>
|
||||
<div class="post-info">
|
||||
<span>Published by</span>
|
||||
<div class="profile">
|
||||
<img class="profile-picture" src="{{{ this.postInfo.author_image }}}" alt>
|
||||
<span>{{{ this.postInfo.author }}}</span>
|
||||
</div>
|
||||
<span>on</span>
|
||||
<span class="date">{{{ this.postInfo.date }}}</span>
|
||||
</div>
|
||||
<span>on</span>
|
||||
<span class="date">1 Sept. 2021</span>
|
||||
</div>
|
||||
|
||||
<h2>Progress report for Sept. 2021</h2>
|
||||
|
||||
<p>It is, but that's the point of the Patreon. Hopefully the Patreon will take off and I can make Pretendo development a full time job</p>
|
||||
|
||||
<p>The way game servers work on the 3DS and WiiU is through a service called NEX, which Nintendo licensed from a different company</p>
|
||||
|
||||
<p>NEX acts as a middleware basically. It provides a set of protocols (auth, secure connection, matchmaking, etc) and each protocol has a set of methods associated with it and each method tends to have its own dedicated custom types</p>
|
||||
|
||||
<img src="https://images-ext-2.discordapp.net/external/uh2DBuQss29KtEKJEmIkIUArxAhKbpmxfcTrS9dvLao/https/pbs.twimg.com/media/E_DzxqmX0AIv8Sn.jpg%3Alarge?width=526&height=702">
|
||||
|
||||
<p>So like every game implements AuthenticationProtocol::Login and AuthenticationProtocol::LoginEx to login with, both of which provide a AuthenticationInfo data structure</p>
|
||||
|
||||
<p>Every game server is made up of a mix of implemented protocols and supported methods. The trouble is that Nintendo basically hacked NEX support into their games so a lot of stuff isn't clear because NEX is a set of generic protocols and methods, and Nintendo tends to abuse them and even fork them into Nintendo-specific protocols and methods</p>
|
||||
|
||||
<p>For example NEX provides a protocol called DataStore, which is basically a file database with some metadata associated with the files. SMM forks DataStore into it's own DataStore(SMM) protocol, which several new (sometimes replaced) methods</p>
|
||||
|
||||
<p>SMM is basically 90% vanilla DataStore and DataStore(SMM), but even though Nintendo forked DataStore into their own protocol they still reuse a lot of the same generic DataStore types and data structures which leads to confusing cases of unused and weirdly used fields</p>
|
||||
|
||||
<iframe src="https://www.youtube.com/embed/wjWQwRlmNho" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
<p>Like for example in DataStore(SMM) there's a method called DataStore(SMM)::RecommendedCourseSearchObject which is called to get the Course World list, and it returns a list of DataStoreCustomRankingResult data structures. Inside this data structure is 2 notable fields; MetaBinary and Ratings. MetaBinary is a NEX::Buffer type which has data like the course type, some crc32's, etc. Ratings though is the weird one. It actually doesn't store any ratings for the course, that's what the Score field does. Ratings is used to track the number of times people have attempted the course</p>
|
||||
|
||||
<p>But because these protocols and methods are very generic and shared between multiple games, it means knowledge of one game can't really be reused for another. Like Splatoon also implements DataStore, and also forks it into DataStore(Splatoon), and things like MetaBinary in that game are completely different</p>
|
||||
|
||||
<p>And then to top it all off, there are MANY versions of NEX being used by Nintendo so several games are also slightly different at the transport level (like does the server support acknowledging multiple packets at once, do the data structures have a header or not, etc). And Nintendo would update the NEX library per-game at times without documenting any version changes internally, so it's all guess work at the start as to what each game supports</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<img src="{{this.postInfo.cover_image}}" class="cover" />
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
|
||||
{{> footer }}
|
||||
|
||||
|
|
|
|||
26
views/blogpost.handlebars
Normal file
26
views/blogpost.handlebars
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<link rel="stylesheet" href="/assets/css/blogpost.css" />
|
||||
|
||||
<div class="wrapper blog">
|
||||
|
||||
{{> header}}
|
||||
|
||||
<div class="purple-card blog-card">
|
||||
|
||||
<h1 class="title">{{{ postInfo.title }}}</h1>
|
||||
<div class="post-info">
|
||||
<span>Published by</span>
|
||||
<div class="profile">
|
||||
<img class="profile-picture" src="{{ postInfo.author_image }}" alt="{{ postInfo.author }}">
|
||||
<span>{{{ postInfo.author }}}</span>
|
||||
</div>
|
||||
<span>on</span>
|
||||
<span class="date">{{{ postInfo.date }}}</span>
|
||||
</div>
|
||||
|
||||
{{{ htmlPost }}}
|
||||
|
||||
</div>
|
||||
|
||||
{{> footer }}
|
||||
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user